Resources Folder Management#
Overview#
Folders in the Resources UI are virtual constructs β not real directories on disk. When a user creates a folder via "New Folder", the server writes a single DB record (a .folder placeholder attachment) with name: '.folder' and type: 'folder' but creates no physical file or directory . All folder-scoped queries rely on the perfixPath column (a comma-separated path like parent,child) stored on every attachments row.
Key Files#
| File | Role |
|---|---|
server/routerTrpc/attachment.ts | tRPC endpoints: createFolder, rename, delete, move, list |
server/lib/files.ts | FileService β filesystem and S3 operations (moveFile, deleteFile, etc.) |
app/src/store/resourceStore.tsx | MobX store driving the Resources UI, folder navigation, and refresh |
How Folder Records Work#
- Creation:
createFolderinserts a row withpath = /api/file/<parent>/<folderName>/.folder,name = '.folder',type = 'folder', andperfixPath = <comma-path>. No directory is created on disk . - Listing: The
listendpoint runs a raw SQLWITH combined_itemsquery thatDISTINCT ON (folder_name)usingperfixPathto aggregate files into folder rows. Both note-linked and standalone attachments are included viaOR [note.accountId, accountId]. - Navigation:
ResourceStoretrackscurrentFolderas a/-separated string; the server converts it to a,-separatedperfixPathinternally .
Known Bugs (issue #1128)#
1. Rename throws ENOENT#
Root cause: The rename endpoint iterates all attachments whose perfixPath starts with the old folder path and calls FileService.moveFile(oldPath, newPath) for each one . The .folder placeholder has no physical file, so moveFile throws ENOENT: no such file or directory /app/.blinko/files/<name>/.folder .
2. Delete shows "Success" but folder persists#
Root cause: The delete endpoint queries attachments using only note.accountId (via the note relation join) . The .folder placeholder is stored with a direct accountId and no noteId, so the query returns zero rows. The code then hits the early return { success: true } without deleting anything from the DB .
3. DB records not cleaned up after deletion#
Even when actual files in a folder were found and deleted from disk, the corresponding attachments rows were never removed β leaving orphaned DB records .
4. UI always shows "Success" toast on failure#
PromiseCall catches errors and returns undefined rather than re-throwing. When wrapped in ToastPlugin.promise(), a resolved-undefined is treated as success, masking the backend ENOENT .
Fix (PR #1169)#
PR #1169 addresses all three backend bugs in server/routerTrpc/attachment.ts:
- Rename: Skip
FileService.moveFile()whenattachment.name === '.folder'orattachment.type === 'folder'; only update the DB record with the new path . - Delete query: Change the folder
findManytoOR [note.accountId, accountId]so the.folderplaceholder is included . - DB cleanup: Add
tx.attachments.deleteMany()after physical file deletion (folder path) andtx.attachments.delete()after single-file deletion . - Skip placeholder on delete: Skip
FileService.deleteFile()for.folderentries to avoid ENOENT when deleting empty folders .
Other Gotchas#
- Missing
depthfield oncreateFolder: The endpoint does not set thedepthcolumn , while file uploads do. This can cause the root-level listing query (which filtersdepth = 0) to miss or mis-sort standalone folder placeholders . - Docker permission issues: If
/app/.blinko/filesis owned byrootbut the app runs as a different user,moveFile/deleteFilealso fail with ENOENT or permission errors regardless of the placeholder bug . - UI state refresh:
ResourceStore.refreshTickertriggers a reload viauseEffect; callers must increment it after mutations or the UI will show stale data .