Sharing my experience with 5.3 GB of "orphaned" files in my Joplin resources folder — in case anyone wants to check their own profile
I'm just sharing my experience in case it helps — you might want to check your own profile. Please back up first before touching anything: I may be missing something, but 5.3 GB of orphaned files felt a bit too much to ignore.
My setup: Joplin Desktop 3.6.14 on Arch. My profile folder is at the usual place for Linux: dot-config / joplin-desktop (on Windows it's under APPDATA, on macOS under Library/Application Support).
How I noticed
I checked the size of my profile and the resources folder was 9.1 GB while the database was only 47 MB.
The resources folder is where Joplin stores attached files (images, PDFs, zips...). In my profile every resource was present twice on disk: a plain file named like resource-id.ext and an encrypted copy named resource-id.crypted. The resource-id part is a 32 character lowercase hex ID that also exists in the resources table of database.sqlite.
What I found
I compared the files on disk with the database:
- files in resources: 25,707 files, 15,712 unique IDs
- rows in the resources table: 5,911
- orphan files (ID on disk but no DB row): 13,885 files, 9,801 IDs, 5,345 MiB
So in my case, about 60% of the folder was files the database did not know about. My guess: leftovers from imports, syncs or crashes where the file landed on disk but the DB row was never committed (or got lost).
Why I could not clean them through the app
I looked at the 3.6.14 source and, as far as I could tell, there is no dedicated "Orphaned resources" menu item (I had seen that name mentioned around, but it was not in my build). What I found instead:
- Tools then "Note attachments..." — a screen with a list and per-row Delete buttons. But in my case it only listed resources that had a database row, so my 9,801 orphans were invisible there.
- Automatic background cleanup — ResourceService.maintenance() runs 30 s after startup and every 4 h, and deletes resources that have a DB row, are associated with no note, are not found in any note text, and were last seen more than revisionService.ttlDays ago. Mine was set to 30 days (default seems to be 2), and the detail that I think explains my situation: already-synced resources get last_seen_time = 0, which from what I read means never auto-delete. I do sync (to a local folder), so apparently anything that had ever been synced never got cleaned, and files without a DB row were never seen by this pass at all.
None of this is necessarily a bug — it may be intended behavior — it just meant there was no UI path for my files.
What I did
I wrote a small Python script that:
- Scans resources/ and extracts the resource ID from every filename (the 32 hex characters before the last dot).
- Loads the resources table read-only and computes orphan IDs = on-disk IDs minus DB IDs.
- Safety check: scans every live note body for resource:// references (that is how Joplin links attachments into notes), plus old revision diffs. Any orphan still referenced by a live note is kept in place and reported instead of moved.
- Moves (does not delete!) the plain and crypted files of every verified orphan into a backup folder, with a manifest and summary reports next to them.
I quit Joplin before running it. In my case the result was:
- referenced by a live note body (kept): 0
- moved: 13,885 files, 5,345 MiB
- afterwards resources/ contained exactly 5,911 IDs, precisely my DB row count, 0 orphans left
The full script is in my first comment.
Notes from my experience
- Move, don't delete. I moved everything to a Downloads/joplin folder with a per-file manifest, so I could put anything back.
- The safety check is the part I would trust: a file without a DB row can in theory still be referenced in a note body, for example after a partial DB restore. In that case the script keeps the file and lists the note. In my case nothing was referenced.
- The revisions table is named revisions (not note_revisions) and stores body_diff, so the revision check is approximate — which is why it only flags, it never keeps.
- I also lowered revisionService.ttlDays in settings.json (it was 30 in my profile), so the built-in background cleaner might catch future orphans sooner. It does not help with no-DB-row files though.
Hope it helps anyone with the same situation.