Files
ssh-manager/docs/superpowers/specs/2026-03-23-remote-to-many-multi-file-design.md
liumangmang c8fa3de679 docs: 添加 superpowers 规格与计划文档
记录 Remote->Many 多文件与相关实现计划/设计,便于后续追踪与复盘。
2026-03-24 13:43:08 +08:00

8.8 KiB

Remote -> Many: Multi-File Source Selection (Files Only)

Date: 2026-03-23 Status: Draft

Context

Transfers page currently supports two modes:

  • Local -> Many: user selects local files (supports multiple via <input type="file" multiple>), then uploads to many target connections.
  • Remote -> Many: user provides a single remote sourcePath on a source connection, then transfers that one file to many target connections.

Current remote transfer backend only supports single-file transfer. If sourcePath is a directory, backend throws an error ("only single file transfer is supported").

Goal

Enable Remote -> Many to select and transfer multiple source files in one run.

Scope constraints:

  • Files only (no directories).
  • Keep existing backend APIs unchanged.
  • Preserve current concurrency semantics and progress UI style.

Non-Goals

  • Directory transfer (recursive) or directory selection.
  • Server-side batch API (single request for multiple source paths).
  • Change Local -> Many behavior.

Current Implementation (Relevant)

  • UI: frontend/src/views/TransfersView.vue

    • Remote -> Many uses remoteSourceConnectionId + remoteSourcePath + remoteTargetDirOrPath.
    • Uses SftpFilePickerModal to pick a single remote path.
  • Store: frontend/src/stores/transfers.ts

    • startRemoteToMany({ sourceConnectionId, sourcePath, targetConnectionIds, targetDirOrPath, concurrency })
    • For each target connection creates a backend task via createRemoteTransferTask(...) and waits via SSE (subscribeRemoteTransferProgress).
  • Backend: backend/src/main/java/com/sshmanager/controller/SftpController.java

    • /sftp/transfer-remote/tasks creates a single transfer task.
  • Backend service: backend/src/main/java/com/sshmanager/service/SftpService.java

    • transferRemote(...) validates sourcePath is not a directory.

Front-end only changes:

  1. Remote -> Many supports selecting multiple sourcePath values.
  2. For each (sourcePath, targetConnectionId) pair, create one backend transfer task using existing /sftp/transfer-remote/tasks.
  3. Reuse current concurrency limiter (runWithConcurrency) across all items.

This is consistent with how Local -> Many expands files x targets into a queue of transfer items.

UX / UI Changes

TransfersView Remote -> Many

  • Replace single path field with a multi-selection model:

    • From: remoteSourcePath: string
    • To: remoteSourcePaths: string[]
  • Display:

    • A compact list of selected files (basename + full path tooltip) with remove buttons.
    • "Clear" action to empty the list.
    • Primary picker button opens the remote picker.
  • Add target path mode toggle (default preserves current behavior):

    • Directory (default): treat input as directory; normalize to end with /; always append source filename.
    • Exact Path (single-file only): treat input as full remote file path; do not append filename.
  • Start button enablement:

    • remoteSourceConnectionId != null
    • remoteSourcePaths.length > 0
    • remoteSelectedTargets.length > 0

SftpFilePickerModal

Add a "multi-select files" mode:

  • Directories are navigable only (click to enter), never selectable.
  • Files are toggle-selectable.
  • Add footer actions:
    • Confirm (N) emits selected file paths
    • Cancel
  • Maintain current search + hidden toggle behavior.

Modal selection UX:

  • Show a small "Selected (N)" summary area in the modal (e.g. in footer) with the ability to remove a selected file by clicking an x.
  • Disable Confirm when N = 0.

Deterministic output order:

  • select-many emits paths in selection-time order (first selected first). If a file is unselected and re-selected, it is appended to the end.

Visual selection indicator:

  • File rows show a checkbox (checked when selected). Directory rows never show a checkbox.

Selection behavior:

  • Selections persist while navigating folders within the modal (select in dir A, navigate to dir B, select more).
  • select-many returns the set selected in the modal session.
  • TransfersView merges returned paths into remoteSourcePaths (append + de-duplicate), so users can open the picker multiple times to add files.

API surface change:

  • Option A (preferred): extend props and emits without breaking existing callers:
    • Add optional prop: multiple?: boolean (default false)
    • If multiple is false: keep existing select signature (path: string).
    • If multiple is true: emit a new event name select-many with (paths: string[]).

Rationale: avoids touching other potential call sites and keeps types explicit.

Data Model / State

  • Keep remoteTargetDirOrPath as the raw input string.

  • Add remoteTargetMode: 'dir' | 'path'.

  • On selection:

    • De-duplicate paths.
    • Preserve ordering: de-duplicate by full path while keeping first-seen order; new selections append in the order confirmed in the picker.
  • Source connection is part of the meaning of a path:

    • When remoteSourceConnectionId changes, clear remoteSourcePaths (require re-pick).
    • Also remove the source connection from remoteSelectedTargets if present.

Execution Model

Transfer items

Each item represents one file transfer from source to one target:

  • Label: #<sourceId>:<sourcePath> -> #<targetId>:<targetDirOrPath>
  • Uses existing backend task creation and SSE progress.

Store API change:

  • Update the existing store method startRemoteToMany to accept sourcePaths: string[] instead of a single sourcePath.
  • TransfersView is the only caller; update types and call site accordingly.

Where target-path resolution lives:

  • Keep the resolution logic in the store (consistent with current buildRemoteTransferPath helper).
  • Extend startRemoteToMany params to include targetMode: 'dir' | 'path'.
  • Store computes targetPath per item using { targetMode, targetDirOrPath, filename }.

Target path resolution

For each sourcePath, compute filename as basename.

Rules are driven by the explicit remoteTargetMode:

  • Mode dir (default):

    • Treat remoteTargetDirOrPath as a directory.
    • Normalize to end with /.
    • Target path = normalizedDir + filename.
  • Mode path (single-file only):

    • Treat remoteTargetDirOrPath as an exact remote file path.
    • Target path = remoteTargetDirOrPath.

Multi-file constraint:

  • When remoteSourcePaths.length > 1, force mode dir (disable Exact Path).

Compatibility note:

  • Keep current behavior as the default: users who previously entered a directory-like value (with or without trailing /) will still get "append filename" semantics.
  • Exact Path is an explicit opt-in for single-file runs.

Examples:

  • source "/var/log/a.txt", mode dir, target input "/tmp" => target path "/tmp/a.txt"
  • source "/var/log/a.txt", mode dir, target input "/tmp/" => target path "/tmp/a.txt"
  • source "/var/log/a.txt", mode path, target input "/tmp/renamed.txt" => target path "/tmp/renamed.txt"

Validation & Errors

  • UI prevents selecting directories in picker.

  • If users type paths manually, guard client-side before starting:

    • Each sourcePath must be non-empty.
    • Each sourcePath must not end with / (heuristic directory indicator).
  • Still handle backend errors (permission denied, no such file) per item.

  • For very large runs (many items), UI should remain responsive:

    • Avoid excessive reactive updates; batch updates where feasible.
  • Basename collision:

    • Backend uses overwrite semantics; selecting two different files with the same basename into one target directory will overwrite.
    • If multiple selected files share the same basename and remoteTargetMode === 'dir', show a warning before starting.
      • Warning is non-blocking but requires explicit acknowledgement ("Continue" / "Cancel").

Observability

  • Keep existing console logs for remote transfers (optional to reduce noise later).

Rollout / Compatibility

  • Backend unchanged.
  • Existing Remote -> Many single-file flow remains possible (select one file).

Testing Strategy

  • Frontend:

    • Type-check build: npm run build (includes vue-tsc).
    • Manual test:
      • Select 2-3 files via picker; verify selected list and remove/clear.
      • Start transfer to 2 targets; confirm item count = files x targets.
      • Verify target path concatenation behavior for directory-like target.
      • Error handling: select a file without permission to confirm per-item error is surfaced.
  • Backend:

    • No change required.

Risks

  • Large files x targets increases total tasks/events and can load the client.
    • Simultaneous SSE connections should be bounded by the concurrency limiter.
    • Must explicitly close each SSE subscription when the task reaches a terminal state (success/error/cancelled) to avoid connection buildup.
    • Mitigation: cap UI selection count in future, or add a backend batch task later.

Open Questions

  • None (scope fixed to files-only, front-end only).