# 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 ``), 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. ## Proposed Approach (Recommended) 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: `#: -> #:` - 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).