# 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).