# File tree model

Source: https://docs.qiick.io/externa/file-tree-model

The file manager stores a hierarchical tree of **file nodes** on the `assets` disk, with content-addressed **versions**, optional **chunked uploads**, and async work for large duplicates and multi-file zips.

> **Related**
> Product UI: [File manager](https://docs.qiick.io/externa/file-manager). Config keys: [Files & storage](https://docs.qiick.io/externa/files-configuration). HTTP: [Admin & files API](https://docs.qiick.io/externa/admin-api).

## File node types

Enum: `App\Enums\FileTypeEnum` (`app/Enums/FileTypeEnum.php`).

| Case       | Value        | Notes                                          |
| ---------- | ------------ | ---------------------------------------------- |
| File       | `file`       | Regular uploaded blob (current UI create path) |
| Folder     | `folder`     | Container node (current UI create path)        |
| Link       | `link`       | Reserved in enum                               |
| External   | `external`   | Reserved in enum                               |
| Generated  | `generated`  | Reserved in enum                               |
| Collection | `collection` | Reserved in enum                               |

Model: `App\Models\File` — hierarchical via `parent_id` / `children`, soft deletes, Spatie `HasTags`.

> **Current create paths**
> `FileService` / controller create flows and the TypeScript `AdminFileRow.type` currently use **`file` and `folder` only**. The other enum values exist for forward compatibility but are not wired into today’s create UI.

## Versions

| Piece    | Detail                                                                                              |
| -------- | --------------------------------------------------------------------------------------------------- |
| Model    | `App\Models\FileVersion`                                                                            |
| Table    | `file_versions`                                                                                     |
| Pointer  | `files.current_version_id` → `File::currentVersion()`                                               |
| Relation | `File::versions()`                                                                                  |
| Dedup    | `FileService::resolveOrCreateVersion()` reuses a version with the same content `hash` when possible |

Version fields include `disk`, `storage_path`, `hash`, `mime_type`, `size`, `width`, `height`, and `meta`. Upload and replace flows update `current_version_id` to the resolved version.

> **Dangerous extensions**
> Upload, replace, chunked init/complete, and rename reject a fixed denylist of dangerous extensions (`ForbiddenUploadExtension`). Display name / metadata / tags are stripped of HTML via `PlainTextSanitizer`. Details: [File manager — Upload security](https://docs.qiick.io/externa/file-manager#upload-security).

## Chunked uploads

Large uploads go through a session model rather than a single request body.

| Piece        | Detail                                                              |
| ------------ | ------------------------------------------------------------------- |
| Model        | `App\Models\FileUpload`                                             |
| Table        | `file_uploads`                                                      |
| Service      | `FileService::initChunkUpload` / `uploadChunk` / complete-and-merge |
| Default disk | `assets`                                                            |
| Session TTL  | 24 hours from init                                                  |
| Cleanup      | `files:cleanup-uploads` (hourly schedule)                           |

Routes (under `can.manage.files` in `routes/admin.php`):

| Method | Path name                | Purpose                      |
| ------ | ------------------------ | ---------------------------- |
| POST   | `files.uploads.init`     | Start session                |
| POST   | `files.uploads.chunk`    | Upload a chunk               |
| POST   | `files.uploads.complete` | Merge and create/update file |
| GET    | `files.uploads.status`   | Poll session status          |

## fileables / HasFiles

Polymorphic attachments let other models own files without owning the blob tree.

| Piece      | Path / fact                                                                                        |
| ---------- | -------------------------------------------------------------------------------------------------- |
| Trait      | `App\Traits\HasFiles`                                                                              |
| Pivot      | `fileables` (`file_id`, `fileable_id`, `fileable_type`, `role`, `order`)                           |
| Trait API  | `files()`, `attachFile`, `detachFile`, `updateFile`, `syncFiles`, `filesByRole`, `firstFileByRole` |
| HTTP       | `POST files/{file}/attach`, `POST files/{file}/detach`                                             |
| Validation | `FileService` attach/detach requires the target class to use `HasFiles`                            |

> No Eloquent model currently `use HasFiles`. The pivot, trait, and service endpoints are ready; domain models must opt in to become attach targets.

## Tags

- Spatie tags via `HasTags` on `File`
- Sync: `FileService::syncTags()`
- Route: `PUT files/{file}/tags` (`files.tags`)
- Permission: `can-tag-files`
- Middleware maps `files.tags` and bulk `tag` / `untag` actions in `EnsureCanManageFiles`

## Favorites

- Pivot: `file_favorites` (`user_id`, `file_id`)
- Relations / service: `File::favoritedBy()`, `FileService::favorite` / `unfavorite` / `bulkFavorite`
- Routes: `POST` / `DELETE` `files/{file}/favorite`
- Permission: `can-favorite-files`
- API resource exposes `is_favorited`

## Disks (`assets`)

Configured in `config/filesystems.php` as disk `assets`:

| Setting    | Value                               |
| ---------- | ----------------------------------- |
| Driver     | `local`                             |
| Root       | `storage/app/public/assets`         |
| URL        | `{FILE_PUBLIC_BASE}/storage/assets` |
| Visibility | `public`                            |

Default for `files.disk`, `file_uploads.disk`, and `FileService` create/upload paths.

## Thumbnails — FileTransformService

Class: `App\Services\FileTransformService`.

| Behavior      | Value                                                                             |
| ------------- | --------------------------------------------------------------------------------- |
| Default size  | `128` px                                                                          |
| Max size      | `512` px (or higher if project transform presets define a larger size)            |
| Cleanup sizes | `64`, `128`, `256`, `512`                                                         |
| Eligible      | `type = file`, `mime_type` starts with `image/`, not SVG                          |
| Fit           | Contain-fit resize (GD)                                                           |
| Format        | WebP q82 if available, else JPEG q82                                              |
| Cache path    | `{dirname(storage_path)}/transforms/{sha256}.webp\|jpg` on the **same** file disk |
| HTTP          | `GET files/{file}/thumbnail` → `files.thumbnail`                                  |
| Resource      | `thumbnail_url` when `isImage()`                                                  |

## Zip and duplicate thresholds

From `config/files.php`:

| Config key                       | Env                              | Default               | Meaning                                                                                                                                         |
| -------------------------------- | -------------------------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `files.duplicate_sync_max_bytes` | `FILES_DUPLICATE_SYNC_MAX_BYTES` | `52428800` (50 MiB)   | Single non-folder files at or below this size duplicate **synchronously**; folders, bulk selections, and larger files queue `DuplicateFilesJob` |
| `files.zip_ttl_minutes`          | `FILES_ZIP_TTL_MINUTES`          | `60`                  | Prepared zips under `storage/app/zips`; cleanup / 410 after TTL (`files:cleanup-zips`)                                                          |
| `files.zip_max_bytes`            | `FILES_ZIP_MAX_BYTES`            | `104857600` (100 MiB) | Hard max archive size in `FileService::buildZipArchive`                                                                                         |

> **Zip is always async for multi-download**
> Multi-file / folder download always queues `PrepareFilesZipJob` (HTTP 202). `zip_max_bytes` is a **size cap**, not a sync/async threshold. Duplicate sync vs async is the size-based split described above (`FileController::shouldDuplicateSynchronously()`).

## Jobs and cleanup

| Job / command           | Role                              |
| ----------------------- | --------------------------------- |
| `DuplicateFilesJob`     | Async copy + notification         |
| `PrepareFilesZipJob`    | Build zip + notify download ready |
| `files:cleanup-uploads` | Stale chunk sessions (hourly)     |
| `files:cleanup-zips`    | Expired zip archives (hourly)     |

## Related pages

- [Architecture](https://docs.qiick.io/externa/architecture) — services and jobs
- [Effective permissions](https://docs.qiick.io/externa/permissions) — `can.manage.files`
- [Files & storage](https://docs.qiick.io/externa/files-configuration)
- [AI assistant model](https://docs.qiick.io/externa/ai-assistant) — `ManageFiles` tool
