[2.4.7] - Bucket Routing & Storage Foundations
Added
- Core Pool Storage Buckets —
StorageBucketnow supportsisCore(boolean) andpriority(integer) fields. Core buckets are shared across all users via least-filled load balancing (fileCount ASC, priority DESC). Dedicated buckets are assigned per-user or per-squad. Priority breaks ties when two core buckets have equal file counts — higher number wins. Default is0(no preference). File → StorageBucketrelation — Files now track which bucket they were stored in viastorageBucketId. This is used at serve time to route reads, streams, deletes, OCR, and thumbnail generation to the correct bucket rather than the global default.- User bucket assignment UI — New
UserBucketAssignmentcomponent in the admin settings Storage tab. Paginated user list with per-user bucket dropdown and save button. Clears assignment (falls back to core pool) or assigns a dedicated bucket; triggers credentials email on assignment. - Admin storage bucket manager —
isCore/priorityUI — Edit dialog now includes a Core Pool Bucket toggle and a Priority number input (visible whenisCoreis on). List rows show Core Pool badge (violet), Dedicated badge, file count, and priority for core buckets. - Bucket connection test — per-bucket provider —
POST /api/admin/storage/buckets/[id]/testnow uses an explicitselectinfindUnique(fixes a@prisma/adapter-pgdriver adapter issue that causedfindUniquewithout a select to returnnullfor existing records). Test error detail is now surfaced inline in the UI alongside the status message. PUT /api/admin/storage/buckets—priorityon create — Create route now reads and persistspriorityfrom the request body. Previously new buckets always defaulted to0regardless of the value submitted.- User bucket page — secret key reveal — The
/dashboard/bucketpage now shows the actual secret access key with a show/hide toggle (BucketSecretRevealclient component). All other credentials are hidden behind a "coming soon" notice while direct S3 credential access is being developed. reconcileDeprovisionedBuckets— atomicSubscription.statusupdate — Both cancellation paths (cancelled subscription, subscription not found in Stripe) now useprisma.$transactionto atomically update the bucket (provisionStatus → deprovisioning), subscription (status → canceled), and clear user assignments in a single operation.
Fixed
- File serving — wrong storage provider for bucket-routed files — All file read/stream/delete paths used
getStorageProvider()(global config bucket singleton) even when the file was stored in a core or dedicated bucket. Affected routes:GET /api/files/[...path],GET /api/files/[id]/thumbnail, suggestion approval inPUT /api/files/[id]/suggestions, OCR processing, file expiry handler, and single/bulk delete in the file service. All now usegetProviderForStoredFile(file.storageBucketId), which routes to the correct bucket and falls back to global for legacy files. - S3 key normalization — Windows backslash paths —
path.join()produces backslash-separated paths on Windows (e.g.uploads\userUrlId\filename.png). The S3 provider's key derivation regex/^uploads\//never matched these, leaving the full backslash path as the S3 key and causingNoSuchKeyon every read. AddedtoS3Key()helper that normalizes backslashes to forward slashes before stripping theuploads/prefix; applied to all five S3 provider methods (uploadFile,getFile,getFileStream,deleteFile,getFileUrl,getFileSize). - Upload path generation — forward slashes enforced —
app/api/files/route.tsandapp/api/files/chunks/route.tsusedpath.join()to buildfilePath, producing backslash paths on Windows that were stored in the database and used as S3 keys. Both now usepath.posix.join()so paths are always forward-slash regardless of platform. - OCR — wrong bucket —
processImageOCRTaskcalledgetStorageProvider()unconditionally. It now looks upfile.storageBucketIdand routes togetProviderForStoredFileso OCR reads from the same bucket the file was uploaded to. - Bucket list —
prioritymissing from select —GET /api/admin/storage/bucketsdid not includepriorityin its field select. The edit dialog receivedpriority: undefined, causing the priority input to be uncontrolled (React warning) and saving to silently leave the existing value unchanged. - Edit dialog — uncontrolled
priorityinput —openEditnow usesbucket.priority ?? 0when populating the form, preventing the React "uncontrolled → controlled" warning whenprioritywas absent from the list response. - Dev logger — error detail suppressed — The development pino transport only printed
obj.msg, silently droppingobj.error. Error message and stack trace are now appended to the console output. - Chunks route — Prisma XOR type error —
tx.file.createmixedstorageBucketId(unchecked input) withuser: { connect: { id } }(checked input), which Prisma's XOR type rejects. Replaced withuserIdscalar field.
Changed
getStorageProvider()scope clarified — The global singleton is now the fallback only for legacy files (storageBucketId = null). All new file operations route throughgetProviderForStoredFileorgetUploadBucketForUser/getUploadBucketForSquad.selectCoreBucketordering — Core bucket selection now orders byfileCount ASC, priority DESCso higher-priority buckets win when file counts are equal.
What's Changed
- Dev (v2.4.7) by @CodeMeAPixel in #107
Full Changelog: v2.4.6...v2.4.7