← Blog

12,000 empty folders came back from the dead

A client keeps its project files in SharePoint. Live projects sit in a client site split into libraries by letter. When a project finishes, it moves to a separate archive site, and the live site stays clean.

Then someone got a new PC.

Their OneDrive synced the project libraries, working from an old local cache. That cache still remembered folders for projects that had since been finished and moved to the archive, and it helpfully put them back. Not the files, just the folders: thousands of empty skeletons of finished jobs, recreated in the live site as if nobody had ever archived them.

It had happened once before, on another person’s device, at about 10,000 folders. This time it was worse.

The fingerprint

You can’t just delete every empty folder. Empty folders are normal; people make them for new jobs all the time. I needed something only the resurrected ones had.

It was in the timestamps. Every item in SharePoint has a Created date (when the server first saw it) and a Modified date. Normally Modified is the same as Created or later. But the cache had stamped every folder with the same Modified date, 17 May, and then uploaded them days afterwards. So these folders were modified before they were created.

That became the rule. A folder had to match all four:

Modified on the cache's stamp day (17 May)
Created on the server days later          # Modified before Created
Last edited by the new PC's user
Zero bytes anywhere beneath it

Any one of those on its own would catch innocent folders. All four together picked out the ghosts.

The audit

A read-only script walked all six live libraries over Microsoft Graph and applied the fingerprint.

LibraryResurrected folders
One10,685
Another1,259
A third8
The other threenone

About 12,000 empty folders, sitting under 98 project folders.

Empty meant deleting them couldn’t lose any data. But “can’t lose data” isn’t the same as “safe to delete”. If one of those folders belonged to a live project someone was about to fill, removing it would still cause a mess. So there was a second test: for each of the 98 projects, look up the same path in the archive site and check the real files were there.

95 had an exact match in the archive, holding the real content. One project was sitting at nearly 2 GB in the archive and 0 bytes in the live site. Those 95 were marked safe.

Deleting carefully

The delete script had four rules, and they were the actual work:

  • Dry run by default. It deletes nothing unless you pass --confirm, and --limit caps how many projects one run can touch.
  • Check again at delete time. Before removing each project folder, it re-checked that it was still empty and still matched the archive. The audit was a snapshot, and anything could have changed since.
  • Recycle bin, not gone. Everything went to the SharePoint recycle bin, where it can be restored for 93 days.
  • Write down every path and item id removed, so a restore wouldn’t depend on anyone’s memory.

95 project folders deleted, zero failures. A fresh audit afterwards found only the three folders I’d held back.

The three that waited

Three projects failed the archive match, and they’re still there.

One is in the archive under almost the same name, minus a dash. It’s very probably the same project, but “very probably” isn’t a match. The other two aren’t in the archive at all, which most likely means they’re live jobs.

All three are empty, so deleting them would lose nothing. They wait anyway, because the rule for deleting automatically was a clean archive match, and anything that doesn’t pass it goes to a person. That’s a question for the client, not a judgement for a script.

A side note on tools

The obvious tool was PnP PowerShell, the usual choice for bulk SharePoint work. It dead-ends on this tenant: its app-only login with a client secret relies on an old SharePoint authentication service that’s switched off here. Going straight to Graph with a certificate-authenticated app registration worked.

Stopping the next one

This will happen again the next time someone gets a new PC. The clean-up is now two reusable scripts, but the better fix is catching it on day one. A post-sync check belongs in the client’s new-PC setup list, so the next ghost shows up as a handful of folders on the day, not twelve thousand after the fact.

The parts I’d reuse anywhere:

  • An odd timestamp is a fingerprint. Modified before Created is rare enough to be a clue.
  • Stack the conditions. Any single test would catch innocent folders. All four together don’t.
  • Re-check at the moment you act. An audit is a snapshot, and the delete happens later.
  • Delete somewhere you can get it back from, and keep a list of what you took.
  • Automate only the clean matches. Everything else goes to a person.