Add initial project files

This commit is contained in:
Igor Martyniuk
2026-09-01 13:03:41 +03:00
parent 22e111bb44
commit 74cb5b6176
1841 changed files with 242819 additions and 30 deletions
+102
View File
@@ -0,0 +1,102 @@
# File Manager Module
Authenticated file browsing and file operations for the admin app: a backend API for storage operations and a frontend UI for browsing, previewing, uploading, renaming, deleting, duplicating, and editing supported files.
## Source Map
| Area | Path |
| ---------------------- | ------------------------------------------------------------------------------------------ |
| Backend module | `apps/backend/app/base_modules/FileManager/` |
| Backend storage config | `apps/backend/config/drive.ts` |
| Backend S3 adapter | `apps/backend/app/base_modules/FileManager/services/storage_service/s3_storage_service.ts` |
| Frontend module | `apps/admin/src/core/FileManager/` |
| Frontend file assets | `apps/admin/public/media/files/` |
| Frontend routes | `apps/admin/src/core/FileManager/config/routing.tsx` |
## Documentation
| Page | Purpose |
| ------------------------- | ---------------------------------------------------------------------------------- |
| [Backend](./backend.md) | API routes, services, storage adapter contract, permissions, and extension points. |
| [Frontend](./frontend.md) | UI structure, React Query and MobX, cache updates, pages, and widgets. |
## Main Capabilities
- Browse, filter, sort, paginate, and search folders and files.
- Preview details and text content; stream raw files.
- Create folders; upload with progress and cancellation; rename, delete, duplicate entries.
- Image editor and image map zones for supported image files.
- Reusable picker components for choosing files and inserting them into the RichText editor.
- Local filesystem or S3-compatible storage through Adonis Drive.
## High-Level Flow
```txt
Admin route
-> FileManager frontend page
-> React Query API hook
-> /file-manager backend route
-> FileManagerController
-> FileManagerService
-> StorageService adapter
-> Adonis Drive fs or s3 disk
```
## Data Contract
Backend responses use `Entry` objects:
```ts
type Entry = FileEntry | FolderEntry;
type FileEntry = {
type: "file";
name: string;
path: string;
url: string;
size?: number;
updatedAt?: string;
};
type FolderEntry = {
type: "folder";
name: string;
path: string;
updatedAt?: string;
};
```
The frontend maps DTOs into frontend entities and adds UI-oriented metadata such as `parent` and
`extension`.
## Ownership Rules
- Backend owns storage, validation, authorization, path safety, and API response shape.
- Frontend owns navigation, selection, view settings, operation progress, and user interactions.
- React Query owns server state in the frontend.
- MobX stores own local UI and operation state in the frontend.
- Storage adapters must implement `storage_service_interface.ts`; callers should depend on that
interface, not a concrete disk implementation.
- The active storage adapter is selected by `DRIVE_DISK`.
## Common Change Guide
| Change | Start Here |
| --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| Add or change an API route | `apps/backend/app/base_modules/FileManager/routes.ts` and `controller/file_manager_controller.ts` |
| Change path validation or naming rules | `services/path_service.ts` |
| Change list filtering, sorting, or pagination | `services/entry_list_service.ts` and frontend query payloads |
| Add or change a storage backend | `services/storage_service/storage_service_interface.ts`, `create_storage_service.ts`, and [Backend storage docs](./backend.md#storage) |
| Add a frontend operation | `apps/admin/src/core/FileManager/data/queries/` and relevant store/widget |
| Add file type icons | `apps/admin/public/media/files/` and `shared/file/fileTypeImportMap.ts` |
| Add preview support | Backend `file_preview_service.ts` and frontend `view/ui/FilePreview/` |
## Planned Work
- Add more non-filesystem storage adapters such as:
- Google Cloud Storage
- Supabase Storage
- Digital Ocean Spaces
- Cloudflare R2
- Add testing notes once module-level backend and frontend tests are established.
- Add libs for rendering large lists.
+110
View File
@@ -0,0 +1,110 @@
# File Manager Backend
Authenticated file operations under `/file-manager`: validation, authorization, path normalization, orchestration, with raw storage delegated to a storage adapter. Source: `apps/backend/app/base_modules/FileManager/`.
## Request Flow
```txt
routes.ts
-> controller/file_manager_controller.ts
-> validator/*
-> policy/file_manager_policy.ts
-> services/file_manager_service.ts
-> services/storage_service/ (interface -> factory -> fs | s3 adapter)
-> Adonis Drive
```
The controller stays thin (auth checks, validation, path extraction, status codes, response shape). `FileManagerService` owns orchestration; storage adapters own raw persistence.
## Routes
All prefixed with `/file-manager`, behind `middleware.auth()`; controller methods check policy abilities (`policy/file_manager_policy.ts`) before doing work. When adding an operation, add or reuse an explicit ability first.
| Method | Route | Controller Method | Ability | Purpose |
| -------- | ------------------ | ----------------- | ---------- | ------------------------------------------------------- |
| `GET` | `/` and `/*` | `index` | `viewList` | List entries for root or a nested directory. |
| `GET` | `/files/details/*` | `show` | `view` | Return file metadata and optional text preview content. |
| `GET` | `/files/raw/*` | `raw` | `view` | Stream raw file content. |
| `GET` | `/search?q=value` | `search` | `viewList` | Search entries recursively. |
| `POST` | `/folders` | `createFolder` | `create` | Create a folder. |
| `POST` | `/files` | `upload` | `create` | Upload a file. |
| `POST` | `/files/duplicate` | `duplicate` | `create` | Duplicate one or more **file** entries. |
| `PUT` | `/entries/rename` | `rename` | `update` | Rename or move an entry within its parent path. |
| `DELETE` | `/*` | `destroy` | `delete` | Delete an entry. |
## Validation
Validators in `validator/`: `list_validator.ts` (filter, sort, order, max results, pagination token), `create_directory_validator.ts`, `rename_entry_validator.ts`, `upload_file_validator.ts`, `duplicate_entries_validator.ts`, plus shared helpers in `schemas.ts`.
Path extraction from wildcard routes goes through `helpers/get_validated_path_from_request.ts` — never parse wildcard paths in controllers or storage adapters.
## Services
| Service | Owns |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `file_manager_service.ts` | Orchestration: listing with filter/sort/pagination, file detail + preview composition, moves, unique upload and copy names, search, storage calls. |
| `path_service.ts` | Client↔storage path conversion, safe joins, dirname/basename, extension metadata, filename sanitizing, unique names. Use it instead of string manipulation. |
| `entry_list_service.ts` | List behavior on raw storage entries: category filter, sort, folders-first, pagination + token encoding. Frontend list controls must stay compatible with its accepted values. |
| `file_preview_service.ts` | Text preview content for previewable extensions (defined in the same file). For rich previews, keep the response small and stream via `/files/raw/*`. |
| `entry_search_service.ts` | Path search including matching parent folders for context. Receives an abort signal from the controller — long searches should respect it. |
## Storage
Abstractions in `services/storage_service/`:
- `storage_service_interface.ts` — the adapter contract: list (flat and recursive), existence, metadata, read/stream/write, create directory, move, delete, copy. Services call the interface, never a concrete adapter.
- `fs_storage_service.ts` — filesystem adapter over the Adonis Drive `fs` disk: maps filesystem objects to entries, handles streams and metadata, preserves directory marker behavior.
- `s3_storage_service.ts` — S3-compatible adapter over the `s3` disk: maps objects and common prefixes to entries, uploads multipart files by streaming the temp file via `putStream`, creates folders with directory marker objects, moves folders by moving every object under the prefix, deletes files exactly and folders by prefix, copies file entries. S3 has no native directories, so it shares the `.keep` marker convention. **Folder operations must use slash-terminated prefixes** — deleting `test/` must not delete `test123.txt`.
- `fs_directory_marker_service.ts` — encapsulates the `.keep` marker convention; never duplicate marker logic outside it.
- `create_storage_service.ts` — factory; the active adapter (`fs` or `s3`) is selected by `DRIVE_DISK`.
## Drive Configuration
`apps/backend/config/drive.ts` configures two disks: `fs` (local development, filesystem storage) and `s3` (S3-compatible object storage, including local MinIO).
S3 environment variables:
| Variable | Local Docker Value | Purpose |
| ----------------------- | --------------------------------------------------------------------- | --------------------------------------------- |
| `AWS_ACCESS_KEY_ID` | `minioadmin` | S3 access key. |
| `AWS_SECRET_ACCESS_KEY` | `minioadmin` | S3 secret key. |
| `AWS_REGION` | `us-east-1` | S3 region. |
| `S3_BUCKET` | `crux-drive` | Bucket used by the file manager. |
| `S3_ENDPOINT` | `http://minio:9000` in Docker, `http://localhost:9000` outside Docker | S3 API endpoint. |
| `S3_CDN_URL` | `http://localhost:9000/crux-drive/` | Browser-facing public URL base for file URLs. |
For MinIO the disk uses `forcePathStyle: true` and `supportsACL: false`; bucket public-read is handled by the Docker bucket setup container.
## Local S3 Simulation
`docker-compose.yml` runs `minio` (the S3 server) and `minio_create_bucket` (one-shot setup that creates the bucket and enables anonymous download — it exits after setup, which is normal). Console at `http://localhost:9001`, credentials `minioadmin` / `minioadmin`. After dependency or Dockerfile changes: `docker compose build --no-cache && docker compose up`.
## Response Shape
```ts
interface EntryBase {
name: string;
path: string;
updatedAt?: string;
}
interface FileEntry extends EntryBase {
type: "file";
size?: number;
url: string;
}
interface FolderEntry extends EntryBase {
type: "folder";
}
```
List responses: `{ data: Entry[]; meta: { nextPaginationToken: string | null } }`.
## Backend Change Checklist
1. Confirm the policy ability for the operation.
2. Add or update a validator for all request input.
3. Keep client paths and storage paths separated through `path_service.ts`.
4. Update the frontend DTO or mapper if the response shape changes.
5. Update this documentation when routes, permissions, storage contracts, or response shapes change.
+123
View File
@@ -0,0 +1,123 @@
# File Manager Frontend
Admin file manager UI: browsing, previews, uploads, delete/duplicate/rename, image editing and image map tools, settings, and reusable file pickers. Source: `apps/admin/src/core/FileManager/`.
**Main principle: React Query owns server state** (entries, file details, raw blobs, search, mutations); **MobX owns local UI and operation state** (active/selected/expanded paths, filters and sort, dialog state, upload/delete progress, loaded-entry lookup).
## Directory Structure
```txt
FileManager/
config/ Routes, menu entries, messages, translations.
data/
api/ Raw HTTP methods and DTOs.
hooks/ Feature hooks that combine queries with stores.
mappers/ DTO-to-entity mapping.
queries/ React Query hooks, keys, mutations, and cache helpers.
stores/ MobX local state stores.
entities/ Domain entities and settings types.
providers/ Feature providers and context hooks.
pages/ Route-level screens.
shared/ FileManager utilities that do not depend on view code.
view/ UI components, widgets, forms, and layouts.
```
## Routes
Module mounted at `/file-manager/*`, configured in `config/routing.tsx`. Menu groups: `Drive` (browsing, viewer), `Tools` (image editor, image map), `Settings`.
| Route | Page | Purpose |
| -------------------------------------- | ------------------- | ------------------------------------------- |
| `/file-manager/drive/explorer/*` | `DriveExplorerPage` | Browse folders and files. |
| `/file-manager/drive/viewer/-/*` | `DriveViewerPage` | View file details and preview. |
| `/file-manager/drive/settings` | `DriveSettingsPage` | Configure file-manager UI settings. |
| `/file-manager/tools/image-editor/-/*` | `ImageEditorPage` | Edit image files. |
| `/file-manager/tools/image-map/-/*` | `ImageMapPage` | Create and edit image map areas for images. |
Build file-manager URLs with the helpers in `shared/routing/driveLinks.ts`, `shared/routing/openEntryNewTab.ts`, `shared/path/leadingSlash.ts` — never manually.
## API Boundary
Raw backend calls are centralized in `data/api/api.ts` + `data/api/dto.ts`, calling the routes documented in [backend.md](./backend.md):
| API Method | Backend Route |
| ----------------- | ------------------------------------ |
| `getEntries` | `GET /file-manager/*` |
| `getFile` | `GET /file-manager/files/details/*` |
| `getRawFile` | `GET /file-manager/files/raw/*` |
| `createDirectory` | `POST /file-manager/folders` |
| `renameEntry` | `PUT /file-manager/entries/rename` |
| `removeEntry` | `DELETE /file-manager/*` |
| `uploadFile` | `POST /file-manager/files` |
| `duplicateFiles` | `POST /file-manager/files/duplicate` |
| `searchEntries` | `GET /file-manager/search` |
DTOs are mapped before reaching UI code (`entities/entry.ts`, `data/mappers/entryMapper.ts`).
## Data Flow
- **Reading entries** — `useEntries()` builds the query payload from `ExplorerStore`, fetches via React Query (`useGetEntries`), and syncs loaded entries into `EntriesStore`. `EntriesStore` is not the fetch owner — it is an index of loaded entries for the sidebar tree, selection, active-entry lookup, and operation state.
- **Deep links** — for a nested folder opened directly, `useEntries()` preloads ancestor folders so the sidebar tree and entry index work for paths like `/folder/subfolder/current`.
- **Mutations** — `useCreateDirectory`, `useRenameEntry`, `useDeleteEntry`, `useDuplicateFiles`, `useUploadFile`. After success, the UI updates the local cache and stores directly instead of invalidating entries queries — no refetch storm when many items are created, deleted, or uploaded in sequence.
## Query Cache Updates
`data/queries/cache.ts` updates existing React Query cache data in memory (no network request):
```ts
addEntriesToCache(parentPath, entries); // filter-aware insertion into the parent folder's cached lists
removeEntriesFromCache(paths); // removal from all cached entry pages
renameEntryInCache(prevPath, renamedEntry);
```
Tradeoff: since the module avoids refetching, exact backend sort order may drift after local insertion until the folder is naturally refetched.
## Stores
All in `data/stores/`:
| Store | Owns |
| ---------------------- | ------------------------------------------------------------------------------------------------------ |
| `EntriesStore` | Index of loaded entries by path, child/selected/active lookup, local remove and rename. Never fetches. |
| `ExplorerStore` | Active path, selected paths, expanded tree paths, breadcrumbs, filter, sort. |
| `DriveDialogStore` | Create-directory and rename dialogs, entry selected for rename. |
| `UploadOperationStore` | Upload queue, progress, retry/remove/abort, operation card visibility. |
| `DeleteOperationStore` | Delete queue, progress, retry/remove, operation card visibility. |
| `SettingsStore` | Persisted view mode, compact mode, sidebar visibility, grid size. |
Operation stores receive the React Query mutation function from the view layer and manage only operation state.
## Providers
`DriveProvider` creates and exposes the feature stores — `const { entriesStore, explorerStore } = useDrive();`. `SettingsProvider` owns persisted settings.
## Pages and Widgets
- `DriveExplorerPage` — main browser: `DriveProvider`, toolbar, content, sidebar, footer, operation watcher.
- `DriveViewerPage` — details and preview, via `useGetFile()`.
- `DriveSettingsPage` — persisted UI preferences.
- `ImageEditorPage` / `ImageMapPage` — use `useGetFile()` + `useGetRawFile()` (editor also `useUploadFile()`). Config and helpers live in `view/widgets/ImageEditor/` so `view` code never imports from `pages`.
- `FilePickerDialog` — reusable picker widgets in `view/widgets/FilePickerDialog/`; keep picker behavior feature-scoped and pass reusable UI state through props/context instead of coupling low-level components to page-level stores.
## View Layer Rules
Feature widgets may use `useDrive()` and `useSettings()`. Lower-level reusable components (`ExplorerFileEntryGrid`, `FileManagerFileIcon`, `FilePreview`, …) stay prop-driven. Never import backend DTOs into view components — views consume frontend entities and props.
## File Types and Icons
Helpers in `shared/file/`; icons in `apps/admin/public/media/files/`. To add an icon: add the asset there, update `shared/file/fileTypeImportMap.ts`, and confirm `FileManagerFileIcon` falls back correctly for unknown extensions.
## Adding a Query or Mutation
1. Add/reuse a method in `data/api/api.ts`.
2. Query: add a key in `data/queries/keys.ts` and a query file in `data/queries/`, mapping DTOs to entities before returning. Mutation: add a mutation hook in `data/queries`, call `mutateAsync` from the consuming widget or operation store.
3. Update `EntriesStore` if lookup state changes; update `data/queries/cache.ts` if the visible cache must change.
4. Never import MobX stores or providers inside `data/queries`. Don't invalidate entries queries per operation unless a full refetch is intentional.
## Frontend Change Checklist
1. Is the data server state or local UI state?
2. Backend calls stay in `data/api`; React Query hooks stay free of MobX provider imports.
3. Map DTOs before view components.
4. Update cache helpers when mutation results should appear immediately.
5. Update backend docs if the change requires a backend contract change.