Documentation
¶
Overview ¶
Package files implements the `olares-cli files ...` command tree, which talks to the per-user files-backend (the upstream `files` repo) over its /api/resources REST surface.
The files-backend models every resource as a 3-segment "front-end path":
<fileType>/<extend>/<subPath>
where `fileType` selects the storage class, `extend` selects the concrete volume / repo / account inside that class, and `subPath` is the relative path inside that volume. See files/pkg/models/file_param.go (FileParam.convert) and files/pkg/common/constant.go for the canonical definitions.
We expose the full path verbatim on the CLI surface — the user always types all three segments — so the tooling stays close to the protocol. path.go centralizes parsing & validation; commands like `ls`, `cat`, `cp` (the latter two land in Phase 2) all consume the resulting FrontendPath.
Index ¶
- func NewCatCommand(f *cmdutil.Factory) *cobra.Command
- func NewCpCommand(f *cmdutil.Factory) *cobra.Command
- func NewDownloadCommand(f *cmdutil.Factory) *cobra.Command
- func NewFilesCommand(f *cmdutil.Factory) *cobra.Command
- func NewLsCommand(f *cmdutil.Factory) *cobra.Command
- func NewMkdirCommand(f *cmdutil.Factory) *cobra.Command
- func NewMvCommand(f *cmdutil.Factory) *cobra.Command
- func NewRenameCommand(f *cmdutil.Factory) *cobra.Command
- func NewReposCommand(f *cmdutil.Factory) *cobra.Command
- func NewRmCommand(f *cmdutil.Factory) *cobra.Command
- func NewShareCommand(f *cmdutil.Factory) *cobra.Command
- func NewUploadCommand(f *cmdutil.Factory) *cobra.Command
- type FrontendPath
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func NewCatCommand ¶
NewCatCommand: `olares-cli files cat <remote-path>`
Streams the raw bytes of a single remote file to stdout. Two wire flows are dispatched based on the FrontendPath's namespace:
- drive / sync / cache / external (and share): GET /api/raw/<encPath>?inline=true — same path the LarePass web app uses for text-content previews; `inline=true` only affects Content-Disposition, the body is identical.
- awss3 / google / dropbox / tencent: GET /drive/download_sync_stream?drive=<type>&cloud_file_path=<path>&name=<extend>, mirroring the web app's `generateDownloadUrl` helper (apps/packages/app/src/api/files/v2/{awss3,google,dropbox}/utils.ts). The /api/raw/ endpoint on these namespaces returns metadata / preview JSON rather than raw bytes, so it's not a substitute for cat — see cat_test.go for the divergence in detail.
Like `cat` itself, this is binary-safe: we don't sniff or interpret the body, we just copy it through. That means cat-ing a huge image will dump the bytes — the user is expected to pipe to `less`, `head`, or a similar tool when they care about safety.
We Stat the path before fetching so a directory target produces a clear "is a directory" error rather than the server's terser "not a file, path: ..." 400.
func NewCpCommand ¶
NewCpCommand: `olares-cli files cp [-r] <src>... <dst>`
Copies one or more remote entries to a remote destination via the per-user files-backend's PATCH /api/paste/<node>/ endpoint. The wire action is "copy"; mv uses the same code path with action="move".
The CLI takes a Unix-style stance on multi-source / target-shape:
- Trailing '/' on <dst> means "drop each source into this directory, preserving its basename" — e.g. `cp foo bar baz/` yields baz/foo and baz/bar. This matches `files upload`'s <remote> rule and download's resolveLocalFile.
- Without a trailing '/' on <dst>, exactly one <src> is allowed and <dst> is treated as the full target path (rename on the way in). Multi-source + non-dir <dst> is rejected.
- --recursive / -r is required for directory sources, same refusal pattern as Unix `cp -r`. -R is accepted as an alias.
The PATCH endpoint returns one task_id per call (the actual byte movement runs on the server's task queue), so a multi-source invocation prints N task_ids. We don't poll for completion in this iteration — that's a separate concern best built once the ws / task-status surface stabilises.
func NewDownloadCommand ¶
NewDownloadCommand: `olares-cli files download <remote> [<local>]`
Pulls a single file or a whole directory tree from the per-user files-backend down to the local filesystem. Single-file downloads resume via the server's native `Range: bytes=N-` support (raw_service.go's parseRangeHeader); directories walk recursively over /api/resources and pull each file with the same code path.
Local destination semantics:
- omitted → ./<basename(remote)> in the current directory
- existing dir → write under that directory using the remote basename (mirrors `cp`'s behavior)
- any other path → treated as the full local target path (file mode), or the directory to create / use as the root (directory mode).
Concurrency only kicks in for directory mode; --parallel N runs N file downloads in flight at once. Per-file resume + retry are independent of --parallel.
func NewFilesCommand ¶
NewFilesCommand returns the `files` parent command, ready to be added to the olares-cli root.
Current verbs:
files ls — list a directory (cmd/ctl/files/ls.go)
files upload — resumable chunked upload (cmd/ctl/files/upload.go)
files download — single-file or recursive pull (cmd/ctl/files/download.go)
files cat — stream a file to stdout (cmd/ctl/files/cat.go)
files mkdir — create a directory (with -p) (cmd/ctl/files/mkdir.go,
internal/files/mkdir/mkdir.go)
files rm — batched DELETE (cmd/ctl/files/rm.go)
files cp — server-side copy via paste (cmd/ctl/files/cp.go)
files mv — server-side move via paste (cmd/ctl/files/cp.go, action="move")
files rename — synchronous in-place rename (cmd/ctl/files/rename.go)
files share — create / list / remove shares (cmd/ctl/files/share.go,
internal: cross-user cmd/ctl/files/share_create.go)
public: external link
smb: Samba network share
files repos — list / inspect Sync (Seafile) (cmd/ctl/files/repos.go,
libraries (repo_id catalog) internal/files/repos/repos.go)
cp / mv share a single PATCH /api/paste/<node>/ wire path (see cmd/ctl/files/cp.go and internal/files/cp/cp.go); the only difference is the action verb in the JSON body. `rename` is a distinct synchronous PATCH /api/resources/.../?destination=... call (see cmd/ctl/files/rename.go and internal/files/rename/rename.go) — no <node> URL segment, no task_id, basename-only payload. `share` fans out across the /api/share/share_path/ surface (see internal/files/share/share.go); the three creation flavors converge on the same POST endpoint and disambiguate via the `share_type` field in the JSON body.
The Factory is supplied by the root command so credential resolution and HTTP-client setup happen once per process. Identity is whichever profile `olares-cli profile use` (or `profile login` / `profile import`) most recently selected; there is no per-invocation override flag.
See cmd/ctl/files/path.go for the front-end path schema and docs/notes/olares-cli-auth-profile-config.md for the broader auth / profile design.
func NewLsCommand ¶
NewLsCommand: `olares-cli files ls <frontendPath> [--json]`
Calls GET <FilesURL>/api/resources/<fileType>/<extend><subPath> on the per-user files-backend (proxied via files.<terminusName>) and renders the result. The access token is injected by Factory's HTTP client as the `X-Authorization` header — see pkg/cmdutil/factory.go for why that header (not the standard Authorization: Bearer) is the right one for Olares.
Errors:
- bad / missing path is rejected client-side via ParseFrontendPath
- 401/403 from the backend is reported with the same "run profile login" CTA that DefaultProvider uses, so the message is consistent across "no token" / "expired token" / "server-rejected token"
- other non-2xx responses surface the backend's error/message JSON field verbatim, which is usually enough to debug (unknown node, missing repo, permission denied, ...)
func NewMkdirCommand ¶
NewMkdirCommand: `olares-cli files mkdir [-p] <remote-path>`.
Creates a directory on the per-user files-backend. Wire shape:
POST /api/resources/<fileType>/<extend><subPath>/
matching the LarePass web app's per-driver `createDir` helpers (apps/.../api/files/v2/{drive,sync,cache,external,awss3,dropbox, google,tencent}/utils.ts) — the trailing '/' on the URL is what distinguishes "create directory" from "create empty file" on this endpoint. Body is empty.
Two important behavioral notes:
**Auto-rename on collision (NOT 409).** The current files- backend silently auto-renames on collision: POST `/.../Documents/` when `Documents` already exists creates `Documents (1)` next to the original instead of returning 409. That's surprising for an "ensure dir exists" call — so the CLI surfaces this in the post-run summary and recommends running `files ls` to confirm what landed.
**`-p` mode does parent-listing existence checks.** Because of the auto-rename quirk, naively POSTing every prefix in `-p drive/Home/A/B/C/` would produce `A (1)/B (1)/C` if any prefix already existed. The CLI works around this by listing each prefix's parent and skipping the segment when the basename is already there as a directory. The cost is one extra GET per existing prefix, which is well worth the correctness win.
Supported namespaces: ALL of the 3-segment frontend types (drive, sync, cache, external, awss3, google, dropbox, tencent — and also share/internal if the user's role permits, the wire endpoint is the same; the server will 403 on read-only views). Tencent is supported here even though `files upload` rejects it — mkdir shares the generic /api/resources POST path with all other namespaces, so there's no upload-pipeline divergence to worry about.
func NewMvCommand ¶
NewMvCommand: `olares-cli files mv [-r] <src>... <dst>`. Same flow as cp; the wire action is "move" instead of "copy". Surfaced as a separate command (rather than an alias of cp --move) because users reach for the verb they know — making mv a typed command also keeps the help text honest about what each verb does.
func NewRenameCommand ¶
NewRenameCommand: `olares-cli files rename <remote-path> <new-name>` (alias: `rn`).
In-place rename — the entry stays in the same parent; only its basename changes. Wire shape mirrors the LarePass web app's renameFileItem helper (apps/.../api/files/v2/common/utils.ts):
PATCH /api/resources/<fileType>/<extend><subPath>[/]?destination=<encName>
This is intentionally NOT routed through `cp` / `mv`'s PATCH /api/paste/<node>/ surface, because:
- Rename is synchronous on the backend (no task_id), so the user gets a "done" response, not a "queued" one.
- It doesn't take a {node} URL segment — /api/resources is the uniform per-resource path that works against drive / sync / cloud / external without any node hint.
- The only payload is the new bare basename in the `destination` query value; there's no JSON body. That matches the frontend's "Rename" modal exactly.
`mv` can also rename (single-source, non-trailing-slash <dst>) but goes through the async paste queue. Prefer `rename` for the simple "I just want to change the name" case — fewer round-trips, no node resolution, and immediate feedback.
CLI semantics:
- <remote-path>: full 3-segment frontend path (e.g. `drive/Home/Documents/foo.pdf`); same parser as `ls`/`cp`. A trailing '/' marks the source as a directory and is preserved on the wire so the backend routes through its directory handler.
- <new-name>: BARE basename only. No '/' or '\\' allowed — cross-directory moves are `mv`'s job. Empty / "."/".." are rejected as obvious typos.
- Refuses to rename the volume root (e.g. `drive/Home/`) — same safety policy as `rm` and `cp`.
- Refuses a no-op rename where <new-name> equals the source's current basename — almost always a typo.
Collision handling is server-decided: the backend may auto-rename, overwrite, or 409 depending on the storage class. We surface its answer verbatim. If a future need for explicit `--force` / `--no-clobber` flags shows up we'll thread `override`/`rename` query params through the rename package.
func NewReposCommand ¶
NewReposCommand returns the `olares-cli files repos` parent command, which surfaces the per-user files-backend's catalog of Sync (Seafile) libraries.
Why a top-level verb rather than a subcommand under, say, `files share` or `files ls`:
- The `<repo_id>` is what the rest of the CLI types into the `<extend>` segment of `sync/<repo_id>/<sub>` — without a way to enumerate IDs from the CLI, the user has to copy them out of the LarePass web app every time. Surfacing repos as a first-class verb keeps the discovery loop in-CLI.
- The Sync repo catalog is its own concept (it's the only fileType whose `<extend>` is a server-assigned UUID rather than a user-typed name). The other fileTypes don't need a symmetric `cache repos` / `external repos` because their `<extend>` values come from `/api/nodes/` (which `files upload --node` already exposes) or from URLs the user knows.
Verbs:
list enumerate the user's repos (mine / share-to-me /
shared / all), with --json for scripting
get fetch a single repo's metadata by id
create provision a new (unencrypted) Sync library
rename change the display name of a repo (id stays stable)
rm delete a repo (irreversible from the CLI)
Encryption / unlock is intentionally NOT exposed here: the per-user files-backend's createLibrary endpoint has no password / encryption flag, and the LarePass UI doesn't expose one either. Encrypted libraries must be created from the LarePass app or directly via Seahub.
func NewRmCommand ¶
NewRmCommand: `olares-cli files rm [-r] [-f] <remote-path>...`
Deletes one or more remote entries via the per-user files-backend's batch DELETE endpoint. Multiple targets sharing a parent directory collapse into a single wire request — the LarePass web app does the same, see batchDeleteFileItems in v2/common/utils.ts.
Conventions:
- --recursive / -r / -R is required to remove directories. A trailing '/' on a target is interpreted as "this is a directory" intent and triggers the same check.
- --force / -f skips the interactive confirmation prompt. Without it, we list what would be deleted and ask y/N. In a non-TTY environment (CI, piped stdin) we refuse rather than guessing — the user has to opt in to deletion explicitly.
- Removing the root of a volume (`drive/Home/`, `sync/<repo>/`, ...) is rejected by the planner; that operation has to be expressed differently if it's ever needed.
func NewShareCommand ¶
NewShareCommand returns the `olares-cli files share` parent command, which groups together the three folder-share creation flavors (internal / public / smb) plus the management verbs the resulting share IDs need to be useful (list / get / rm).
All three creation flavors converge on the same wire endpoint —
POST /api/share/share_path/<fileType>/<extend><subPath>/
— with the share-type discriminator in the JSON body. The split into three subcommands lives at the CLI surface only, because each flavor has a meaningfully different flag set: internal takes a member list, public takes a password + expiration + upload limits, SMB takes a public toggle + SMB-account list. Trying to share-by- flag would force the user to think about the wire shape; share-by- subcommand keeps each flag set focused on one workflow.
Management verbs (`list` / `get` / `rm`) are kept at this level rather than under the per-type subcommand because a share id is share-type-agnostic on the wire (the `/api/share/share_path/` surface treats every type the same way once the share exists).
func NewUploadCommand ¶
NewUploadCommand: `olares-cli files upload <local> <remote>`
Pushes a single file or a whole directory tree from the local filesystem into one of the supported per-user files-backend namespaces (drive/Home, drive/Data, sync/<repo_id>, cache/<node>, external/<node>/<volume>, awss3/<account>, google/<account>, or dropbox/<account>) using the same chunked-resumable protocol the LarePass web app speaks (Resumable.js + the Drive v2 endpoints under /upload/upload-link, /upload/file-uploaded-bytes, /api/resources/...). See internal/files/upload/uploader.go for the wire-level details.
awss3 / google / dropbox share the chunk pipeline + resume probe with Drive (the web app's Awss3DataAPI / GoogleDataAPI / DropboxDataAPI all extend DriveDataAPI **without** overriding getFileServerUploadLink or getFileUploadedBytes — see apps/packages/app/src/api/files/v2/{awss3,google,dropbox}/data.ts), so stage 1 is byte-identical to Drive. They're a TWO-STAGE upload though: stage 1 only delivers bytes to the Olares files-backend's staging area; stage 2 is a server-side "Olares-staging → cloud bucket" transfer task that the backend queues and the client must wait on. The taskId for stage 2 is returned in the FINAL chunk's response body, and we drive the polling via upload.Client.WaitCloudTask (see runUploads). This mirrors resumejs.ts onFileUploadSuccess L591-606, where the web app's Taskmanager.addTask consumes the same taskId and runs the same poll loop. Tencent is the lone holdout (see TencentDataAPI.getFileServerUploadLink, which posts to /drive/create_direct_upload_task and uploads via the octet /drive/direct_upload_file flow); we don't speak that protocol yet, so this verb rejects it explicitly with a self-describing error.
Resume is enabled by default and is server-driven: before each file the CLI calls /upload/file-uploaded-bytes/<node>/ to ask "how much do you already have?", floors that to a chunk boundary, and resumes from there. There's no local progress file — re-running the same command after a Ctrl-C just re-asks the server, which is robust against any state drift between client invocations.
File-level concurrency: --parallel N runs N files concurrently through an errgroup. Within a single file, chunks are sent sequentially (matching the web app's simultaneousUploads=1 default); pipelining chunks per file is not implemented because the resume-probe + chunk-sequence assumes a single in-flight chunk per file at a time.
Path schema for <remote>: same as `files ls`, but the upload target must live under one of:
- drive/Home/<sub>
- drive/Data/<sub>
- sync/<repo_id>/<sub>
- cache/<node>/<sub>
- external/<node>/<volume>/<sub>
- awss3/<account>/<bucket>/<sub>
- google/<account>/<sub>
- dropbox/<account>/<sub>
Tencent (`tencent/<account>/<sub>`) uses a different upload protocol (POST /drive/create_direct_upload_task → octet upload via /drive/direct_upload_file/<task_id>) that this verb does not yet implement; it's rejected with a self-describing error. Trailing '/' on <remote> is significant — it's how we distinguish "upload into this directory" from "upload as this exact path (rename)" for the single-file case.
Node selection cascade (mirrors `files cp`):
- --node <name>: explicit override, wins over everything else.
- cache/<node>/... or external/<node>/...: <node> from the path.
- everything else (drive/sync/awss3/google/dropbox): first node returned by /api/nodes/.
Types ¶
type FrontendPath ¶
type FrontendPath struct {
// FileType is the (always-lowercase) storage class: drive/cache/sync/...
FileType string
// Extend is the volume / repo / account selector. Its semantics depend on
// FileType (Home|Data for drive, node name for cache/external, repo_id for
// sync, account key for cloud drives, ...). CLI-side we only hard-validate
// the drive case; everything else is left to the backend.
Extend string
// SubPath is the path inside Extend, always starting with '/'. Root is "/".
// A trailing slash present in the input is preserved (the backend uses it
// as a "this is a directory" hint in some places). It is also synthesized
// for `<fileType>/<extend>` (no subpath), where the only valid backend
// interpretation is "the extend root", because the backend's
// FileParam.convert() splits on '/' and rejects len < 3 for non-root
// resources.
SubPath string
}
FrontendPath is the parsed view of a 3-segment files-backend front-end path. Construct via ParseFrontendPath; the zero value has no meaning.
func ParseFrontendPath ¶
func ParseFrontendPath(raw string) (FrontendPath, error)
ParseFrontendPath parses a user-supplied path string into a FrontendPath.
Examples:
"drive/Home/" → {drive, Home, "/"}
"drive/Home" → {drive, Home, "/"} (root synthesized)
"drive/Home/Documents" → {drive, Home, "/Documents"}
"drive/Home/Documents/" → {drive, Home, "/Documents/"}
"sync/<repo_id>/sub/dir" → {sync, <repo_id>, "/sub/dir"}
"awss3/<account>/<bucket>/k.txt" → {awss3, <account>, "/<bucket>/k.txt"}
Validation:
- Path must have at least 2 non-empty segments (fileType + extend). `drive/Home` (no trailing slash, no subpath) is accepted and treated as the extend root, because the backend's FileParam.convert() splits on '/' and rejects len < 3 — there is no valid reading of a bare extend other than "the root directory".
- FileType must be a known value (case-sensitive lowercase). Unknown values fail fast on the client to avoid an opaque 500 from the server.
- When FileType=="drive", Extend must be "Home" or "Data" (case-sensitive).
- Other FileTypes' Extend values (node names, repo ids, account keys) are not pre-validated locally; the backend is the source of truth for those.
- Path traversal segments like ".." are NOT stripped here — the backend applies its own sandboxing. We do collapse runs of "//" to a single "/" via path.Clean while preserving any user-supplied trailing slash.
func (FrontendPath) HasTrailingSlash ¶
func (p FrontendPath) HasTrailingSlash() bool
HasTrailingSlash reports whether String() ends with '/' — i.e. whether the path represents a directory. Useful for callers that want to disambiguate "list this directory" from "fetch this resource by exact name" without re-parsing. Derived from SubPath so it always agrees with String(); in particular, `<fileType>/<extend>` (no subpath, no trailing slash on input) reports true because SubPath is "/" — the only valid interpretation of a bare extend reference is its root directory.
func (FrontendPath) String ¶
func (p FrontendPath) String() string
String renders the canonical front-end path as `<fileType>/<extend><subPath>`. SubPath always starts with '/', so the output naturally looks like "drive/Home/Documents" or "drive/Home/" for the root with a trailing slash.
This is the human-readable form, suitable for error messages and logs. For URL construction use URLPath() — String() does NOT percent-encode.
func (FrontendPath) URLPath ¶
func (p FrontendPath) URLPath() string
URLPath returns the same logical path as String() but percent-encoded with internal/files/encodepath.EncodeURL — the Go counterpart of the web app's apps/packages/app/src/utils/encode.ts `encodeUrl` (encodeURIComponent per '/' segment). This MUST stay aligned with download/cat/rm/upload, which all use internal/files/encodepath; url.PathEscape is not equivalent (e.g. '+' and '!*'()' differ) and would make `ls` hit different wire paths than the other verbs for the same user-typed path.