dsh-over-ssh 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +119 -2
- package/agents/agent-manifest.json +20 -0
- package/agents/linux-amd64/dsh-over-ssh-agent +0 -0
- package/agents/linux-arm64/dsh-over-ssh-agent +0 -0
- package/cordis.patch.yml +84 -0
- package/lib/client.js +6611 -0
- package/lib/file-references.js +366 -0
- package/lib/fs.js +1368 -0
- package/lib/index.js +19034 -0
- package/lib/local-bridge-refs.js +16 -0
- package/lib/local-bridge.js +16 -0
- package/lib/terminal-wrapper.js +294 -0
- package/lib/types/src/client/controls.d.ts +131 -0
- package/lib/types/src/client/core/card-focus-trap.d.ts +45 -0
- package/lib/types/src/client/core/controls.d.ts +164 -0
- package/lib/types/src/client/core/latest.d.ts +48 -0
- package/lib/types/src/client/core/locale.d.ts +38 -0
- package/lib/types/src/client/core/message.d.ts +37 -0
- package/lib/types/src/client/core/panel-model.d.ts +134 -0
- package/lib/types/src/client/core/portal.d.ts +266 -0
- package/lib/types/src/client/core/remote.d.ts +84 -0
- package/lib/types/src/client/core/styles.d.ts +458 -0
- package/lib/types/src/client/file-tree-io.d.ts +19 -0
- package/lib/types/src/client/file-tree.d.ts +271 -0
- package/lib/types/src/client/flows/directory-browser.d.ts +587 -0
- package/lib/types/src/client/flows/session-directory.d.ts +415 -0
- package/lib/types/src/client/flows/workspace-directory-flow.d.ts +480 -0
- package/lib/types/src/client/index.d.ts +49 -0
- package/lib/types/src/client/locale.d.ts +31 -0
- package/lib/types/src/client/panel-model.d.ts +37 -0
- package/lib/types/src/client/panel.d.ts +77 -0
- package/lib/types/src/client/roots/connection-test.d.ts +69 -0
- package/lib/types/src/client/roots/roots-panel-state.d.ts +307 -0
- package/lib/types/src/client/roots/roots-panel-view.d.ts +280 -0
- package/lib/types/src/client/roots/roots-panel.d.ts +364 -0
- package/lib/types/src/client/roots/transfer-panel-view.d.ts +35 -0
- package/lib/types/src/client/roots/transfer-panel.d.ts +241 -0
- package/lib/types/src/client/roots/transfer-progress.d.ts +67 -0
- package/lib/types/src/client/roots-panel-view.d.ts +236 -0
- package/lib/types/src/client/roots-panel.d.ts +347 -0
- package/lib/types/src/client/session/session-panel-view.d.ts +336 -0
- package/lib/types/src/client/session/session-panel.d.ts +322 -0
- package/lib/types/src/client/session/session-remote-mark.d.ts +110 -0
- package/lib/types/src/client/session/session-row-actions.d.ts +398 -0
- package/lib/types/src/client/session-panel.d.ts +152 -0
- package/lib/types/src/client/styles.d.ts +252 -0
- package/lib/types/src/client/workspace-directory-flow.d.ts +342 -0
- package/lib/types/src/config.d.ts +312 -0
- package/lib/types/src/file-references.d.ts +25 -0
- package/lib/types/src/fs.d.ts +46 -0
- package/lib/types/src/host/app/backend.d.ts +170 -0
- package/lib/types/src/host/app/recycle-bin/recycle-bin-contract.d.ts +247 -0
- package/lib/types/src/host/app/recycle-bin/recycle-bin-move.d.ts +18 -0
- package/lib/types/src/host/app/recycle-bin/recycle-bin-service.d.ts +45 -0
- package/lib/types/src/host/app/recycle-bin/recycle-bin-world.d.ts +38 -0
- package/lib/types/src/host/app/recycle-bin-contract.d.ts +215 -0
- package/lib/types/src/host/app/recycle-bin-move.d.ts +18 -0
- package/lib/types/src/host/app/recycle-bin-service.d.ts +45 -0
- package/lib/types/src/host/app/recycle-bin-world.d.ts +35 -0
- package/lib/types/src/host/app/roots/roots-api-connections.d.ts +49 -0
- package/lib/types/src/host/app/roots/roots-api-contract.d.ts +369 -0
- package/lib/types/src/host/app/roots/roots-api-directories.d.ts +13 -0
- package/lib/types/src/host/app/roots/roots-api-http.d.ts +77 -0
- package/lib/types/src/host/app/roots/roots-api-long-poll.d.ts +130 -0
- package/lib/types/src/host/app/roots/roots-api-projections.d.ts +102 -0
- package/lib/types/src/host/app/roots/roots-api-roots.d.ts +13 -0
- package/lib/types/src/host/app/roots/roots-api-state.d.ts +22 -0
- package/lib/types/src/host/app/roots/roots-api-transfers.d.ts +26 -0
- package/lib/types/src/host/app/roots/roots-api.d.ts +55 -0
- package/lib/types/src/host/app/roots/user-roots.d.ts +552 -0
- package/lib/types/src/host/app/roots-api-connections.d.ts +49 -0
- package/lib/types/src/host/app/roots-api-contract.d.ts +339 -0
- package/lib/types/src/host/app/roots-api-directories.d.ts +13 -0
- package/lib/types/src/host/app/roots-api-http.d.ts +77 -0
- package/lib/types/src/host/app/roots-api-projections.d.ts +102 -0
- package/lib/types/src/host/app/roots-api-roots.d.ts +13 -0
- package/lib/types/src/host/app/roots-api-state.d.ts +22 -0
- package/lib/types/src/host/app/roots-api-support.d.ts +182 -0
- package/lib/types/src/host/app/roots-api-transfers.d.ts +13 -0
- package/lib/types/src/host/app/roots-api.d.ts +55 -0
- package/lib/types/src/host/app/routed-fs.d.ts +239 -0
- package/lib/types/src/host/app/serialize.d.ts +37 -0
- package/lib/types/src/host/app/session-admin/session-admin-context.d.ts +36 -0
- package/lib/types/src/host/app/session-admin/session-admin-contract.d.ts +221 -0
- package/lib/types/src/host/app/session-admin/session-admin-delete.d.ts +34 -0
- package/lib/types/src/host/app/session-admin/session-admin-service.d.ts +40 -0
- package/lib/types/src/host/app/session-admin-context.d.ts +36 -0
- package/lib/types/src/host/app/session-admin-contract.d.ts +214 -0
- package/lib/types/src/host/app/session-admin-delete.d.ts +34 -0
- package/lib/types/src/host/app/session-admin-service.d.ts +38 -0
- package/lib/types/src/host/app/takeover/takeover-plan.d.ts +227 -0
- package/lib/types/src/host/app/takeover/takeover-rows.d.ts +55 -0
- package/lib/types/src/host/app/transport/transport-selector.d.ts +98 -0
- package/lib/types/src/host/app/transport/transport-service.d.ts +82 -0
- package/lib/types/src/host/app/transport-selector.d.ts +98 -0
- package/lib/types/src/host/app/transport-service.d.ts +75 -0
- package/lib/types/src/host/app/user-roots.d.ts +461 -0
- package/lib/types/src/host/domain/bin-location.d.ts +79 -0
- package/lib/types/src/host/domain/capability.d.ts +78 -0
- package/lib/types/src/host/domain/connection.d.ts +95 -0
- package/lib/types/src/host/domain/errors.d.ts +55 -0
- package/lib/types/src/host/domain/glob-pattern.d.ts +62 -0
- package/lib/types/src/host/domain/literal-text.d.ts +63 -0
- package/lib/types/src/host/domain/path-grammar.d.ts +84 -0
- package/lib/types/src/host/domain/recycle-bin.d.ts +337 -0
- package/lib/types/src/host/domain/root.d.ts +74 -0
- package/lib/types/src/host/domain/roots/bin-location.d.ts +91 -0
- package/lib/types/src/host/domain/roots/connection.d.ts +106 -0
- package/lib/types/src/host/domain/roots/root.d.ts +108 -0
- package/lib/types/src/host/domain/roots/route-table.d.ts +102 -0
- package/lib/types/src/host/domain/roots/session-roots.d.ts +151 -0
- package/lib/types/src/host/domain/roots/ssh-config.d.ts +73 -0
- package/lib/types/src/host/domain/route-table.d.ts +80 -0
- package/lib/types/src/host/domain/session/session-admin.d.ts +345 -0
- package/lib/types/src/host/domain/session/terminal-target.d.ts +136 -0
- package/lib/types/src/host/domain/session-admin.d.ts +330 -0
- package/lib/types/src/host/domain/shell.d.ts +47 -0
- package/lib/types/src/host/domain/ssh-config.d.ts +73 -0
- package/lib/types/src/host/domain/terminal-target.d.ts +132 -0
- package/lib/types/src/host/domain/tool-requests.d.ts +369 -0
- package/lib/types/src/host/domain/transfer/recycle-bin.d.ts +337 -0
- package/lib/types/src/host/domain/transfer/transfer.d.ts +260 -0
- package/lib/types/src/host/domain/transfer.d.ts +260 -0
- package/lib/types/src/host/infra/backend/local-backend.d.ts +84 -0
- package/lib/types/src/host/infra/backend/local-version.d.ts +43 -0
- package/lib/types/src/host/infra/backend/platform-local-backend.d.ts +89 -0
- package/lib/types/src/host/infra/backend/remote-backend.d.ts +239 -0
- package/lib/types/src/host/infra/cache/lru.d.ts +52 -0
- package/lib/types/src/host/infra/cache/versioned.d.ts +77 -0
- package/lib/types/src/host/infra/temp-name.d.ts +19 -0
- package/lib/types/src/host/infra/transport/agent/agent-installer.d.ts +121 -0
- package/lib/types/src/host/infra/transport/agent/agent-protocol-client.d.ts +132 -0
- package/lib/types/src/host/infra/transport/agent/agent-protocol-codec.d.ts +140 -0
- package/lib/types/src/host/infra/transport/agent/agent-protocol-errors.d.ts +41 -0
- package/lib/types/src/host/infra/transport/agent/agent-protocol-frames.d.ts +197 -0
- package/lib/types/src/host/infra/transport/agent/agent-protocol-types.d.ts +564 -0
- package/lib/types/src/host/infra/transport/agent/agent-transport.d.ts +418 -0
- package/lib/types/src/host/infra/transport/agent-installer.d.ts +121 -0
- package/lib/types/src/host/infra/transport/agent-protocol-client.d.ts +123 -0
- package/lib/types/src/host/infra/transport/agent-protocol-codec.d.ts +131 -0
- package/lib/types/src/host/infra/transport/agent-protocol-errors.d.ts +41 -0
- package/lib/types/src/host/infra/transport/agent-protocol-frames.d.ts +197 -0
- package/lib/types/src/host/infra/transport/agent-protocol-types.d.ts +509 -0
- package/lib/types/src/host/infra/transport/agent-transport.d.ts +353 -0
- package/lib/types/src/host/infra/transport/alias-transport.d.ts +206 -0
- package/lib/types/src/host/infra/transport/askpass-fs.d.ts +20 -0
- package/lib/types/src/host/infra/transport/askpass.d.ts +133 -0
- package/lib/types/src/host/infra/transport/command-pool.d.ts +61 -0
- package/lib/types/src/host/infra/transport/fs-commands.d.ts +140 -0
- package/lib/types/src/host/infra/transport/git-status-route.d.ts +63 -0
- package/lib/types/src/host/infra/transport/git-status.d.ts +101 -0
- package/lib/types/src/host/infra/transport/node-spawn.d.ts +26 -0
- package/lib/types/src/host/infra/transport/session-window.d.ts +51 -0
- package/lib/types/src/host/infra/transport/shell-quote.d.ts +59 -0
- package/lib/types/src/host/infra/transport/ssh/alias-transport.d.ts +206 -0
- package/lib/types/src/host/infra/transport/ssh/askpass-fs.d.ts +20 -0
- package/lib/types/src/host/infra/transport/ssh/askpass.d.ts +133 -0
- package/lib/types/src/host/infra/transport/ssh/shell-quote.d.ts +62 -0
- package/lib/types/src/host/infra/transport/ssh/ssh-argv.d.ts +43 -0
- package/lib/types/src/host/infra/transport/ssh/ssh2-transport.d.ts +328 -0
- package/lib/types/src/host/infra/transport/ssh-argv.d.ts +43 -0
- package/lib/types/src/host/infra/transport/ssh2-transport.d.ts +302 -0
- package/lib/types/src/host/infra/transport/transport-base.d.ts +34 -0
- package/lib/types/src/host/infra/transport/transport.d.ts +527 -0
- package/lib/types/src/host/integration/agent/agent-install-io.d.ts +33 -0
- package/lib/types/src/host/integration/agent/agent-preflight.d.ts +56 -0
- package/lib/types/src/host/integration/agent/askpass-registry.d.ts +66 -0
- package/lib/types/src/host/integration/agent-install-io.d.ts +33 -0
- package/lib/types/src/host/integration/agent-preflight.d.ts +56 -0
- package/lib/types/src/host/integration/anchor-directory.d.ts +56 -0
- package/lib/types/src/host/integration/askpass-registry.d.ts +59 -0
- package/lib/types/src/host/integration/bin-root-resolver.d.ts +81 -0
- package/lib/types/src/host/integration/connection-test.d.ts +73 -0
- package/lib/types/src/host/integration/file-reference-provider.d.ts +115 -0
- package/lib/types/src/host/integration/fs/file-reference-provider.d.ts +142 -0
- package/lib/types/src/host/integration/fs/fs-provider.d.ts +368 -0
- package/lib/types/src/host/integration/fs/git-status-wiring.d.ts +21 -0
- package/lib/types/src/host/integration/fs/platform-errors.d.ts +94 -0
- package/lib/types/src/host/integration/fs/remote-file-tools.d.ts +369 -0
- package/lib/types/src/host/integration/fs/remote-index.d.ts +177 -0
- package/lib/types/src/host/integration/fs-provider.d.ts +226 -0
- package/lib/types/src/host/integration/lsp/lsp-framing.d.ts +38 -0
- package/lib/types/src/host/integration/lsp/lsp-registry.d.ts +67 -0
- package/lib/types/src/host/integration/lsp/lsp-session.d.ts +130 -0
- package/lib/types/src/host/integration/management-tools.d.ts +64 -0
- package/lib/types/src/host/integration/recycle-bin-service.d.ts +247 -0
- package/lib/types/src/host/integration/remote/connection-test.d.ts +73 -0
- package/lib/types/src/host/integration/remote/remote-face.d.ts +127 -0
- package/lib/types/src/host/integration/remote/remote-tool-backend.d.ts +108 -0
- package/lib/types/src/host/integration/remote-face.d.ts +127 -0
- package/lib/types/src/host/integration/remote-file-tools.d.ts +241 -0
- package/lib/types/src/host/integration/remote-home.d.ts +69 -0
- package/lib/types/src/host/integration/remote-index.d.ts +165 -0
- package/lib/types/src/host/integration/remote-job-tools.d.ts +295 -0
- package/lib/types/src/host/integration/remote-tool-backend.d.ts +97 -0
- package/lib/types/src/host/integration/remote-tools.d.ts +206 -0
- package/lib/types/src/host/integration/root-registry.d.ts +115 -0
- package/lib/types/src/host/integration/roots/anchor-directory.d.ts +56 -0
- package/lib/types/src/host/integration/roots/bin-root-resolver.d.ts +81 -0
- package/lib/types/src/host/integration/roots/remote-home.d.ts +69 -0
- package/lib/types/src/host/integration/roots/root-registry.d.ts +144 -0
- package/lib/types/src/host/integration/roots/roots-wiring.d.ts +219 -0
- package/lib/types/src/host/integration/roots/session-root-index.d.ts +52 -0
- package/lib/types/src/host/integration/roots-api.d.ts +407 -0
- package/lib/types/src/host/integration/roots-wiring.d.ts +211 -0
- package/lib/types/src/host/integration/session/session-api.d.ts +88 -0
- package/lib/types/src/host/integration/session/session-bin-lock.d.ts +80 -0
- package/lib/types/src/host/integration/session/session-header.d.ts +151 -0
- package/lib/types/src/host/integration/session/session-labels.d.ts +56 -0
- package/lib/types/src/host/integration/session/session-route-wiring.d.ts +50 -0
- package/lib/types/src/host/integration/session-admin-service.d.ts +236 -0
- package/lib/types/src/host/integration/session-api.d.ts +81 -0
- package/lib/types/src/host/integration/session-bin-lock.d.ts +80 -0
- package/lib/types/src/host/integration/session-header.d.ts +118 -0
- package/lib/types/src/host/integration/session-labels.d.ts +56 -0
- package/lib/types/src/host/integration/session-route-wiring.d.ts +50 -0
- package/lib/types/src/host/integration/session-scope.d.ts +113 -0
- package/lib/types/src/host/integration/session-tools.d.ts +44 -0
- package/lib/types/src/host/integration/strict-arguments.d.ts +79 -0
- package/lib/types/src/host/integration/takeover/takeover-api.d.ts +62 -0
- package/lib/types/src/host/integration/takeover/takeover-route-wiring.d.ts +60 -0
- package/lib/types/src/host/integration/takeover/takeover-service.d.ts +167 -0
- package/lib/types/src/host/integration/terminal-manifest.d.ts +94 -0
- package/lib/types/src/host/integration/tools/management-tools.d.ts +245 -0
- package/lib/types/src/host/integration/tools/remote-job-tools.d.ts +316 -0
- package/lib/types/src/host/integration/tools/remote-lsp-tools.d.ts +43 -0
- package/lib/types/src/host/integration/tools/remote-tools.d.ts +236 -0
- package/lib/types/src/host/integration/tools/session-target.d.ts +52 -0
- package/lib/types/src/host/integration/tools/session-tools.d.ts +46 -0
- package/lib/types/src/host/integration/tools/strict-arguments.d.ts +79 -0
- package/lib/types/src/host/integration/tools/terminal-manifest.d.ts +94 -0
- package/lib/types/src/host/integration/transfer/remote-copy.d.ts +66 -0
- package/lib/types/src/host/integration/transfer/transfer-adapters.d.ts +48 -0
- package/lib/types/src/host/integration/transfer/transfer-service.d.ts +212 -0
- package/lib/types/src/host/integration/transfer/transfer-tracker.d.ts +106 -0
- package/lib/types/src/host/integration/transfer-adapters.d.ts +44 -0
- package/lib/types/src/host/integration/transfer-service.d.ts +164 -0
- package/lib/types/src/host/integration/transfer-tracker.d.ts +106 -0
- package/lib/types/src/host/integration/user-roots.d.ts +461 -0
- package/lib/types/src/index.d.ts +66 -0
- package/lib/types/src/local-bridge-refs.d.ts +26 -0
- package/lib/types/src/local-bridge.d.ts +36 -0
- package/lib/types/src/shared/format.d.ts +13 -0
- package/lib/types/src/wiring/connections.d.ts +189 -0
- package/lib/types/src/wiring/fs.d.ts +13 -0
- package/lib/types/src/wiring/helpers.d.ts +144 -0
- package/lib/types/src/wiring/install-memo.d.ts +32 -0
- package/lib/types/src/wiring/legacy-dirs.d.ts +62 -0
- package/lib/types/src/wiring/package-paths.d.ts +35 -0
- package/lib/types/src/wiring/roots.d.ts +21 -0
- package/lib/types/src/wiring/state-dir.d.ts +29 -0
- package/lib/types/src/wiring/tools.d.ts +35 -0
- package/package.json +120 -4
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recycle bin: entry model, on-disk layout, retention policy and the undo stack — pure logic only.
|
|
3
|
+
*
|
|
4
|
+
* Why the domain layer (AGENTS.md 红线 2): entry ids, the bin path layout, the retention decision and the
|
|
5
|
+
* undo state machine are all decisions that can be verified offline in milliseconds. Every real filesystem
|
|
6
|
+
* or SSH interaction stays behind the injected seam of
|
|
7
|
+
* [`integration/recycle-bin-service.ts`](../integration/recycle-bin-service.ts).
|
|
8
|
+
*
|
|
9
|
+
* Contract notes:
|
|
10
|
+
*
|
|
11
|
+
* - **Deterministic ids.** The same inputs (root key, original path, deletion instant, delete ordinal for
|
|
12
|
+
* that path) always yield the same id. Two deletes of the same path at the same millisecond are told
|
|
13
|
+
* apart by `sequence`, and {@link allocateRecycleBinId} breaks genuine hash collisions deterministically.
|
|
14
|
+
* - **Layout.** `<binRoot>/<id>/<original basename>`. The id is unique, so same-named files never collide;
|
|
15
|
+
* keeping the original basename in the bin means a restore does not rename anything. Everything else a
|
|
16
|
+
* restore needs (root key, original absolute path) lives on the entry, which the service persists in an
|
|
17
|
+
* index file next to the bins.
|
|
18
|
+
* - **Restore conflicts.** When the original path is occupied, {@link planRestore} reports `blocked` plus an
|
|
19
|
+
* informational suffix candidate; the caller must **refuse**, never silently write to a different name.
|
|
20
|
+
* - **Retention.** `maxEntries` / `maxBytes` / `maxAgeMs` are applied per dimension: a **non-positive value
|
|
21
|
+
* means "no limit"** for that dimension (0 and negatives never wipe anything — deliberate, because a
|
|
22
|
+
* misconfigured 0 must not empty the bin). Oversized bins shed **oldest first**, with the id as a stable
|
|
23
|
+
* tie-break, so the same input always produces the same decision.
|
|
24
|
+
* - **Undo.** A stack. The inverse of `discard` is `restore` (and vice versa, which is what makes the undo
|
|
25
|
+
* redoable); `purge` is a physical delete and is therefore **explicitly irreversible**.
|
|
26
|
+
*
|
|
27
|
+
* @module dsh-over-ssh/domain/recycle-bin
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* Opaque identity of one recycle-bin entry.
|
|
31
|
+
*
|
|
32
|
+
* Branded on purpose: an id is the directory name inside the bin and a handle the UI holds across calls, so
|
|
33
|
+
* it must not be confused with a path or a session id.
|
|
34
|
+
*/
|
|
35
|
+
export type RecycleBinEntryId = string & {
|
|
36
|
+
readonly __recycleBinEntryId: 'RecycleBinEntryId';
|
|
37
|
+
};
|
|
38
|
+
/** Which world a target lives in; same vocabulary as `RootRef.location`. */
|
|
39
|
+
export type RecycleBinLocation = 'local' | 'remote';
|
|
40
|
+
/** Name of the index file inside a bin root. */
|
|
41
|
+
export declare const RECYCLE_BIN_INDEX_FILE = "index.json";
|
|
42
|
+
/** Version stamped into the index file; a different version is a hard parse failure. */
|
|
43
|
+
export declare const RECYCLE_BIN_INDEX_VERSION = 1;
|
|
44
|
+
/** Inputs that determine an entry id. */
|
|
45
|
+
export interface RecycleBinIdInput {
|
|
46
|
+
/** Stable root key (`rootKey()`); scopes ids to one root. */
|
|
47
|
+
readonly rootKey: string;
|
|
48
|
+
/** Absolute path of the deleted target, in this repository's normalized spelling. */
|
|
49
|
+
readonly originalPath: string;
|
|
50
|
+
/** Deletion instant in milliseconds since the epoch. */
|
|
51
|
+
readonly deletedAtMs: number;
|
|
52
|
+
/** Which delete of that same path this is, 1-based. */
|
|
53
|
+
readonly sequence: number;
|
|
54
|
+
}
|
|
55
|
+
/** One recycle-bin entry: everything a restore needs, plus the facts the bin view shows. */
|
|
56
|
+
export interface RecycleBinEntry {
|
|
57
|
+
readonly id: RecycleBinEntryId;
|
|
58
|
+
/** Root key (`rootKey()`) the target was deleted from. */
|
|
59
|
+
readonly rootKey: string;
|
|
60
|
+
readonly location: RecycleBinLocation;
|
|
61
|
+
/** Original absolute path, normalized (local: `/` separators; remote: POSIX). */
|
|
62
|
+
readonly originalPath: string;
|
|
63
|
+
/** Final segment of {@link originalPath}; the bin keeps this name. */
|
|
64
|
+
readonly originalName: string;
|
|
65
|
+
/** Bin root directory (`binRootFor(root)`). */
|
|
66
|
+
readonly binRoot: string;
|
|
67
|
+
/** Actual location inside the bin: `<binRoot>/<id>/<originalName>`. */
|
|
68
|
+
readonly binPath: string;
|
|
69
|
+
readonly deletedAtMs: number;
|
|
70
|
+
/** Bytes reported by the pre-delete `stat`; for directories this is the directory entry, not a sum. */
|
|
71
|
+
readonly sizeBytes: number;
|
|
72
|
+
readonly isDirectory: boolean;
|
|
73
|
+
/** 1-based ordinal of this delete among deletes of the same path. */
|
|
74
|
+
readonly sequence: number;
|
|
75
|
+
}
|
|
76
|
+
/** Input for {@link makeRecycleBinEntry}; {@link RecycleBinIdInput} plus the id and the observed facts. */
|
|
77
|
+
export interface MakeRecycleBinEntryInput extends RecycleBinIdInput {
|
|
78
|
+
readonly id: RecycleBinEntryId;
|
|
79
|
+
readonly location: RecycleBinLocation;
|
|
80
|
+
readonly binRoot: string;
|
|
81
|
+
readonly sizeBytes: number;
|
|
82
|
+
readonly isDirectory: boolean;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Normalize a deletion target into the spelling the rest of the repository uses.
|
|
86
|
+
*
|
|
87
|
+
* Local targets go through `normalizeLocalPath` (separators to `/`, case preserved), remote targets through
|
|
88
|
+
* `normalizeRemotePath` (POSIX, absolute). A filesystem root is rejected because it can never be a target.
|
|
89
|
+
*
|
|
90
|
+
* @param path - target path in either separator spelling.
|
|
91
|
+
* @param location - which world the path belongs to.
|
|
92
|
+
* @returns the normalized absolute path.
|
|
93
|
+
* @throws SshDevError `ROOT_INVALID` when the path is empty, relative, or a filesystem root.
|
|
94
|
+
*/
|
|
95
|
+
export declare function normalizeRecycleTarget(path: string, location: RecycleBinLocation): string;
|
|
96
|
+
/**
|
|
97
|
+
* The deterministic id for one {@link RecycleBinIdInput}.
|
|
98
|
+
*
|
|
99
|
+
* Same input → same id (a test can therefore pin ids). Different inputs collide only if FNV-1a collides,
|
|
100
|
+
* which {@link allocateRecycleBinId} handles.
|
|
101
|
+
*
|
|
102
|
+
* @param input - root key, original path, deletion instant and delete ordinal.
|
|
103
|
+
* @returns a filesystem-safe id of the form `rb-<8 hex>`.
|
|
104
|
+
*/
|
|
105
|
+
export declare function makeRecycleBinEntryId(input: RecycleBinIdInput): RecycleBinEntryId;
|
|
106
|
+
/**
|
|
107
|
+
* Allocate an id that is not already taken by `existing`.
|
|
108
|
+
*
|
|
109
|
+
* The first candidate is {@link makeRecycleBinEntryId}; on collision the suffix `-2`, `-3`, … is appended.
|
|
110
|
+
* The loop is bounded by the number of live entries, because every candidate it rejects is one of them.
|
|
111
|
+
*
|
|
112
|
+
* @param existing - ids already present in the bin index.
|
|
113
|
+
* @param input - id material.
|
|
114
|
+
* @returns an id not present in `existing`.
|
|
115
|
+
*/
|
|
116
|
+
export declare function allocateRecycleBinId(existing: Iterable<string>, input: RecycleBinIdInput): RecycleBinEntryId;
|
|
117
|
+
/**
|
|
118
|
+
* Build one entry, validating its inputs and computing the bin layout.
|
|
119
|
+
*
|
|
120
|
+
* @param input - id, root, worlds, observed `stat` facts.
|
|
121
|
+
* @returns the entry, with `binPath` = `<binRoot>/<id>/<originalName>`.
|
|
122
|
+
* @throws SshDevError `ROOT_INVALID` when a path, the bin root, or a numeric field is unusable.
|
|
123
|
+
*/
|
|
124
|
+
export declare function makeRecycleBinEntry(input: MakeRecycleBinEntryInput): RecycleBinEntry;
|
|
125
|
+
/**
|
|
126
|
+
* Join segments under an absolute base with `/`.
|
|
127
|
+
*
|
|
128
|
+
* Both worlds use this spelling (`local` roots are stored normalized by `normalizeLocalPath`, remote ones are
|
|
129
|
+
* POSIX), so the bin layout is spelled the same way everywhere. A drive-rooted base (`D:`) keeps its slash.
|
|
130
|
+
*
|
|
131
|
+
* @param base - absolute base directory.
|
|
132
|
+
* @param segments - segments to append.
|
|
133
|
+
* @returns the joined path.
|
|
134
|
+
*/
|
|
135
|
+
export declare function joinUnderPath(base: string, ...segments: readonly string[]): string;
|
|
136
|
+
/** A restore decision: either the move is ready, or the original path is occupied and must not be reused. */
|
|
137
|
+
export type RestorePlan = {
|
|
138
|
+
readonly kind: 'ready';
|
|
139
|
+
readonly from: string;
|
|
140
|
+
readonly to: string;
|
|
141
|
+
} | {
|
|
142
|
+
readonly kind: 'blocked';
|
|
143
|
+
readonly to: string;
|
|
144
|
+
readonly reason: 'occupied';
|
|
145
|
+
readonly suggestedPath: string;
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* Decide how to restore one entry.
|
|
149
|
+
*
|
|
150
|
+
* Policy: **an occupied original path is refused, never renamed silently.** `suggestedPath` is returned so a
|
|
151
|
+
* UI can *offer* an alternative, but no code path in this module moves a target to it.
|
|
152
|
+
*
|
|
153
|
+
* @param entry - entry to restore.
|
|
154
|
+
* @param options - `occupied` is the caller's `stat` of `entry.originalPath`.
|
|
155
|
+
* @returns the plan.
|
|
156
|
+
*/
|
|
157
|
+
export declare function planRestore(entry: RecycleBinEntry, options: {
|
|
158
|
+
readonly occupied: boolean;
|
|
159
|
+
}): RestorePlan;
|
|
160
|
+
/** Retention policy. A non-positive value means "no limit" for that dimension. */
|
|
161
|
+
export interface RecycleBinPolicy {
|
|
162
|
+
/** Keep at most this many entries; non-positive = unlimited. */
|
|
163
|
+
readonly maxEntries: number;
|
|
164
|
+
/** Keep at most this many bytes (sum of entry sizes); non-positive = unlimited. */
|
|
165
|
+
readonly maxBytes: number;
|
|
166
|
+
/** Keep entries at most this old, in milliseconds; non-positive = unlimited. */
|
|
167
|
+
readonly maxAgeMs: number;
|
|
168
|
+
}
|
|
169
|
+
/** Neutral element: no dimension is limited, therefore nothing is ever swept with it. */
|
|
170
|
+
export declare const NO_RECYCLE_BIN_LIMITS: RecycleBinPolicy;
|
|
171
|
+
/** Which dimension caused one entry to be swept. */
|
|
172
|
+
export type SweepDimension = 'age' | 'count' | 'bytes';
|
|
173
|
+
/** Why one entry is scheduled for physical deletion. */
|
|
174
|
+
export interface SweepReason {
|
|
175
|
+
readonly id: RecycleBinEntryId;
|
|
176
|
+
readonly dimension: SweepDimension;
|
|
177
|
+
readonly detail: string;
|
|
178
|
+
}
|
|
179
|
+
/** The pure retention decision for one bin. */
|
|
180
|
+
export interface SweepDecision {
|
|
181
|
+
/** Ids to delete physically, oldest first. */
|
|
182
|
+
readonly purge: readonly RecycleBinEntryId[];
|
|
183
|
+
/** Ids to keep, oldest first. */
|
|
184
|
+
readonly keep: readonly RecycleBinEntryId[];
|
|
185
|
+
/** One reason per purged id, in the same order. */
|
|
186
|
+
readonly reasons: readonly SweepReason[];
|
|
187
|
+
/** Entries and bytes left after applying the decision. */
|
|
188
|
+
readonly remainingEntries: number;
|
|
189
|
+
readonly remainingBytes: number;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Apply a retention policy to one bin.
|
|
193
|
+
*
|
|
194
|
+
* Rules, in this order (all of them deterministic: entries are ordered oldest first with the id as tie-break):
|
|
195
|
+
*
|
|
196
|
+
* 1. `maxAgeMs > 0`: purge everything older than the limit. An entry exactly **at** the limit is kept, and a
|
|
197
|
+
* negative age (clock skew, future timestamp) is never purged.
|
|
198
|
+
* 2. `maxBytes > 0`: walk the survivors oldest first and purge until the remaining sum is within the limit.
|
|
199
|
+
* A single entry larger than the whole budget is purged too — otherwise the budget would be meaningless.
|
|
200
|
+
* 3. `maxEntries > 0`: purge the oldest survivors until the count is within the limit.
|
|
201
|
+
*
|
|
202
|
+
* @param entries - entries of one bin.
|
|
203
|
+
* @param policy - limits; non-positive values are ignored.
|
|
204
|
+
* @param nowMs - current instant in milliseconds since the epoch.
|
|
205
|
+
* @returns the decision.
|
|
206
|
+
*/
|
|
207
|
+
export declare function decideSweep(entries: readonly RecycleBinEntry[], policy: RecycleBinPolicy, nowMs: number): SweepDecision;
|
|
208
|
+
/** What one committed operation did; the unit of the undo stack. */
|
|
209
|
+
export type OperationKind = 'discard' | 'restore' | 'purge';
|
|
210
|
+
/** One committed operation. */
|
|
211
|
+
export interface RecycleBinOperation {
|
|
212
|
+
readonly kind: OperationKind;
|
|
213
|
+
/** Entry the operation acted on; `undefined` for a permanent delete, which has no bin entry. */
|
|
214
|
+
readonly entry: RecycleBinEntry | undefined;
|
|
215
|
+
/** Entry id, or a synthesized id for a permanent delete. */
|
|
216
|
+
readonly id: RecycleBinEntryId;
|
|
217
|
+
readonly atMs: number;
|
|
218
|
+
}
|
|
219
|
+
/** Stack-shaped undo state: `undo` is the history, `redo` what was undone and can be re-applied. */
|
|
220
|
+
export interface UndoStack {
|
|
221
|
+
readonly undo: readonly RecycleBinOperation[];
|
|
222
|
+
readonly redo: readonly RecycleBinOperation[];
|
|
223
|
+
/** Operations dropped for exceeding the depth limit; explains "too old to undo". */
|
|
224
|
+
readonly dropped: number;
|
|
225
|
+
}
|
|
226
|
+
/** Fresh state with no history. */
|
|
227
|
+
export declare const EMPTY_UNDO_STACK: UndoStack;
|
|
228
|
+
/** Why an undo/redo step was refused. */
|
|
229
|
+
export type UndoRejectionReason = 'nothing-to-undo' | 'nothing-to-redo' | 'entry-missing' | 'irreversible';
|
|
230
|
+
/** The world state an undo decision is validated against. */
|
|
231
|
+
export interface UndoContext {
|
|
232
|
+
/**
|
|
233
|
+
* Ids **currently present in the recycle bin**.
|
|
234
|
+
*
|
|
235
|
+
* One rule covers both directions: a `restore` action needs the bin copy (the id must be listed), and a
|
|
236
|
+
* `discard` action needs the file to be back at its original path (the id must not be listed).
|
|
237
|
+
*/
|
|
238
|
+
readonly liveIds: readonly RecycleBinEntryId[];
|
|
239
|
+
}
|
|
240
|
+
/** A step to execute: move the entry out of the bin, or back into it. */
|
|
241
|
+
export interface UndoAction {
|
|
242
|
+
readonly kind: 'restore' | 'discard';
|
|
243
|
+
readonly operation: RecycleBinOperation;
|
|
244
|
+
readonly entry: RecycleBinEntry;
|
|
245
|
+
}
|
|
246
|
+
/** Why a step is not executable. */
|
|
247
|
+
export interface UndoRejection {
|
|
248
|
+
readonly reason: UndoRejectionReason;
|
|
249
|
+
readonly message: string;
|
|
250
|
+
readonly operation: RecycleBinOperation | undefined;
|
|
251
|
+
}
|
|
252
|
+
/** Outcome of one undo/redo step: either an action to run, or a rejection. */
|
|
253
|
+
export interface UndoOutcome {
|
|
254
|
+
/** State after the step; a rejected record is dropped from the stack (it can never succeed). */
|
|
255
|
+
readonly stack: UndoStack;
|
|
256
|
+
readonly action: UndoAction | undefined;
|
|
257
|
+
readonly rejection: UndoRejection | undefined;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Record one committed operation.
|
|
261
|
+
*
|
|
262
|
+
* The redo half is cleared (a new operation starts a new branch of history), the record goes on top of the
|
|
263
|
+
* undo half, and anything beyond `undoDepth` is dropped. A non-positive `undoDepth` keeps no history at all.
|
|
264
|
+
*
|
|
265
|
+
* @param stack - current state.
|
|
266
|
+
* @param operation - the operation that just succeeded.
|
|
267
|
+
* @param options - `undoDepth` from configuration.
|
|
268
|
+
* @returns the new state.
|
|
269
|
+
*/
|
|
270
|
+
export declare function recordOperation(stack: UndoStack, operation: RecycleBinOperation, options: {
|
|
271
|
+
readonly undoDepth: number;
|
|
272
|
+
}): UndoStack;
|
|
273
|
+
/** The operation {@link takeUndo} would consume, or `undefined` for an empty history. */
|
|
274
|
+
export declare function nextUndo(stack: UndoStack): RecycleBinOperation | undefined;
|
|
275
|
+
/** The operation that can be re-applied, or `undefined` when nothing was undone. */
|
|
276
|
+
export declare function nextRedo(stack: UndoStack): RecycleBinOperation | undefined;
|
|
277
|
+
/**
|
|
278
|
+
* Consume the newest operation as an **inverse** step.
|
|
279
|
+
*
|
|
280
|
+
* `discard` reverses to `restore`; `restore` reverses to `discard`; `purge` is refused as `irreversible`
|
|
281
|
+
* (a physical delete cannot be undone, and the record is dropped instead of being offered again).
|
|
282
|
+
*
|
|
283
|
+
* @param stack - current state.
|
|
284
|
+
* @param context - which ids are actually in the bin right now.
|
|
285
|
+
* @returns the action to run plus the new state, or a rejection plus the state with the dead record dropped.
|
|
286
|
+
*/
|
|
287
|
+
export declare function takeUndo(stack: UndoStack, context: UndoContext): UndoOutcome;
|
|
288
|
+
/**
|
|
289
|
+
* Re-apply the newest undone operation.
|
|
290
|
+
*
|
|
291
|
+
* This is the exact inverse of {@link takeUndo}: it consumes the redo half and pushes the same operation back
|
|
292
|
+
* onto the undo half.
|
|
293
|
+
*
|
|
294
|
+
* @param stack - current state.
|
|
295
|
+
* @param context - which ids are actually in the bin right now.
|
|
296
|
+
* @returns the action to run plus the new state, or a rejection.
|
|
297
|
+
*/
|
|
298
|
+
export declare function takeRedo(stack: UndoStack, context: UndoContext): UndoOutcome;
|
|
299
|
+
/** The persisted index document. */
|
|
300
|
+
export interface RecycleBinIndex {
|
|
301
|
+
readonly version: number;
|
|
302
|
+
readonly entries: readonly RecycleBinEntry[];
|
|
303
|
+
}
|
|
304
|
+
/** One record the parser refused, with the reason (a corrupt index must never silently lose data). */
|
|
305
|
+
export interface SkippedIndexRecord {
|
|
306
|
+
readonly index: number;
|
|
307
|
+
readonly reason: string;
|
|
308
|
+
}
|
|
309
|
+
/** Result of {@link parseRecycleBinIndex}. */
|
|
310
|
+
export interface ParsedRecycleBinIndex {
|
|
311
|
+
readonly entries: readonly RecycleBinEntry[];
|
|
312
|
+
readonly skipped: readonly SkippedIndexRecord[];
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Serialize the index.
|
|
316
|
+
*
|
|
317
|
+
* Entries are sorted by id so the same set always produces byte-identical text (easy to diff, easy to test).
|
|
318
|
+
* The result is a plain JavaScript string: written as UTF-8 it carries **no BOM**, and {@link parseRecycleBinIndex}
|
|
319
|
+
* tolerates one anyway because remote tooling and editors add them.
|
|
320
|
+
*
|
|
321
|
+
* @param entries - entries to persist.
|
|
322
|
+
* @returns JSON text.
|
|
323
|
+
*/
|
|
324
|
+
export declare function serializeRecycleBinIndex(entries: readonly RecycleBinEntry[]): string;
|
|
325
|
+
/**
|
|
326
|
+
* Parse the index.
|
|
327
|
+
*
|
|
328
|
+
* A malformed document or an unknown version is a hard failure (the bin contents cannot be trusted); a single
|
|
329
|
+
* unusable record is skipped and reported. Stored `binPath` values are taken as authoritative and not recomputed,
|
|
330
|
+
* so changing the `binRootFor` configuration later does not invalidate existing entries.
|
|
331
|
+
*
|
|
332
|
+
* @param text - index file content.
|
|
333
|
+
* @returns the usable entries plus the skipped records.
|
|
334
|
+
* @throws SshDevError `TRASH_NOT_FOUND` when the document cannot be read as an index.
|
|
335
|
+
*/
|
|
336
|
+
export declare function parseRecycleBinIndex(text: string): ParsedRecycleBinIndex;
|
|
337
|
+
//# sourceMappingURL=recycle-bin.d.ts.map
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure transfer arithmetic: chunk plans, progress maths, root-confined remote targets and file-name
|
|
3
|
+
* validation.
|
|
4
|
+
*
|
|
5
|
+
* Kept in the domain layer on purpose (AGENTS.md 红线 2): no `@deepseek-ai/*` import, no process, no
|
|
6
|
+
* socket and no clock — every number it needs is a parameter. That is what lets the whole transfer
|
|
7
|
+
* policy (offsets, percentages, resume decisions, `..` folding rules) be tested offline in
|
|
8
|
+
* milliseconds, and it is why the seam in `integration/transfer-service.ts` only has to move bytes.
|
|
9
|
+
*
|
|
10
|
+
* Three behaviours are deliberately *deterministic* rather than "reasonable":
|
|
11
|
+
*
|
|
12
|
+
* - **A zero-byte file has zero chunks** but still needs one empty write to create the target
|
|
13
|
+
* (`needsEmptyWrite`), so "upload an empty file" is a real operation rather than a no-op.
|
|
14
|
+
* - **A plan is bounded**: the chunk list is materialised, so `MAX_TRANSFER_CHUNKS` caps it and an
|
|
15
|
+
* absurd `chunkBytes` fails loudly instead of exhausting memory.
|
|
16
|
+
* - **`..` never escapes the root**: the spelling is folded the way `joinUnderRoot` folds it, and the
|
|
17
|
+
* result is re-checked against the root, so a future change to the folder cannot silently open a
|
|
18
|
+
* hole (see {@link assertInsideRoot}).
|
|
19
|
+
*
|
|
20
|
+
* @module dsh-over-ssh/domain/transfer
|
|
21
|
+
*/
|
|
22
|
+
import { formatBytes, formatDuration } from '../../../shared/format.ts';
|
|
23
|
+
import { SshDevError, type SshDevErrorCode } from '../errors.ts';
|
|
24
|
+
/**
|
|
25
|
+
* Upper bound on the number of chunks one plan may contain.
|
|
26
|
+
*
|
|
27
|
+
* This is **not** a deployment knob — the deployment knob is `chunkBytes`. It exists because
|
|
28
|
+
* {@link planTransfer} materialises the chunk list, so a tiny `chunkBytes` on a huge file would
|
|
29
|
+
* otherwise allocate until the process dies.
|
|
30
|
+
*/
|
|
31
|
+
export declare const MAX_TRANSFER_CHUNKS = 100000;
|
|
32
|
+
/** One slice of a transfer: bytes `[offset, offset + length)` of the source. */
|
|
33
|
+
export interface TransferChunk {
|
|
34
|
+
/** 0-based position of this chunk inside the plan it was produced by. */
|
|
35
|
+
readonly index: number;
|
|
36
|
+
/** Byte offset in the source. */
|
|
37
|
+
readonly offset: number;
|
|
38
|
+
/** Number of bytes to move; always ≥ 1. */
|
|
39
|
+
readonly length: number;
|
|
40
|
+
}
|
|
41
|
+
/** A complete, finite plan for moving `totalBytes`. */
|
|
42
|
+
export interface TransferPlan {
|
|
43
|
+
readonly totalBytes: number;
|
|
44
|
+
readonly chunkBytes: number;
|
|
45
|
+
/** Chunks in increasing offset order; empty for a zero-byte source. */
|
|
46
|
+
readonly chunks: readonly TransferChunk[];
|
|
47
|
+
/** Whether the target still has to be created with one write of zero bytes. */
|
|
48
|
+
readonly needsEmptyWrite: boolean;
|
|
49
|
+
}
|
|
50
|
+
/** Why a transfer could not do something the caller asked for, or could not report a digest. */
|
|
51
|
+
export interface TransferDegradation {
|
|
52
|
+
/** Stable code, so a caller can branch without reading the hint. */
|
|
53
|
+
readonly code: 'resume-unsupported' | 'resume-unusable' | 'resume-mismatch' | 'resume-unverified' | 'digest-unavailable';
|
|
54
|
+
/** Actionable explanation for the user. */
|
|
55
|
+
readonly hint: string;
|
|
56
|
+
}
|
|
57
|
+
/** What to do about a possibly-partial target before starting. */
|
|
58
|
+
export interface ResumeDecision {
|
|
59
|
+
/** Byte offset the transfer starts at. */
|
|
60
|
+
readonly startOffset: number;
|
|
61
|
+
/** Whether bytes already present on the target are being kept. */
|
|
62
|
+
readonly resumed: boolean;
|
|
63
|
+
/** Reasons the transfer is not resuming; empty when it resumes or was never asked to. */
|
|
64
|
+
readonly degradations: readonly TransferDegradation[];
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Cut `totalBytes` into contiguous chunks of at most `chunkBytes`.
|
|
68
|
+
*
|
|
69
|
+
* @param totalBytes - source length; 0 produces an empty plan plus `needsEmptyWrite`.
|
|
70
|
+
* @param chunkBytes - per-chunk cap; must be a safe integer ≥ 1.
|
|
71
|
+
* @returns the plan, with offsets starting at 0 and covering exactly `totalBytes`.
|
|
72
|
+
* @throws SshDevError `ROOT_INVALID` when either argument is not a usable integer, or when the plan
|
|
73
|
+
* would exceed {@link MAX_TRANSFER_CHUNKS}.
|
|
74
|
+
*/
|
|
75
|
+
export declare function planTransfer(totalBytes: number, chunkBytes: number): TransferPlan;
|
|
76
|
+
/**
|
|
77
|
+
* The chunks still to move when the target already holds `startOffset` bytes.
|
|
78
|
+
*
|
|
79
|
+
* The first returned chunk is trimmed to start exactly at `startOffset`, and indices are renumbered
|
|
80
|
+
* from 0 because the caller reports "how many writes did *this* transfer make".
|
|
81
|
+
*
|
|
82
|
+
* @param plan - the full plan.
|
|
83
|
+
* @param startOffset - bytes already present; ≤ 0 means "nothing", ≥ `totalBytes` means "nothing left".
|
|
84
|
+
* @returns the remaining chunks; the input plan is not mutated.
|
|
85
|
+
*/
|
|
86
|
+
export declare function remainingChunks(plan: TransferPlan, startOffset: number): readonly TransferChunk[];
|
|
87
|
+
/**
|
|
88
|
+
* Decide where a transfer starts, given what the target already holds.
|
|
89
|
+
*
|
|
90
|
+
* Precedence (each rule is a test): no resume requested → 0; zero-byte source → 0; the injected
|
|
91
|
+
* implementation cannot resume → 0 with `resume-unsupported`; nothing usable on the target → 0;
|
|
92
|
+
* target is at least as long as the source → 0 with `resume-unusable` (a same-size target is **never**
|
|
93
|
+
* assumed complete, because nothing here can verify its content); otherwise the target's length.
|
|
94
|
+
*
|
|
95
|
+
* @param input - source length, target length (`undefined` when absent/unknown) and the two flags.
|
|
96
|
+
* @returns the decision, including why resume did not happen.
|
|
97
|
+
*/
|
|
98
|
+
export declare function decideStartOffset(input: {
|
|
99
|
+
readonly totalBytes: number;
|
|
100
|
+
readonly partialBytes: number | undefined;
|
|
101
|
+
readonly resumeRequested: boolean;
|
|
102
|
+
readonly resumeSupported: boolean;
|
|
103
|
+
}): ResumeDecision;
|
|
104
|
+
/** Progress of one transfer at one instant. */
|
|
105
|
+
export interface TransferProgress {
|
|
106
|
+
readonly transferredBytes: number;
|
|
107
|
+
readonly totalBytes: number;
|
|
108
|
+
/** 0–100, rounded to one decimal; a zero-byte transfer is 100 (nothing left to do). */
|
|
109
|
+
readonly percent: number;
|
|
110
|
+
/** Average rate since the transfer started; 0 when no time has passed. */
|
|
111
|
+
readonly bytesPerSecond: number;
|
|
112
|
+
readonly elapsedMs: number;
|
|
113
|
+
/** Estimated time left; `null` when no rate is known yet, 0 when finished. */
|
|
114
|
+
readonly remainingMs: number | null;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Compute progress from the two byte counts and elapsed time.
|
|
118
|
+
*
|
|
119
|
+
* Pure and monotonic: for a fixed `totalBytes` and a non-decreasing `transferredBytes`, every field
|
|
120
|
+
* is non-decreasing.
|
|
121
|
+
*
|
|
122
|
+
* @param input - bytes moved, bytes expected and milliseconds since the start.
|
|
123
|
+
* @returns the progress snapshot.
|
|
124
|
+
*/
|
|
125
|
+
export declare function progressOf(input: {
|
|
126
|
+
readonly transferredBytes: number;
|
|
127
|
+
readonly totalBytes: number;
|
|
128
|
+
readonly elapsedMs: number;
|
|
129
|
+
}): TransferProgress;
|
|
130
|
+
/** Binary byte units, largest last. */
|
|
131
|
+
/**
|
|
132
|
+
* One-line progress text for the UI or a CLI.
|
|
133
|
+
*
|
|
134
|
+
* @param progress - a snapshot from {@link progressOf}.
|
|
135
|
+
* @param options - `label` names the direction (`上传`/`下载`); defaults to `传输`.
|
|
136
|
+
* @returns e.g. `上传 40.0%(4 B/10 B,2 B/s,剩余约 3.0s)`, or `…,耗时 350ms)` once finished.
|
|
137
|
+
*/
|
|
138
|
+
export declare function formatProgress(progress: TransferProgress, options?: {
|
|
139
|
+
readonly label?: string;
|
|
140
|
+
}): string;
|
|
141
|
+
/** A remote target resolved inside one root. */
|
|
142
|
+
export interface RemoteTarget {
|
|
143
|
+
/** Normalized root the target lives in. */
|
|
144
|
+
readonly root: string;
|
|
145
|
+
/** Root-absolute POSIX path of the target. */
|
|
146
|
+
readonly absolute: string;
|
|
147
|
+
/** The spelling requested, with `.`/`..` folded (may differ from `absolute`). */
|
|
148
|
+
readonly relative: string;
|
|
149
|
+
/** Final path segment. */
|
|
150
|
+
readonly name: string;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Fold `root + requested` into a root-absolute path that cannot leave the root.
|
|
154
|
+
*
|
|
155
|
+
* The folding rule is exactly `joinUnderRoot`'s (imported, not re-implemented): a leading `/` is
|
|
156
|
+
* treated as root-relative, `.` is dropped, and `..` pops within the *requested* part only — extra
|
|
157
|
+
* `..` at the top are ignored rather than climbing out of the root. The result is re-checked with
|
|
158
|
+
* {@link assertInsideRoot} so a future change to the folder cannot silently open a hole.
|
|
159
|
+
*
|
|
160
|
+
* @param root - absolute POSIX root directory.
|
|
161
|
+
* @param requested - remote spelling relative to the root (absolute spellings are root-relative too).
|
|
162
|
+
* @returns the resolved target.
|
|
163
|
+
* @throws SshDevError `ROOT_INVALID` for an unusable root/request, `ROOT_OUTSIDE` when the request
|
|
164
|
+
* names the root itself, ends with `/` (a directory spelling) or somehow escapes the root.
|
|
165
|
+
*/
|
|
166
|
+
export declare function resolveRemoteTarget(root: string, requested: string): RemoteTarget;
|
|
167
|
+
/**
|
|
168
|
+
* Check that a root-absolute path really is inside its root.
|
|
169
|
+
*
|
|
170
|
+
* @param root - absolute POSIX root directory.
|
|
171
|
+
* @param absolute - root-absolute candidate.
|
|
172
|
+
* @throws SshDevError `ROOT_OUTSIDE` when the candidate is not the root or below it.
|
|
173
|
+
*/
|
|
174
|
+
export declare function assertInsideRoot(root: string, absolute: string): void;
|
|
175
|
+
/**
|
|
176
|
+
* Validate one remote file name (a single path segment).
|
|
177
|
+
*
|
|
178
|
+
* @param name - the segment to check.
|
|
179
|
+
* @returns the name, unchanged.
|
|
180
|
+
* @throws SshDevError `ROOT_INVALID` for an empty/blank name, one containing `/` or NUL;
|
|
181
|
+
* `ROOT_OUTSIDE` for `.`/`..`, which name a directory rather than a file.
|
|
182
|
+
*/
|
|
183
|
+
export declare function validateRemoteFileName(name: string): string;
|
|
184
|
+
/**
|
|
185
|
+
* Validate one local file name for the platform the download will land on.
|
|
186
|
+
*
|
|
187
|
+
* The platform is a parameter rather than `process.platform` so the Windows rules stay testable on
|
|
188
|
+
* any host (see [`run-tests.md`] on platform-dependent assertions).
|
|
189
|
+
*
|
|
190
|
+
* @param name - the segment to check (a base name, never a path).
|
|
191
|
+
* @param options - `platform` selects the rule set; defaults to the running platform.
|
|
192
|
+
* @returns the name, unchanged.
|
|
193
|
+
* @throws SshDevError `ROOT_INVALID` for an empty/blank name, a name containing a path separator or
|
|
194
|
+
* (on win32) a forbidden character, a reserved device name or a trailing dot/space;
|
|
195
|
+
* `ROOT_OUTSIDE` for `.`/`..`.
|
|
196
|
+
*/
|
|
197
|
+
export declare function validateLocalFileName(name: string, options?: {
|
|
198
|
+
readonly platform?: 'win32' | 'posix';
|
|
199
|
+
}): string;
|
|
200
|
+
/**
|
|
201
|
+
* The final segment of a local path, in either separator spelling.
|
|
202
|
+
*
|
|
203
|
+
* @param path - host path.
|
|
204
|
+
* @returns the base name; empty when the path has no segment.
|
|
205
|
+
*/
|
|
206
|
+
export declare function localFileBasename(path: string): string;
|
|
207
|
+
/**
|
|
208
|
+
* What went wrong in a transfer, precisely.
|
|
209
|
+
*
|
|
210
|
+
* This is **not** an error code (the code table lives in `domain/errors.ts` and is unchanged by this
|
|
211
|
+
* module). It is the `details.kind` of the thrown {@link SshDevError}, the same shape
|
|
212
|
+
* `sshFailureToError` uses to keep "which precise cause" separate from "which stable code".
|
|
213
|
+
*/
|
|
214
|
+
export type TransferFailureKind =
|
|
215
|
+
/** Arguments or spelling are unusable (empty root, empty name, bad chunk cap). */
|
|
216
|
+
'invalid-request'
|
|
217
|
+
/** The target names a directory, the root itself, or leaves the root. */
|
|
218
|
+
| 'outside-root' | 'local-stat-failed' | 'local-read-failed' | 'local-write-failed' | 'remote-stat-failed' | 'remote-not-found' | 'remote-read-failed' | 'remote-short-read' | 'remote-write-failed'
|
|
219
|
+
/** The source changed size while it was being read. */
|
|
220
|
+
| 'source-changed' | 'verify-failed' | 'cancelled';
|
|
221
|
+
/** Every {@link TransferFailureKind}, for exhaustive checks and tests. */
|
|
222
|
+
export declare const TRANSFER_FAILURE_KINDS: readonly ["invalid-request", "outside-root", "local-stat-failed", "local-read-failed", "local-write-failed", "remote-stat-failed", "remote-not-found", "remote-read-failed", "remote-short-read", "remote-write-failed", "source-changed", "verify-failed", "cancelled"];
|
|
223
|
+
/**
|
|
224
|
+
* Map a precise transfer failure onto one of the **existing** plugin error codes.
|
|
225
|
+
*
|
|
226
|
+
* No code is invented here (see the audit): a local I/O failure has no dedicated code in
|
|
227
|
+
* `SSH_DEV_ERROR_CODES`, so it maps to `ROOT_INVALID` ("this path is unusable") and stays
|
|
228
|
+
* distinguishable through `details.kind`.
|
|
229
|
+
*
|
|
230
|
+
* @param kind - the precise failure.
|
|
231
|
+
* @returns the stable code a caller can branch on.
|
|
232
|
+
*/
|
|
233
|
+
export declare function failureCodeFor(kind: TransferFailureKind): SshDevErrorCode;
|
|
234
|
+
/**
|
|
235
|
+
* Build the plugin failure for one transfer step.
|
|
236
|
+
*
|
|
237
|
+
* @param kind - precise failure kind, recorded as `details.kind`.
|
|
238
|
+
* @param message - actionable Chinese message (what failed and what to do).
|
|
239
|
+
* @param details - structured context; must not contain secrets.
|
|
240
|
+
* @returns an {@link SshDevError} carrying the mapped code.
|
|
241
|
+
*/
|
|
242
|
+
export declare function transferFailure(kind: TransferFailureKind, message: string, details?: Readonly<Record<string, unknown>>): SshDevError;
|
|
243
|
+
/**
|
|
244
|
+
* Test whether a value is a transfer failure (optionally of one precise kind).
|
|
245
|
+
*
|
|
246
|
+
* @param error - candidate.
|
|
247
|
+
* @param kind - when given, also require `details.kind` to equal it.
|
|
248
|
+
* @returns whether the value is a plugin error produced by this module.
|
|
249
|
+
*/
|
|
250
|
+
export declare function isTransferFailure(error: unknown, kind?: TransferFailureKind): boolean;
|
|
251
|
+
/**
|
|
252
|
+
* Compare two digests tolerantly (hex digests arrive with mixed case and stray whitespace).
|
|
253
|
+
*
|
|
254
|
+
* @param left - one digest, or `undefined`.
|
|
255
|
+
* @param right - the other digest, or `undefined`.
|
|
256
|
+
* @returns whether both are present and equal ignoring case and surrounding whitespace.
|
|
257
|
+
*/
|
|
258
|
+
export declare function digestsMatch(left: string | undefined, right: string | undefined): boolean;
|
|
259
|
+
export { formatBytes, formatDuration };
|
|
260
|
+
//# sourceMappingURL=transfer.d.ts.map
|