Documentation
¶
Overview ¶
Package syspath is the ONE definition of the directories filex keeps for itself inside a storage, and of how a path is judged against them.
filex writes its own machinery next to people's files: the soft-delete bin (`.filex-trash`), version history (`.versions`), legacy thumbnails (`.thumbs`), the desktop app's "open with filex" working copies (`.filex-open`) and the empty-folder marker (`.keepdir`). None of it is anybody's document, and a person must never be shown it — not in a listing, a search hit, a share, an archive, a notification's text or a notification's target — nor write there (Refused: every person-facing write asks it, and only the desktop's open-with round trip gets through).
⚠⚠ Why a package of its own. Until this one existed the list was written by hand in seventeen places in the backend alone (the five protocol servers, the two listing projectors, the AI surface three times, share browsing, the archive walk, the cross-storage copy, versioning, the antivirus eligibility check, the thumbnail backfill twice) and no two of them agreed. `.filex-open` was known to exactly one of them — the thumbnail backfill — so the listing API returned it, global search returned the working copies inside it, and a notification about a desktop "open with" save targeted `.filex-open/<session>-<name>` and opened it (reported by the owner, 2026-09-21). `.versions` was missing from share browsing, the AI listing and the AI archive walk. A second copy of this list is a second chance to forget a name; import this package instead.
⚠ The client carries the same list (packages/core/src/lib/internalPaths.ts) for the explorer's own guards, and web/tests/lib/internalPaths.test.ts parses THIS file and fails when the two differ. Add a name here and the build tells you where else it has to go.
Leaf package on purpose: it imports nothing from filex, so trash, versioning, notify, every protocol server and every handler can depend on it without a cycle.
Index ¶
- Constants
- Variables
- func Check(v Verb, rels ...string) error
- func Dirs() []string
- func DraftDir(person int64, key string) string
- func DraftOwner(rel string) (int64, bool)
- func Hidden(rel string) bool
- func InDir(rel string) bool
- func InDrafts(rel string) bool
- func InTrash(rel string) bool
- func IsDirName(name string) bool
- func IsDraftOf(rel string, person int64) bool
- func IsKeyFile(rel string) bool
- func IsName(name string) bool
- func OpenWithOriginal(rel string) (string, bool)
- func Refused(v Verb, rel string) bool
- func RefusedBy(v Verb, rel string, person int64) bool
- func Reserved(rel string) string
- func Sealed(rel string) bool
- func SealedFor(rel string, person int64) bool
- func Unlisted(name string) bool
- type ReservedError
- type Verb
Constants ¶
const ( // Trash is the soft-delete bin. Its contents are reached only through the // trash API (listed by ORIGINAL path, restored by node id), never by path. Trash = ".filex-trash" // Versions holds version snapshots, `.versions/<node id>/<n>`. Reached only // through the versions API. Versions = ".versions" // Thumbs is the legacy in-storage thumbnail cache. Thumbnails are served // by node id from /api/files/thumb, never by path. Thumbs = ".thumbs" // OpenWith is the desktop app's "open with filex" working area // (desktop/src/openwith.ts SCRATCH_DIR_NAME): a local document is uploaded // here as `<session>-<name>`, edited in the browser editor, and the result // is written back over the ORIGINAL file on the person's own computer. // // ⚠⚠ Hidden, but NOT sealed. Every desktop release since 0.29.0 creates it // (`action=newfolder`), uploads into it, and — the part that matters — // stats its copy by LISTING this folder (`action=index`) and downloads it // by exact path. Refusing any of those would not show an error: the // desktop's change detection reads "not there" as "not changed" and the // edit is silently never written back. So the server serves this folder // to whoever asks for it by exact path and offers it to nobody. OpenWith = ".filex-open" // Drafts is where a new document lives until its first save (issue #71): // `.filex-drafts/<user id>/<draft key>/<name>`, at the root of the storage // the person chose, with the folder and name it is meant for kept in the // `drafts` table. "Save" moves it there; "Discard" moves it to the trash. // // ⚠⚠ Sealed, and one person's. Nobody lists, searches, shares or mounts it // (it is Hidden and InDir like every name here), the scanner does not walk // it (Sealed), and the ONE shape anybody is served is a draft file of their // OWN (IsDraftOf): SealedFor lets its owner's editor read it, RefusedBy // lets its owner's editor save it, and acl.Set gives everybody else — an // administrator included — no access to it at all. Drafts = ".filex-drafts" )
The internal directory names. Each lives at a storage's root today, but they are matched as ANY path segment (see InDir) — which is what every protocol server already did, and the only rule that also covers a shared sub-folder or a confined root whose own children are judged relative to it.
const E2EKeyFile = ".filex-e2e.json"
E2EKeyFile is the key file the browser writes at the root of an end-to-end encrypted folder (the "marker", docs/E2E-ENCRYPTION.md): the folder's salt, its wrapped key slots and its level. internal/e2e names it MarkerName, from here, so the two cannot disagree.
⚠ It is NOT filex's own machinery and none of the rules above apply to it: the web UI writes it, a copy or a move of the folder must carry it, a zip of the folder must pack it (`filex decrypt` reads it from there) and a protocol client syncing the folder must mirror it. Two rules only, each its own function: a person-facing listing leaves it out (Unlisted), and a surface that holds no key may not write it (Keyless).
const KeepMarker = ".keepdir"
KeepMarker is the zero-byte file filex writes into a folder it creates, so a blob store (which has no directories, only keys) keeps the empty folder. It is a FILE, not a directory, which is why it is not in Dirs: a protocol client that deletes a folder recursively must still be able to remove it.
Variables ¶
var ErrReserved = errors.New("reserved for filex's own use")
ErrReserved is what a person-facing mutation gets when the path it names is one of filex's own (Refused). Handlers answer it as 403 `RESERVED_NAME`.
Functions ¶
func Check ¶
Check is Refused as an error: nil, or a *ReservedError naming the first of rels that is refused. For services that answer with errors rather than HTTP (the operations queue, an app's output, archive extraction).
func Dirs ¶
func Dirs() []string
Dirs returns the internal directory names. A copy: callers cannot edit the set by appending to what they were handed.
func DraftDir ¶ added in v0.48.0
DraftDir is the folder one draft is written in, storage-relative, no leading slash: `.filex-drafts/<person>/<key>`.
func DraftOwner ¶ added in v0.48.0
DraftOwner reports whose drafts area rel lies in: `.filex-drafts/<id>/…` (the id's own folder included). ok is false for anything outside the area, for the area's root, and for a second segment that is not a person's id. Anchored at the storage root, like InTrash: drafts are only ever written there.
func Hidden ¶
Hidden reports whether a person must never be shown rel: it is inside an internal directory, or it is a keep marker. The rule every person-facing listing, search, recent/starred/tag view, share and notification applies.
func InDir ¶
InDir reports whether rel IS an internal directory or lies inside one, at any depth. The rule the protocol servers (WebDAV, SFTP, FTPS, NFS, S3) apply to every path they are asked about.
func InDrafts ¶ added in v0.48.0
InDrafts reports whether rel is the drafts area or lies inside it (at the storage root).
func InTrash ¶ added in v0.47.0
InTrash reports whether rel is the soft-delete bin at the storage root, or lies inside it: the path a trashed row carries (trash.NewKey mints `.filex-trash/<key>`). Anchored at the root, unlike InDir — the one place a row's bytes can have been put in the trash.
func IsDraftOf ¶ added in v0.48.0
IsDraftOf reports whether rel is exactly one draft FILE of person's: `.filex-drafts/<person>/<key>/<name>`, where the key is a key NewDraftKey could have minted and the name is not one of filex's own. The one shape the owner's editor is served (SealedFor) and may save (RefusedBy); a folder of the area, a second level under a draft, another person's draft are not.
func IsKeyFile ¶ added in v0.50.0
IsKeyFile reports whether rel's last segment is an encrypted folder's key file.
func IsName ¶
IsName reports whether a single listing entry is filex's own: an internal directory or the keep marker. What a person-facing listing asks about each child it is about to return.
func OpenWithOriginal ¶
OpenWithOriginal maps a desktop "open with filex" working copy back to the name of the document it is a copy of: `.filex-open/a1b2c3d4e5f6-Bütçe.xlsx` → `Bütçe.xlsx`, ok=true. Anything else — a path outside the working area, a sub-folder in it, a name without the session prefix — is ok=false.
⚠⚠ The NAME is all there is to map back to. The original lives on the person's own computer (a document that already had a copy on this server — a synced one — is edited in place and never gets a working copy at all), so no path on any storage names it and no click can open it. The desktop keeps the local path to itself; the server is never told where on somebody's machine a file sits, and it must not be, because every notification is also a webhook body.
The name is the desktop's cleaned-up spelling: characters no file system accepts (`\ / : * ? " < > |`) arrive as `_`, the extension is lower-cased and a stem over 60 characters is cut. For the names people actually use — `Bütçe Özeti.xlsx`, `Rapor 2026.docx` — it is the original, byte for byte.
func Refused ¶
Refused reports whether a person-facing mutation must be refused because rel names filex's own machinery: anything Hidden, except the one shape the desktop app's "open with filex" writes.
⚠⚠ The exception is exact on purpose. Every desktop release since 0.29.0 creates `.filex-open` at the storage ROOT with `action=newfolder`, uploads `<12 hex>-<name>` straight into it with `action=upload`, lets the editor save that copy, and deletes it with `action=delete` — and refusing any one of those does not show an error anywhere a person looks: the upload failing ends the open with a dialog, a refused save is an edit that never comes back, and a refused delete leaves copies behind forever. Nothing else is let through: not a sub-folder in the area, not a copy without the session prefix, not a rename or move into or out of it, not an archive extracted into it, not the area anywhere but at the root.
func RefusedBy ¶ added in v0.48.0
RefusedBy is Refused asked on behalf of a person: identical, except that OwnDraft lets exactly that person's own draft files through (IsDraftOf). person 0 is nobody, and nobody owns a draft.
func Reserved ¶
Reserved names the part of rel that made Refused say no — the first internal directory in it, the keep marker, or (Keyless) the key file - for an error a person can read. "" when rel is none of them.
func Sealed ¶
Sealed reports whether rel must not be SERVED by path at all to a person-facing file API: it is inside the trash, the version history or the thumbnail cache. Each has its own API, keyed by something other than the path, and nothing legitimate ever asks for these by path.
⚠ `.filex-open` is deliberately not sealed — see OpenWith for the desktop clients that would lose edits silently if it were.
The drafts area IS sealed: nothing asks for it by path but its owner's editor, and SealedFor is the one exception that lets that editor through.
func SealedFor ¶ added in v0.48.0
SealedFor is Sealed as asked by one person: the drafts area stays sealed except for that person's own draft files, which their editor reads by path (preview, download, the document server's config). person 0 — no person — is Sealed.
func Unlisted ¶ added in v0.50.0
Unlisted reports whether a person-facing LISTING leaves one entry out: one of filex's own names (IsName) or an encrypted folder's key file (E2EKeyFile). The explorer's listings, search, Recent/Starred/tag views and the AI surface's file_list and file_search all ask it, by entry name.
⚠ A listing rule only. What a copy, an archive, a transfer or a protocol server carries is IsName's business: a zip of an encrypted folder without its key file is a zip nobody can ever decrypt.
Types ¶
type ReservedError ¶
type ReservedError struct{ Rel string }
ReservedError is ErrReserved for one path, from a service that refuses a mutation (Check) — it keeps the path so the answer can name the part of it that is filex's.
func (*ReservedError) Error ¶
func (e *ReservedError) Error() string
func (*ReservedError) Unwrap ¶
func (e *ReservedError) Unwrap() error
type Verb ¶
type Verb int
Verb says which person-facing mutation is asking about a path — only so the desktop's open-with round trip can be told apart from everything else.
const ( // Change is every mutation that is not one of the three below: new // folder anywhere else, new file, rename, move, copy, archive // extraction and creation, text save, chunked and staged upload, share, // grant, restore, an app's output. Refused on every Hidden path. Change Verb = iota // MakeWorkArea is `action=newfolder` creating `<storage>://.filex-open` // (desktop/src/openwith-io.ts ensureScratchDir). MakeWorkArea // PutWorkCopy is `action=upload` of `<session>-<name>` into it // (uploadFile) and the document editor's save of that copy (the // OnlyOffice config the desktop's editor window asks for). PutWorkCopy // DropWorkCopy is `action=delete` of that copy after it was written back // (deleteRemote). DropWorkCopy // Mounted is a protocol client's write (WebDAV, SFTP, FTPS, NFS, S3): // filex's directories are refused (InDir), and the keep marker is an // ordinary file there — a client deleting a folder recursively must be // able to remove it, and a sync tool copying one must be able to write // it, or the folder can never be emptied or mirrored. Mounted // OwnDraft is a person acting on a draft of their OWN (issue #71): their // editor saving its content in place (the text editor, the document // server's callback, an app committing a new version of it) and the trash // bringing a discarded one back. Only RefusedBy can let it through — it // needs to know whose draft it is and who is asking; to Refused (which // knows neither) it is Change. OwnDraft // Keyless is Change from a surface that holds no encryption key: the AI // REST routes and the MCP tools (handlers.aiOps), and everything that // writes through them (ShareX, the upload ticket). Refused wherever // Change is, AND on an encrypted folder's key file (E2EKeyFile). Such a // surface cannot read or re-key that file, and an overwrite, a rename or // a delete of it loses the keys of every file in the folder; a key file // written where there was none makes an ordinary folder look encrypted. Keyless )