feat(core): file domain + tenants + fiely-storage-local plugin - #5
Open
benjaminLedel wants to merge 4 commits into
Open
feat(core): file domain + tenants + fiely-storage-local plugin#5benjaminLedel wants to merge 4 commits into
benjaminLedel wants to merge 4 commits into
Conversation
Adds the file tree the product revolves around, backed by a new pluggable storage layer. Covers items #1 and #2 of the next-priorities plan. Schema (V3, V4): - tenants table with max_upload_bytes setting; seeds a stable default tenant - auth_users.tenant_id with a DB default so the existing jwt-plugin UserRepository keeps working unmodified - files: single self-referential table, folder-XOR-blob CHECK, unique name per (owner, parent) + partial unique index for the root Core: - FileEntity / FileRepository / FileService / FileController with OpenAPI annotations matching AuthController's style - CurrentUserResolver factors bearer-token -> UserInfo + tenant lookup out of the controllers (no Spring Security; matches existing ad-hoc pattern) - Tenant entity + repository; per-tenant upload cap enforced in FileService - REST surface: POST /api/files (multipart), POST /api/files/folder, GET /api/files, GET /api/files/{id}, GET /api/files/{id}/content (streamed), PATCH /api/files/{id} (rename/move), DELETE /api/files/{id} (recursive) Plugin fiely-storage-local: - Implements StorageProvider with UUID-sharded filesystem layout (<root>/<tenant>/<xx>/<yy>/<fileId>/v<N>) - Atomic-move semantics so partial writes never materialise - Path-escape defenses on all ref resolutions Tests: - LocalStorageProviderTest: round-trip, prune-on-delete, path-escape, partial-write rejection - FileControllerTest: end-to-end via H2 + stubbed AuthProvider/StorageProvider; covers upload/list/download/delete, 401/409/413, folder nesting, rename, move-into-descendant rejection - FilePluginIntegrationTest: real Postgres + real plugin JARs + real JWT, round-trips bytes through the filesystem (skipped without Docker, same pattern as AuthPluginIntegrationTest) Build: - kotlin("plugin.jpa") applied to fiely-core so Hibernate can instantiate entities - copyTestPlugins now bundles fiely-storage-local alongside fiely-auth-jwt Configuration: - fiely.storage.provider (active provider id) - fiely.plugins.fiely-storage-local.root (blob root) - spring.servlet.multipart.max-file-size raised to 1GB as a transport ceiling; per-tenant limit is the real cap
Adds a structure for per-file metadata: namespaced JSON documents keyed by
(file_id, namespace). Each producer owns its namespace — `user` for
caller-supplied tags/ratings/notes, `exif`/`pdf` for extractors,
`fiely-ai-*` for model outputs — so writes don't collide.
Schema (V5):
- file_metadata(file_id, namespace, data TEXT, timestamps); PK(file_id,
namespace). `data` is TEXT for H2-portability; migrating to JSONB when we
need GIN is a one-line ALTER.
Core:
- FileMetadataEntity with @EmbeddedId (FileMetadataId)
- FileMetadataRepository with list/delete helpers
- FileMetadataService with auth-scoped CRUD; namespace validated against
[A-Za-z0-9][A-Za-z0-9._-]* so plugin ids fit
- FileMetadataController under /api/files/{id}/metadata[/{namespace}]:
GET list, GET one, PUT (upsert verbatim), DELETE
Refactors:
- FileService.delete no longer relies on DB ON DELETE CASCADE to prune
subtrees. Hibernate's auto-generated DDL (used in H2-backed tests)
doesn't carry our entities' FK cascades, so the service now walks the
subtree, drops blobs via StorageProvider, nukes metadata rows, then
deletes file rows children-first. Works identically in Postgres.
- File exception handlers moved out of FileController into a shared
@ControllerAdvice scoped to cloud.fiely.file.web so both controllers
map NotFoundException / ConflictException / etc. to HTTP statuses.
- Download tests now use asyncDispatch, the correct pattern for
StreamingResponseBody in MockMvc.
Tests:
- FileMetadataControllerTest: PUT/GET/list/DELETE round-trip, multi-
namespace coexistence, cascade-on-file-delete, cross-owner 404,
invalid-namespace 400, missing-namespace 404, unauthenticated 401
Adds the post-login experience: React Router, JWT session management,
an app shell header, and a full file browser that exercises every file
API endpoint.
Routing:
- / → redirects to /files (authenticated) or /login (not)
- /login → login page, redirects to /files if already authenticated
- /files/:folderId? → file browser, scoped to a folder or root
Auth:
- AuthProvider context wraps the app; calls /api/auth/me on mount to
verify the stored token and resolve the current user
- useAuth() hook exposes { user, loading, logout, refreshUser }
- apiFetch() wrapper auto-attaches the Bearer header and redirects to
/login on 401
- Login.tsx updated to use navigate() + refreshUser() instead of a full
page reload
App shell:
- Header with logo, user display name, sign-out button
- Outlet for routed content
- Responsive (name hidden on small screens, icons always visible)
File browser:
- Lists files/folders from GET /api/files (folders first, alphabetical)
- Breadcrumb trail — built on navigation, resolves on deep-link via
walking parentId chain from the API
- Upload (multipart, multi-file) via hidden input + styled button
- Create folder via window.prompt
- Rename via PATCH + window.prompt
- Delete via DELETE + window.confirm (recursive for folders)
- Download via blob URL
- Empty state illustration
- File size formatting
- Hover-reveal action buttons (download, rename, delete)
Dependencies:
- react-router-dom ^7.14.2 (only new dep)
Replace window.prompt/confirm with proper Tailwind modals, add
drag-and-drop upload, and refine the app shell and file list to match
the login page's design quality.
App shell:
- Sticky header with backdrop blur
- User avatar initial circle (brand-colored)
- Subtle separator between user and sign-out
File browser:
- Table layout with Name / Size / Modified columns (responsive)
- Relative timestamps ("3h ago", "5d ago", or "Apr 25")
- Drag-and-drop upload zone with dashed border + icon overlay
- Refined empty state with icon container
- Hover-reveal row actions remain, now in table cells
Modals (new components):
- Modal — reusable backdrop + card, Escape to close, click-outside
- ConfirmDialog — wraps Modal with confirm/cancel buttons and
destructive variant (red icon + button)
- Used for: create folder, rename, delete confirmation
No new dependencies (react-router-dom added in prior commit).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds the file tree the product revolves around, backed by a new pluggable
storage layer. Covers items #1 and #2 of the next-priorities plan.
Schema (V3, V4):
UserRepository keeps working unmodified
per (owner, parent) + partial unique index for the root
Core:
annotations matching AuthController's style
the controllers (no Spring Security; matches existing ad-hoc pattern)
GET /api/files, GET /api/files/{id}, GET /api/files/{id}/content (streamed),
PATCH /api/files/{id} (rename/move), DELETE /api/files/{id} (recursive)
Plugin fiely-storage-local:
(/////v)
Tests:
partial-write rejection
covers upload/list/download/delete, 401/409/413, folder nesting, rename,
move-into-descendant rejection
round-trips bytes through the filesystem (skipped without Docker, same
pattern as AuthPluginIntegrationTest)
Build:
entities
Configuration:
ceiling; per-tenant limit is the real cap