PCOperations¶
PCOperations is the file-operation engine: copy, move, delete, mkdir, plus
the higher-level "file utility" operations (attribute/permission changes,
checksums, duplicate detection, file splitting/combining, encode/decode, and
per-file comments). It owns how work is actually performed on bytes and paths —
progress reporting, cancellation/pause, conflict resolution, and the fast paths
(clonefile(2), rename(2)) that make the common case instant.
It contains no AppKit and no UI. Everything here is driven by a caller
(normally PCApp) that supplies an OperationResolver
for interactive decisions and a progress callback. Tests drive it directly with
non-interactive resolvers.
Design mandate: Swift Concurrency, no GCD (ADR-008) — actors + structured
concurrency for cancellation, AsyncStream for progress coalesced to ≤ 30 Hz.
Purpose & responsibility¶
| Concern | Owned here? |
|---|---|
| Recursive copy/move/delete/mkdir on the local file system | Yes |
| clone/rename/cross-device fallbacks, symlink safety | Yes |
| Progress accounting, throughput, speed limiting | Yes |
Cooperative cancel/pause (OperationControl) |
Yes |
| Conflict & per-file error resolution policy plumbing | Yes (policy decisions are delegated to a resolver) |
| VFS-level file utilities (attrs, checksum, dup-find, split, encode, comments) | Yes |
| The actual conflict dialog, progress UI, transfer manager window | No — PCApp |
The VirtualFileSystem protocol and concrete file systems |
No — PCVFS and its providers |
Hash algorithms, CRC32, Base64/UU codecs, descript.ion parsing, rename masks, SplitInfo |
No — PCFoundation (this module orchestrates them) |
Two layers coexist in the module:
- Local POSIX fast-path engines (
CopyEngine,MoveEngine,DeleteEngine,MkDirEngine) operate on plainStringpaths and syscalls for maximum speed on local→local operations (SPEC-004 §1–§8). - VFS-based utility engines (
AttributeEngine,ChecksumEngine,DuplicateFinder,SplitCombineEngine,EncodeDecodeEngine,CommentStore,VFSTreeWalker) operate over the abstractVirtualFileSystemfromPCVFS, so they work uniformly on local disk, archives, and network file systems (SPEC-016, SPEC-008).
Dependencies¶
graph TD
PCApp --> PCOperations
PCOperations --> PCVFS
PCOperations --> PCFoundation
PCVFS --> PCFoundation
Needs (from project.yml):
- PCFoundation — logging (PCFoundationLogger), ChecksumHasher/ChecksumAlgorithm/CRC32,
Base64Codec, UUCodec, CopyRenameMask, SplitInfo, ChecksumEntry/ChecksumFileFormat,
DescriptionFile.
- PCVFS — VirtualFileSystem, VFSPath, VFSEntry/VFSEntryBatch, VFSAttributes,
WriteOptions, read/write streams. The VFS-based engines require these; the
local POSIX engines do not touch the VFS.
Depended on by: only PCApp (import PCOperations) — via
PanelController+Operations, PanelController+Clipboard, TransferManager,
TransferManagerWindowController, ProgressDialog, OverwriteResolver,
DuplicateFinderWindowController, AutomationRunner, InputDialog. No engine
module depends on PCOperations.
Public interfaces & key types¶
Shared value types (OperationTypes.swift)¶
| Type | Kind | Role |
|---|---|---|
OpProgress |
struct | Live counts: files/bytes total & done, currentItem, bytesPerSecond. |
OpEvent |
enum | Stream event: .progress, .log, .completed(processed:), .failed, .cancelled. |
OperationError |
enum | Typed errors (cancelled, sourceNotFound, cannotCreateFile, readFailed, writeFailed, renameFailed, deleteFailed, aborted, invalidName, …). Mapped to user text only in PCApp. |
OverwriteDecision |
enum | .overwrite / .skip / .rename(String) / .append / .abort (append = F-086). |
ErrorDecision |
enum | .retry / .skip / .abort (F-089). |
FileFacts |
struct | Minimal facts (path, name, size, modified, isDirectory) handed to a resolver. |
CopyOptions |
struct | preserveMetadata, useCloneWhenPossible, followSymlinks, onlyNewer, chunkSize (1 MiB default), maxBytesPerSecond (0 = unlimited), renameMask (F-080). |
OperationResolver |
protocol | Async resolveOverwrite / resolveError. |
OverwriteAllResolver, SkipAllResolver |
struct | Non-interactive defaults for tests/background runs. |
OperationControl |
actor | Cooperative cancel/pause/checkpoint. |
OperationControl (actor)¶
The single cancellation/pause authority, shared between an engine and its caller:
public actor OperationControl {
public func cancel(); public func pause(); public func resume()
public var isCancelled: Bool; public var isPaused: Bool
public func checkpoint() async throws // throws .cancelled, else blocks while paused
}
Engines call await control.checkpoint() at every natural boundary (per item,
per directory child, per read chunk). Pause is implemented as a 20 ms poll loop
inside checkpoint() that still honours cancellation.
TransferQueue (TransferQueue.swift)¶
The public entry point that runs one operation and exposes its event stream.
Naming note / open question. The architecture overview and SPEC-004 refer to an
OperationQueueactor with a plan → execute → verify pipeline and multiple queues. In the current source the runner isTransferQueue, afinal classmarked@unchecked Sendable(not anactor), the only actor isOperationControl, and there is no distinct verify phase or multi-queue scheduler in this module. "Planning" today meansCopyEngine.planTotals/DeleteEngine.countItems. Multi-queue/verify behaviour is either handled inPCApp'sTransferManageror still unbuilt.
public enum OperationKind { // copy / move / trash / delete / custom
case copy(items:[String], toDirectory:String, options:CopyOptions)
case move(items:[String], toDirectory:String, options:CopyOptions)
case trash(items:[String])
case delete(items:[String])
case custom(run: @Sendable (OperationControl,
@Sendable (OpProgress)->Void) async throws -> [String])
}
public final class TransferQueue: @unchecked Sendable {
public let control = OperationControl()
public func run(_ kind: OperationKind,
resolver: OperationResolver = OverwriteAllResolver()) -> AsyncStream<OpEvent>
public func runToCompletion(_ kind: OperationKind,
resolver: OperationResolver = OverwriteAllResolver()) async throws -> [String]
}
runstarts the work in a detached task (heavy I/O never touches the caller's actor, e.g.@MainActor) and returns a liveAsyncStream<OpEvent>.- Progress is coalesced by an internal
ProgressThrottle(default 30 Hz,NSLock-guarded) — the ≤ 30 Hz rule of ADR-008. - Cancelling the stream, or
control.cancel(), stops the operation: the stream'sonTerminationcallscontrol.cancel()and cancels the task. .customletsPCApppush pack/unpack (PCArchive) or other work through the same queue so it backgrounds and appears in the transfer manager.
Local POSIX engines¶
| Type | Kind | Notes |
|---|---|---|
CopyEngine |
final class |
Recursive copy; clone/stream/metadata; append (F-086); rename mask (F-080); only-newer; speed limit. |
MoveEngine |
final class |
rename(2) fast path; cross-device / dir-merge → CopyEngine + DeleteEngine. |
DeleteEngine |
final class |
moveToTrash (via FileManager.trashItem) and recursive permanentDelete (unlink/rmdir). |
MkDirEngine |
enum (static) |
create(spec:in:) — nested a/b/c and |-separated multi-create. |
OverwriteRules |
enum (static) |
Pure decision functions: overwrite-if-newer / -if-larger, autoRenameName. |
FSLowLevel |
enum (internal) |
lstat-based kind/size/facts/sameDevice/readSymlink. Symlink-safe throughout. |
VFS utility engines¶
| Type | Kind | Purpose |
|---|---|---|
AttributeEngine |
enum (static) |
Apply VFSAttributes (posix mode, mtime, BSD flags, owner/group) to paths, optionally recursive; returns (changed, failed). |
ChecksumEngine |
enum (static) |
compute/create/verify digests over a VFS; VerifyResult/EntryStatus; extension→algorithm guess. |
DuplicateFinder |
enum (static) |
Two-tier (size bucket → content digest) exact-duplicate detection; DuplicateGroup; sorted by wasted space. |
SplitCombineEngine |
enum (static) |
Stream a file into .001/.002/… parts + <name>.crc sidecar; recombine and CRC-verify; SplitCombineError. |
EncodeDecodeEngine |
enum (static) |
Base64 / uu / xx encode; decodeAuto (uu/xx frame → hex → Base64); EncodeDecodeError. |
CommentStore |
enum (static) |
Read/write per-file comments in descript.ion via the VFS. |
VFSTreeWalker |
enum (static) |
Recursively collect regular-file paths in a VFS subtree; symlink-skipping, depth-guarded (default maxDepth: 16). |
Inputs & outputs¶
- Local engines take
[String]source paths + a destination directory string, and produce side effects on disk plus a[String]of fully processed source paths (the return value /.completed(processed:)). Progress flows out through the injected@Sendable (OpProgress) -> Voidcallback. - VFS engines take
VFSPaths plus aVirtualFileSystemand stream data throughopenRead/openWrite. Outputs are typed results ([VerifyResult],[DuplicateGroup],SplitInfo,[String: String]comment maps,(changed, failed)counts) rather than events. - The public boundary for a running job is
AsyncStream<OpEvent>fromTransferQueue.run.
Lifecycle¶
A TransferQueue-driven operation:
sequenceDiagram
participant UI as PCApp
participant TQ as TransferQueue
participant E as Engine (Copy/Move/…)
participant C as OperationControl (actor)
UI->>TQ: run(kind, resolver)
TQ-->>UI: AsyncStream<OpEvent>
TQ->>E: execute() in Task.detached
loop per item / chunk
E->>C: await checkpoint() (cancel? pause?)
E->>TQ: progress(OpProgress) --coalesced 30Hz--> UI
E-->>UI: resolveOverwrite/resolveError (on conflict/error)
end
E-->>TQ: processed [String] or throws
TQ-->>UI: .completed / .failed / .cancelled ; stream.finish()
CopyEngine.run first computes totals (planTotals, a stack walk with
FileManager.contentsOfDirectory) so the UI has a denominator, then copies each
top-level item, appending to processed only on success.
Threading / concurrency assumptions¶
- ADR-008. Actors + structured concurrency; cancellation via
OperationControl+Task; progress viaAsyncStreamcoalesced to ≤ 30 Hz. OperationControlis the only shared mutable state and is anactor, so cancel/pause are race-free across the worker task and the caller.- Each engine instance (
CopyEngine,MoveEngine,DeleteEngine) is single-consumer: it mutates its ownstate/processedfrom within one task. They are not designed for concurrentruncalls on the same instance. TransferQueueis@unchecked Sendable; the unchecked-ness is justified by running the body in a singleTask.detachedand guarding progress emission withProgressThrottle'sNSLock.- The progress callback may be invoked from the detached worker task; callers
that touch UI must hop to
@MainActorthemselves (asPCAppdoes).
Error handling¶
- Engines throw typed
OperationError..cancelledalways propagates immediately and is never routed to the resolver. - Conflicts (target exists): the engine builds
FileFactsfor source and target and callsresolver.resolveOverwrite. Decisions: .overwrite→ remove target (or, for files, letrename(2)replace it),.skip→ count as done, continue,.rename(newLeaf)→ retarget,.append(F-086) → keep target, stream source onto its end (regular files only; treated as overwrite for dirs/symlinks; on move, appends then deletes the source),.abort→ throw.aborted.- Per-file errors (F-089): the copy/move loop calls
resolver.resolveErrorand honours.retry(loop again),.skip(continue with next item),.abort(rethrow). This gives "continue on error" semantics. - Partial-write safety: a failed new target is
unlinked in thecopyFileDatacatch; an append target is never unlinked (that would destroy pre-existing data). - Symlink safety:
FSLowLeveluseslstatthroughout, and delete/copy never descend through a symlinked directory —unlinkremoves the link, not its target;VFSTreeWalkerskipssymlinkDir/symlinkFile. - VFS utility engines favour partial success:
AttributeEnginereturns(changed, failed)counts,ChecksumEngine.createskips unreadable files,VFSTreeWalkerswallows listing errors and returns what it has.
Fast paths (why the code is shaped this way)¶
- Copy: try
clonefile(2)(same volume, target absent) — instant, copy-on-write, preserves metadata. OnEXDEV/ENOTSUP/EEXISTfall through to a chunkedread/writeloop (cancellable, pausable, speed-limited viaCopyOptions.maxBytesPerSecond) thencopyfile(3)withCOPYFILE_METADATA. - Move:
rename(2)when same-device and not a dir-into-dir merge (atomic, replaces a file target). Otherwise copy the whole tree then delete the source — and only delete after a successful copy. - Duplicate find: size bucket first (cheap
stat), hash only same-size candidates — avoids hashing unique files.
These mirror SPEC-004's requirement that the common local→local case be as fast as the OS allows, while still supporting arbitrary VFS→VFS transfers elsewhere.
How it is tested¶
Target PCOperationsTests (~61 test functions across 13 files) exercises the
module directly, with no UI, using OverwriteAllResolver / SkipAllResolver or
bespoke resolvers:
PCOperationsTests.swift— core copy/move/delete/mkdir behaviour.CopyAppendTests,CopyContinueOnErrorTests,CopyMaskCopyTests— F-086 append, F-089 continue-on-error, F-080 rename masks.OverwriteRulesTests— the pure decision functions.AttributeEngineTests,ChecksumEngineTests,DuplicateFinderTests,SplitCombineEngineTests,EncodeDecodeEngineTests,CommentStoreTests,VFSTreeWalkerTests— the VFS utility engines against in-memory/local VFS fixtures.CustomOperationTests— the.customOperationKindpath throughTransferQueue.
Part of the repo-wide ~1304-test suite; run via Tools/test.sh.
Extension points¶
- New operation type: add a case to
OperationKindand a branch inTransferQueue.execute. For app-specific work (pack/unpack, sync) prefer.custom— it needs no engine changes and still backgrounds + reports through the same stream. - New conflict/error policy: implement
OperationResolver. The interactive GUI resolver lives inPCApp(OverwriteResolver); tests supply their own. - New blanket overwrite rule: add a pure function to
OverwriteRulesand have a resolver call it (keeps policy unit-testable without the GUI). - New file-utility operation over the VFS: follow the existing static-
enumpattern (ChecksumEngine,EncodeDecodeEngine, …) — read/transform/write viaVirtualFileSystem, keep algorithms inPCFoundation, keep orchestration here. ReuseVFSTreeWalkerfor recursion. - New copy behaviour toggle: extend
CopyOptions(it is the single options bag threaded through copy/move).
Open questions¶
- The
OperationQueueactor + plan/execute/verify pipeline and the multi-queue scheduler described in SPEC-004 / the architecture overview are not present in this module today (see the naming note above). Whether "verify" and multi-queue scheduling land inPCOperationsor stay inPCApp'sTransferManageris undecided. CopyOptions.followSymlinks = trueresolves viaresolvingSymlinksInPathand copies the target; cycle protection on followed links is not explicit.