files

package
v0.0.0-...-310f30e Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 15, 2026 License: AGPL-3.0 Imports: 34 Imported by: 0

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

Constants

This section is empty.

Variables

This section is empty.

Functions

func NewCatCommand

func NewCatCommand(f *cmdutil.Factory) *cobra.Command

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

func NewCpCommand(f *cmdutil.Factory) *cobra.Command

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

func NewDownloadCommand(f *cmdutil.Factory) *cobra.Command

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

func NewFilesCommand(f *cmdutil.Factory) *cobra.Command

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

func NewLsCommand(f *cmdutil.Factory) *cobra.Command

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

func NewMkdirCommand(f *cmdutil.Factory) *cobra.Command

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:

  1. **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.

  2. **`-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

func NewMvCommand(f *cmdutil.Factory) *cobra.Command

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

func NewRenameCommand(f *cmdutil.Factory) *cobra.Command

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

func NewReposCommand(f *cmdutil.Factory) *cobra.Command

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

func NewRmCommand(f *cmdutil.Factory) *cobra.Command

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

func NewShareCommand(f *cmdutil.Factory) *cobra.Command

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

func NewUploadCommand(f *cmdutil.Factory) *cobra.Command

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL