# File Module Architecture > **SoT scope** — **this document** owns: module boundaries, type system (`FileHandle` / `FileEntry` / `FileInfo`), IPC / DataApi contracts, layered architecture (no-FS-side-effect vs FS-side-effect paths), business-service integration, and service lifecycle assignment. FileManager **internal implementation** (storage layout, version detection, atomic writes, recycle bin, reference cleanup, watcher internals, orphan sweep, DanglingCache state machine) lives in [`file-manager-architecture.md`](./file-manager-architecture.md). In case of conflict, the layer ownership above decides: positioning / contract → this document, implementation → the other. > > **Contract stability**: the JSDoc, type signatures, and behavioral tables in this document (and `file-manager-architecture.md`) are **binding commitments** for the implementation — not provisional notes. When implementation reveals a contract that cannot be honored (a cleanup semantic that collides with reality, an error-type that needs expanding, a signature shape that doesn't fit), the required workflow is: **(1) open a PR revising the contract doc first**, with justification in the PR description; **(2) land that doc revision**; **(3) implement against the updated contract**. Do not ship an implementation that silently diverges from the doc — the cost of doc revision is minutes, the cost of hidden divergence compounds indefinitely. > > Related documents: > > - `docs/references/file/file-manager-architecture.md` — FileManager submodule design (FileEntry model, origin semantics, atomic writes, version detection, DirectoryWatcher, AI SDK integration) > - `docs/references/file/directory-tree.md` — DirectoryTreeBuilder primitive design (in-memory tree + chokidar watcher + .gitignore coordination, `DirectoryTreeManager` lifecycle service, `File_Tree*` IPC contract, renderer-side `useDirectoryTree` hook) --- ## 1. Module Scope ### 1.0 Core Principle > **FileManager manages files introduced via explicit calls to `createInternalEntry` / `ensureExternalEntry`**—files exist as one of two origins: `internal` (Cherry owns the content) or `external` (records a path reference only). Which origin the caller chooses is a business-layer decision; FileManager makes no assumptions about it. ### 1.0.1 Semantics of Origin The `origin` field on a FileEntry defines content ownership, with two values: - **`internal`**: Cherry owns the file content, physically stored at `{userData}/Data/Files/{id}.{ext}`. The caller hands a Buffer/Stream/source file to FileManager, which copies and takes ownership. `name` / `ext` / `size` are authoritative on the row (atomic writes keep DB and FS in sync). - **`external`**: Cherry only records an absolute path reference on the user's side, does not copy content, and does not own the file. `name` / `ext` on the row are pure projections of `externalPath` (basename / extname); `size` is **not stored** (always `null`) — live value is obtained via File IPC `getMetadata`. File availability and content changes are determined by the user side. Which origin to pick is the **caller's** decision; FileManager makes no assumption about the business layer. ### 1.0.2 Best-effort Semantics for External An external entry is a persistent record that "the caller expressed the intent to reference this path at some point in time"—analogous to the "best-effort external reference" seen in tools like codex. It does not guarantee the file remains stable, nor that the content matches what it was when first referenced. Cherry does not actively mirror FS changes; instead, FS changes naturally surface as "reading new content next time" or "the entry turns dangling". ### 1.1 What the File Module Includes The File IPC adapter lives outside `src/main/services/file/` (`src/main/ipc/handlers/file.ts`) but is part of the file-module boundary: it owns renderer transport and `FileHandle` dispatch, depends on FileManager for entry-aware operations, and depends on `services/file/utils/*` for file-module path-arm helpers. It must not import `node:fs` directly. ``` File Module (src/main/services/file/) │ ├── index.ts ← module barrel; exports FileManager + public types only │ (entry internals stay hidden; selected file-module │ utils are imported by explicit path when needed) │ ├── FileManager.ts ← lifecycle runtime service + public facade for FileEntry ops │ │ public methods are thin delegates to internal/*; owns versionCache │ │ does not own the File IPC architecture │ ├── FileEntry lifecycle (create-or-upsert / write / trash / restore / rename / copy / permanentDelete) │ ├── Version detection & concurrency control (read / writeIfUnchanged / withTempCopy) │ ├── Metadata & system ops (getMetadata / open / showInFolder) │ └── Electron dialog (showOpenDialog / showSaveDialog) │ ├── internal/ ← private implementation, not re-exported by index.ts; external imports forbidden │ │ every pure function explicitly receives FileManagerDeps │ │ (fileEntryService / fileRefService / danglingCache / versionCache) │ ├── deps.ts — FileManagerDeps type │ ├── dispatch.ts — dispatchHandle (temporary home while legacy │ │ FileManager IPC handlers still exist; final │ │ ownership is the File IPC adapter layer) │ ├── entry/ │ │ ├── create.ts — createInternal / ensureExternal │ │ ├── lifecycle.ts — trash / restore / permanentDelete + batches │ │ ├── rename.ts │ │ └── copy.ts │ ├── content/ │ │ ├── read.ts — read / createReadStream (including `*ByPath` variants) │ │ ├── write.ts — write / writeIfUnchanged / createWriteStream │ │ └── hash.ts — getContentHash / getVersion │ ├── system/ │ │ ├── shell.ts — open / showInFolder │ │ └── tempCopy.ts — withTempCopy │ └── orphanSweep.ts — temp-session ref prune + FS-level orphan sweep │ │ ├── utils/ ← file-module path/API helpers (not raw FS primitives) │ ├── pathResolver.ts — FileEntry → physical FilePath resolution + external canonicalization │ └── metadata.ts — path-arm PhysicalFileMetadata projection for File IPC dispatch │ ├── versionCache.ts ← LRU type definition; instance held as private field on FileManager │ ├── danglingCache.ts (singleton) │ ├── check(entry): DanglingState — query in-memory / cold-path stat │ ├── onFsEvent(path, state) — receives watcher events │ ├── Reverse index Map> (populated from DB at file_module startup) │ └── Queried by File IPC handlers; automatically wired by the watcher factory │ ├── watcher/ │ └── DirectoryWatcher (not a service, a generic FS monitoring primitive) │ ↳ factory createDirectoryWatcher() auto-wires events into danglingCache │ └── tree/ ← second top-level primitive, parallel to FileManager │ SoT: docs/references/file/directory-tree.md ├── builder.ts ← DirectoryTreeBuilder: in-memory TreeDirRoot │ mirror + chokidar watcher + initial ripgrep scan ├── DirectoryTreeManager.ts ← @Injectable WhenReady service; │ owns the File_Tree* IPC contract; dedupes │ builders by (rootPath, options) across treeIds ├── search.ts ← listDirectory: ripgrep + optional fuzzy match ├── gitignore.ts ← .gitignore parsing shared by ripgrep --ignore-file │ and chokidar's ignored predicate └── index.ts ← barrel: createDirectoryTree + DirectoryTreeBuilder Pure FS primitives (src/main/utils/file/) — shared raw FS primitives, open to the entire main process ├── fs.ts — basic FS: read / write / stat / copy / move / remove │ atomic write: atomicWriteFile / atomicWriteIfUnchanged / createAtomicWriteStream │ version: statVersion / contentHash (xxhash-h64) ├── shell.ts — system ops: open / showInFolder ├── path.ts — path utils: resolvePath / isPathInside / canWrite / isNotEmptyDir / canonicalizeExternalPath ├── metadata.ts — type detection: getFileType / isTextFile / mimeToExt ├── search.ts — directory search: listDirectory (ripgrep + fuzzy matching) ├── legacyFile.ts — shared legacy helpers (`getFileType(ext)` / `sanitizeFilename` / `getAllFiles` / `pathExists` / …); planned to be split into the modules above over time └── index.ts — barrel: re-exports `./legacyFile` so cross-module callers can `import from '@main/utils/file'` Data Module dependencies (src/main/data/) ├── FileEntryService (data repository, pure DB) — file_entry table ├── FileRefService (read facade + temp-session store) — aggregates chat/painting refs; owns temp-session CacheService refs └── DataApi Handler (files.ts) — SQL-first read endpoints; no FS access, no main-side resolvers ``` **Implementation status**: - **`FileUploadService` — manual implementation ahead of AI SDK stable.** Provider-specific file uploads (OpenAI Files API, Gemini, etc.) are a real, currently-unmet need; the existing `FileServiceManager` (`src/main/services/remotefile/`) already implements per-provider upload but is wired as an ad-hoc v1 IPC layer rather than a lifecycle service. **No longer deferred** — we will refactor `FileServiceManager` into a proper `FileUploadService` lifecycle service ahead of the Vercel AI SDK Files Upload API stabilising. Concrete design (interface, table schema, IPC surface, whether `file_upload` table + `FileUploadRepository` ship in the same PR or split out) is **TBD**; when the AI SDK ships its stable Files API the manual implementation should converge toward `file-manager-architecture.md §9`. AI SDK reference: [`uploadFile`](https://ai-sdk.dev/v7/docs/reference/ai-sdk-core/upload-file). ### 1.2 FileManager's Position Within the Module The file module has **two top-level primitives** — `FileManager` and `DirectoryTreeBuilder` — sitting alongside the shared infrastructure (File IPC adapters, file-module utils, DanglingCache, DirectoryWatcher, FS primitives). Neither subsumes the other; they manage **orthogonal resource concerns**: - **FileManager** is the **sole public entry point for the FileEntry management system** — responsible for the full lifecycle and content operations of `FileEntry` (DB row + content bytes). Its public API only accepts entry-scoped inputs such as `FileEntryId` plus create/upsert params. It exposes `runSweep()` for the cleanup UI / explicit callers, but **does not auto-run orphan sweep at startup**. "Sole public entry" here is scoped to **FileEntry management**, not the file module as a whole — see File IPC and DirectoryTreeBuilder below. - **FileManager is a facade, not a God class** — business methods are delegated to private pure-function modules. The class itself owns only lifecycle, entry orchestration, and instance-scoped caches. It does **not** own renderer transport or `FileHandle.kind` dispatch; those belong to the File IPC adapter layer. Implementation mechanics (deps passing, module layout, extension rules) live in [FileManager Architecture §1.6](./file-manager-architecture.md) — this document stays at the positioning layer. - **File IPC adapters** (`src/main/ipc/handlers/file.ts`) own renderer-facing File IPC routes. They validate request schemas, dispatch `FileHandle` routes, and delegate entry branches to FileManager and path branches to `src/main/services/file/utils/*`. They must not import `node:fs` directly. - **DirectoryTreeBuilder** is the **second top-level primitive**, parallel to FileManager. It manages in-memory tree mirrors + chokidar watchers for arbitrary directories (Notes workspace, future ArtifactPane, …). It is **not** DB-backed — every tree is rebuilt from disk on `File_TreeCreate`. Its IPC surface (`File_TreeCreate` / `File_TreeDispose` / `File_TreeMutation`) is owned by the `DirectoryTreeManager` lifecycle service. SoT: [directory-tree.md](./directory-tree.md). The two primitives observe the same paths independently — a directory can be watched (tree) without its contents being entered (entries), and vice versa. - **DanglingCache** is a file_module singleton—maintains the `'present' | 'missing'` state of external entries, pushed by watcher events, with cold-path stat as a fallback, and served to the renderer via File IPC `getDanglingState` / `batchGetDanglingStates` (never DataApi). - **DirectoryWatcher** is a generic FS primitive, **not a lifecycle service**; business modules (such as a future NoteService) new/dispose instances themselves via the `createDirectoryWatcher()` factory; the factory internally wires events into DanglingCache. `DirectoryTreeBuilder` is one of its consumers. - **File-module path/API helpers** live under `src/main/services/file/utils/`. They are higher-level than raw FS primitives and encode file-module semantics (for example, path-arm metadata projection or FileEntry path resolution). FileManager and File IPC adapters may both depend on them; other main modules may also use them when they want the file module's path semantics without creating a FileEntry. - **Raw FS / path primitives** live under `src/main/utils/file/` (imported as `@main/utils/file/fs`, `@main/utils/file/path`, etc.). They do not depend on the entry system and are open to the entire main process. Main modules may use `services/file/utils`, `@main/utils/file/*`, or direct `node:fs` depending on the abstraction level they need; File IPC adapters specifically use the first two and never import `node:fs` directly. #### Public / Private Boundaries | Location | Visibility | Access | |---|---|---| | File IPC adapter (`src/main/ipc/handlers/file.ts`) | **Renderer transport boundary** | Routes `ipcApi.request('file.*')` calls; delegates entry branches to FileManager and path branches to `src/main/services/file/utils/*`. No direct `node:fs` imports. | | `FileManager` class + public types | **Entire main process** | Resolve the runtime instance via `application.get('FileManager')`; import public types from `@main/services/file` | | `src/main/services/file/utils/*` | **Entire main process** | File-module path/API helpers (for example `getMetadataByPath`, `resolvePhysicalPath`) when callers want file-module semantics without registering a FileEntry. | | `DirectoryTreeManager` + `DirectoryTreeBuilder` factory | **Entire main process** (renderer via IPC) | Renderer: `window.api.tree.create/dispose/onMutation`. Main: `application.get('DirectoryTreeManager')` or `createDirectoryTree` from `@main/services/file/tree`. | | Raw FS primitives (`@main/utils/file/{fs,metadata,path,search,shell}`) | **Entire main process** | Shared convenience wrappers over file / shell operations (BootConfig, MCP oauth, etc. can use directly). Shared legacy helpers (`getFileType(ext)`, `sanitizeFilename`, etc.) are barrel-exported from `@main/utils/file` itself. | | Direct `node:fs` imports | **Entire main process** | Allowed when a module deliberately needs raw Node FS APIs not covered by a shared helper. Do not use direct FS writes for FileEntry-backed paths. | | `watcher/` (`createDirectoryWatcher` factory) | **Entire main process** | Business services call this when they need to watch external directories | | `danglingCache` | **Internal to file-module** | External callers read it via File IPC `getDanglingState` / `batchGetDanglingStates`; never imported directly, never exposed via DataApi | | `internal/*` | **File module implementation only** | FileManager owns most imports. Temporary exception: File IPC adapters may import `internal/dispatch.ts` while legacy FileManager IPC handlers still exist. Move `dispatchHandle` to the IPC adapter layer once FileManager no longer registers any IPC handlers. Do not make `internal/*` a general business-service surface. | Boundary enforcement: `src/main/services/file/index.ts` barrel does not re-export `internal/*`; external `import from '@main/services/file'` cannot reach it. If violations surface, add an ESLint `no-restricted-imports` rule as a fallback. ### 1.3 Out of Scope The following categories are **not** managed by the File Module (no FileEntry is produced): | Category | Owner | Why it's not managed by FileManager | |---|---|---| | Notes file tree (files browsed/edited inside the Notes app) | Notes module (FS-first) | Notes has its own notes dir storage and external editor compatibility; **not mirrored wholesale into FileEntry**. Tree state itself is provided by `DirectoryTreeBuilder` ([directory-tree.md](./directory-tree.md)) — a separate top-level primitive — not by FileManager. Notes joins the tree with sparse renderer-side state (`noteTable` overlays for starred / metadata). | | Knowledge base vector index | KnowledgeService | Auto-generated derived data, not a user file | | MCP server configuration | MCP module | System/user configuration, not user-uploaded files | | Preference / BootConfig | Config module | Application state | | Log files | LoggerService | Auto-generated | | Backup / export files | Corresponding business | Business-generated artifacts in transit | | Agent workspace files | AgentService | Agent-produced at runtime | | OCR / PDF pagination intermediates | Business module / `os.tmpdir` | Temporary computational artifacts | | Real-time sync mirror of external directories | Business module assembles with DirectoryWatcher | File_module does not do bidirectional DB-FS sync | **Note**: The table above is the boundary for "certain business data does not enter FileManager", not "certain file types don't enter". The same physical file can simultaneously belong to an FS-first business domain AND an external FileEntry (the latter is merely a reference to that path)—these are not mutually exclusive. These modules manage their own files and may use `node:fs` or `@main/utils/file/*` directly; they are not bound by the FileManager of the file module. --- ## 2. Type System: Reference vs Data Shape ### 2.1 Two Layers of File Types The file module organizes its types along two layers — the **reference layer** (how a call site names the target file when crossing a boundary) and the **data-shape layer** (what the handler receives after resolving that reference): ``` Entry-referenced Path-referenced ──────────────── ──────────────── Reference layer FileEntryHandle FilePathHandle (across boundaries) { kind: 'entry', entryId } { kind: 'path', path } │ │ ▼ FileManager.getEntry ▼ fs.stat + projection Data-shape layer FileEntry FileInfo (after resolution) { id, origin, name, ext, { path, name, ext, size, size, deletedAt, ... } mime, type, modifiedAt, ... } ``` Picking a handle variant is a **call-site choice of reference form**, not a statement about the file itself. Crucially, **the two axes are orthogonal**: - **Reference form** (this layer): `FileEntryHandle` routes through the entry system (FileManager, versionCache, DanglingCache updates); `FilePathHandle` bypasses it and hits the `@main/utils/file/*` primitives directly. - **Content ownership** (`FileEntry.origin`, not visible in the handle): `internal` means Cherry owns `{userData}/Data/Files/{id}.{ext}`; `external` means Cherry only records a reference to a user-owned path. The **same physical external file** can therefore be reached by either handle variant. A `FileEntryHandle` to its entry goes through the entry-aware code path (dangling updates, version cache, identity-tracked operations); a `FilePathHandle` to the same absolute path goes through pure FS. Picking one is a matter of which subsystem the caller wants in the loop — not a property of the file. ### 2.2 `FileHandle`: the Polymorphic Reference `FileHandle = FileEntryHandle | FilePathHandle` (see [`src/shared/data/types/file.ts`](../../../src/shared/data/types/file.ts)) is the first-class reference type crossing the IPC boundary. Every IPC method that makes sense regardless of which subsystem is in the loop accepts a `FileHandle`; handlers dispatch internally on `handle.kind`. See §3.3 for the full dispatch table. Use `FileHandle` whenever a signature does not *inherently* require an entry row (e.g. anything that isn't a lifecycle op on a FileEntry). ### 2.3 `FileEntry` vs `FileInfo` Once a handle is dispatched, the handler works with either a `FileEntry` (the DB row identified by an entryId) or a `FileInfo` (a live descriptor produced from a path). They are the two "data shapes" of a file: | Aspect | `FileEntry` | `FileInfo` | |----------------|------------------------------------------------------------|-----------------------------------------------------------| | Role | DB row identified by `id` | Live descriptor identified by `path` | | Identity field | `id` (UUID — v7 from `uuidPrimaryKeyOrdered`, v4 preserved from v1 migration) | `path` (absolute filesystem path) | | Liveness | Persistent record — identity + stable projections only | Live view — re-read from `fs.stat` | | Lifecycle | Persistent; trash/restore (internal-origin only) | Transient — per-call descriptor | | Produced by | `createInternalEntry` / `ensureExternalEntry` / DataApi | `fs.stat(path)` / `toFileInfo(entry)` | | Typical use | FileManager ops, UI management panels, ref association creation | Pure content processors (OCR, hashing, tokenization) | **Field overlap is inherent, not redundant**: `name`, `ext`, `type` (and `mime` / `size` on `FileInfo`) describe a file regardless of whether an entry row exists for it. What distinguishes the two types is the *surrounding* fields and the *liveness* of the shared ones: - **`FileEntry` has identity fields** `FileInfo` lacks: `id`, `origin`, `externalPath`, `deletedAt`. - **`FileInfo` has live fields** `FileEntry` lacks: `path` (derived, never stored on `FileEntry`), `modifiedAt`, and a live `size`. - **`FileEntry.size` is origin-gated**. For `origin='internal'` it is an authoritative byte count (kept in sync by atomic writes). For `origin='external'` it is **always `null`** — external files may change outside Cherry at any time, so no DB snapshot is stored. Consumers that need a live value for an external entry call File IPC `getMetadata(handle)` / `batchGetMetadata({ items })`, which runs `fs.stat` on demand. This eliminates the "is this snapshot current?" question at the type level rather than at call sites. - **`FileEntry.name` / `FileEntry.ext` never drift**. For internal they are user-editable SoT; for external they are pure projections of `externalPath` (basename / extname) and therefore stable as long as the entry itself exists. **Projection is one-way**. `FileEntry → FileInfo` is always possible via `toFileInfo(entry)` (async — performs `fs.stat` plus path resolution based on `origin`, which is also how the live `size` is materialized for external). The reverse is **not a type conversion**: it is a state change, and requires explicit registration through `FileManager.createInternalEntry` or `ensureExternalEntry`. The Zod brand on `FileEntrySchema` enforces this — arbitrary object literals cannot satisfy the `FileEntry` type. ### 2.4 Signature Selection Guide Default to the narrowest type that covers the need. "When in doubt, `FileHandle`" for cross-boundary calls, and "when in doubt, `FileInfo`" for leaf content processors. | What the consumer needs | Signature | |--------------------------------------------------------------------------------------------|------------------------------------------| | Doesn't care which subsystem is in the loop; just operates on a file | `FileHandle` ⭐ default for IPC | | Only to call a FileManager lifecycle op (trash, restore, permanentDelete, …) | `FileEntryId` | | Only to hand a path to an ops-level FS function | `FilePath` | | The entry row's fields (UI management panel, origin-aware rendering, ref creation) | `FileEntry` | | A resolved on-disk descriptor for pure content processing | `FileInfo` (typically a return type) | Anti-patterns to avoid: - **Requiring `FileEntry` when only `path` or `size` is read** — this couples the caller to the entry system. Accept `FileHandle` (and dispatch), or accept `FileInfo` (and have the caller project). - **Returning a value typed `FileEntry` whose contract is "might or might not be registered"** — use `FileHandle` or an explicit variant instead. - **Synthesising a `FileEntry` from a `FileInfo`** — registration must go through sanctioned FileManager methods; the Zod brand is specifically there to prevent this. --- ## 3. IPC Design ### 3.1 Design Motivation The renderer needs a unified entry point for file operations (a single `read` can read both FileEntry and an external path), but inside the main process, entry management (DB + FS coordination) and pure path operations (FS directly) are two very different responsibilities. Solution: **File IPC adapter + handler-level dispatch**. The renderer-facing adapter (`src/main/ipc/handlers/file.ts`) owns route validation and dispatch; each handler delegates to different implementations based on target type. Entry branches call FileManager; path branches call file-module path/API helpers in `src/main/services/file/utils/*`. The adapter does not import `node:fs` directly. ### 3.2 Handler Dispatch ``` Renderer → IpcApi route (`src/main/ipc/handlers/file.ts`) ├── target: FileEntryId / FileEntryHandle → FileManager method (entry coordination) └── target: FilePathHandle → services/file/utils/* (file-module path API) ``` Other services in the main process can call FileManager, `src/main/services/file/utils/*`, `@main/utils/file/*`, or direct `node:fs` as needed, without going through IPC. The "no direct `node:fs`" rule is specific to the File IPC adapter layer. ### 3.3 IPC Method Categories > **Current wiring status.** New renderer-facing File IPC lives in IpcApi > routes declared in `src/shared/ipc/schemas/file.ts` and handled by > `src/main/ipc/handlers/file.ts`. The routes currently registered are: > `file.batch_get_metadata`, `file.batch_get_physical_paths`, > `file.batch_get_dangling_states`, `file.batch_create_internal_entries`, > `file.batch_trash`, `file.batch_restore`, `file.batch_permanent_delete`, > `file.rename`, `file.open`, and `file.show_in_folder`. Legacy > `IpcChannel.File_*` routes are compatibility-only for remaining preload > consumers (notably singular metadata / path helpers) and MUST NOT be used by > new renderer code. The tables below describe the logical File IPC surface; > the IpcApi schema registry is the source of truth for routes wired today. All operations that can act on any file (FileEntry or arbitrary path) **accept a `FileHandle` tagged union** (`{ kind: 'entry', entryId } | { kind: 'path', path }`). File IPC handlers dispatch by `handle.kind` to FileManager (entry branch) or file-module path helpers (path branch). **Operations that accept FileHandle (entry + path branches unified)**: | Method | Description | entry, internal-origin | entry, external-origin | path | |---|---|---|---|---| | `read` | Read content | read(userDataPath) | read(externalPath) (live) | read(path) | | `getMetadata` | Live physical metadata (`fs.stat`) — batch variant `batchGetMetadata` accepts caller-keyed `FileHandle` items | resolve + stat | stat(externalPath) — **sole live-size source for external** | path metadata projection via `services/file/utils/metadata` | | `getVersion` | FileVersion (live `fs.stat`) | stat userData | stat externalPath | statVersion | | `getContentHash` | xxhash-h64 | read userData + hash | read externalPath + hash | contentHash | | `write` | Atomic write | atomic → userData + DB size update | atomic → externalPath (explicit user edit; no DB size column to touch) | atomic → path | | `writeIfUnchanged` | Optimistic concurrent write | same as write plus version check | same | same (caller must getVersion first) | | `permanentDelete` | Delete entry | unlink userData + delete from DB | **delete from DB only** (physical file untouched; path-level deletion remains available via a `FilePathHandle` to `remove`) | remove(path) | | `rename` | Rename | pure DB (UUID path unchanged) | fs.rename + DB update (name + externalPath) | rename(path, newPath) | | `copy` | Copy to a new internal-origin entry | read source + create new internal | read source external + create new internal | read path + create new internal | | `open` / `showInFolder` | System ops | resolve + shell | resolve + shell | shell | **Operations accepting only FileEntryId (meaningful only when you already hold an entry id)**: | Method | Description | |---|---| | `createInternalEntry` / `batchCreateInternalEntries` | Create a new Cherry-owned FileEntry (writes to `{userData}/Data/Files/{id}.{ext}`; each call produces an independent new entry, no conflict possible) | | `ensureExternalEntry` / `batchEnsureExternalEntries` | Pure upsert by `externalPath`—the entry point first `canonicalizeExternalPath(raw)` normalizes it (see `pathResolver.ts`); reuses the existing entry with the same path or inserts a new one. Idempotent by design—callers may safely repeat calls. No "restore" branch: external entries cannot be trashed. External rows carry no stored `size` (always `null`); live values come from `getMetadata`. | | `trash` / `restore` | Soft delete based on deletedAt (DB only). **Internal-origin only** — external-origin entries cannot be trashed (`fe_external_no_delete` CHECK); passing an external id throws. | | `batchTrash` / `batchRestore` | Batch versions of `trash` / `restore` — same internal-origin-only rule. | | `batchPermanentDelete` | Batch version of `permanentDelete`. | | `withTempCopy` | Copy isolation for calling third-party libraries | | `getDanglingState` / `batchGetDanglingStates` | Query external-origin entry presence (FS-backed via DanglingCache; cold miss triggers a single `fs.stat`). Internal-origin entries always `'present'`. | | `getPhysicalPath` / `batchGetPhysicalPaths` | Resolve absolute path for a FileEntry (main-side `resolvePhysicalPath`). Intended for agent context / drag-drop / subprocess spawn. Also the input to `toSafeFileUrl` for `` / `