From cbe68485190a89b031d317aab9a03febe87caec1 Mon Sep 17 00:00:00 2001 From: CMf-pixel Date: Thu, 10 Sep 2026 21:59:39 +0500 Subject: [PATCH] Rewrite README with user-focused usage, safety, and build documentation --- .github/ISSUE_TEMPLATE/bug_report.yml | 55 +++++++ .github/workflows/ci.yml | 24 +++ .github/workflows/release.yml | 59 +++++++ CHANGELOG.md | 15 ++ CONTRIBUTING.md | 22 +++ README.md | 220 +++++++------------------- docs/images/fileflow-main.png | Bin 0 -> 20114 bytes docs/releasing.md | 92 +++++++++++ global.json | 7 + scripts/Publish-Portable.ps1 | 76 +++++++++ src/Directory.Build.props | 9 ++ 11 files changed, 417 insertions(+), 162 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 docs/images/fileflow-main.png create mode 100644 docs/releasing.md create mode 100644 global.json create mode 100644 scripts/Publish-Portable.ps1 create mode 100644 src/Directory.Build.props diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..3fc81fc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,55 @@ +name: Bug report +description: Report a reproducible problem in FileFlow +title: "[Bug]: " +body: + - type: markdown + attributes: + value: | + Thanks for reporting a problem. Please remove usernames, personal folder names, filenames, and other private data from anything you share. + - type: input + id: version + attributes: + label: FileFlow version + description: For example, 0.1.0. + placeholder: 0.1.0 + validations: + required: true + - type: input + id: windows + attributes: + label: Windows version + description: Include the edition and version or build when known. + placeholder: Windows 11 24H2 + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: Steps to reproduce + description: Use temporary sample folders where possible. Describe the rule and exact sequence of actions. + placeholder: | + 1. Create a Copy rule for .txt files... + 2. Choose Run... + 3. Choose Execute... + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behavior + validations: + required: true + - type: textarea + id: actual + attributes: + label: Actual behavior + description: Include any error message shown by FileFlow. + validations: + required: true + - type: textarea + id: logs + attributes: + label: Sanitized logs or additional context + description: Optional. Attach only material you have checked for usernames, personal paths, filenames, and other private data. + validations: + required: false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..14184e1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,24 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +jobs: + build-and-test: + runs-on: windows-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-dotnet@v6 + with: + dotnet-version: '8.0.x' + - name: Restore + run: dotnet restore FileFlow.sln + - name: Release tests + run: dotnet test FileFlow.sln --configuration Release --no-restore + - name: Release build + run: dotnet build FileFlow.sln --configuration Release --no-restore diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..daabcbc --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,59 @@ +name: Draft release + +on: + push: + tags: ['v*'] + +permissions: + contents: read + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +jobs: + draft-release: + runs-on: windows-latest + permissions: + contents: write + defaults: + run: + shell: pwsh + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-dotnet@v6 + with: + dotnet-version: '8.0.x' + - name: Validate tag and dated changelog + env: + RELEASE_TAG: ${{ github.ref_name }} + run: | + if ($env:RELEASE_TAG -notmatch '^v\d+\.\d+\.\d+$') { throw 'Expected a vMAJOR.MINOR.PATCH tag.' } + $version = (dotnet msbuild src/FileFlow.App/FileFlow.App.csproj -nologo -getProperty:Version) -join "`n" + if ($LASTEXITCODE -ne 0) { throw 'Could not read application version.' } + $version = $version.Trim() + if ($env:RELEASE_TAG -cne "v$version") { throw 'Tag does not match application version.' } + $heading = '(?m)^## \[' + [regex]::Escape($version) + '\] - (\d{4}-\d{2}-\d{2})\r?$' + if ((Get-Content CHANGELOG.md -Raw) -notmatch $heading) { throw 'Date the changelog before creating the release tag.' } + [datetime]::ParseExact($Matches[1], 'yyyy-MM-dd', [cultureinfo]::InvariantCulture) | Out-Null + - name: Restore + run: dotnet restore FileFlow.sln + - name: Release tests + run: dotnet test FileFlow.sln --configuration Release --no-restore + - name: Release build + run: dotnet build FileFlow.sln --configuration Release --no-restore + - name: Package portable Windows build + run: ./scripts/Publish-Portable.ps1 + - name: Create unpublished draft with portable ZIP + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + RELEASE_TAG: ${{ github.ref_name }} + run: | + # Include drafts: --draft alone does not guarantee uniqueness for an existing tag. + $existingTags = @(gh api --paginate "repos/$env:GH_REPO/releases?per_page=100" --jq '.[].tag_name') + if ($LASTEXITCODE -ne 0) { throw 'Could not check existing releases; refusing to create a draft.' } + if ($env:RELEASE_TAG -cin $existingTags) { throw 'A release or draft already exists for this tag.' } + gh release create $env:RELEASE_TAG "artifacts/FileFlow-$env:RELEASE_TAG-win-x64.zip" ` + --draft --verify-tag --title "FileFlow $env:RELEASE_TAG" --notes-file CHANGELOG.md + if ($LASTEXITCODE -ne 0) { throw 'Draft creation failed; inspect GitHub before retrying.' } diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..beec58a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,15 @@ +# Changelog + +All notable changes to FileFlow are documented in this file. + +## [0.1.0] - Unreleased + +- Added a Windows desktop workflow for creating, editing, deleting, previewing, and explicitly executing saved Move or Copy rules. +- Added case-insensitive direct-file extension matching and immediate-folder scanning. +- Added read-only previews with ordered operations and blocking diagnostics for missing directories, conflicts, unsupported network paths, and reparse points. +- Added guarded execution with whole-batch preflight validation, per-operation revalidation, non-overwriting file operations, and ordered results. +- Added local JSON rule storage at `%LOCALAPPDATA%\FileFlow\rules.json`, including validation, writer locking, and retention of one previous valid document. +- Added per-Windows-user single-instance handling. +- Added Windows-targeted automated tests for Core behavior and the desktop presentation workflow. +- Added a self-contained portable Windows x64 ZIP packaging path with runtime license notices and overwrite protection. +- Added pull request and `main` CI plus guarded tag-based preparation of unpublished draft releases. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..326ed5f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,22 @@ +# Contributing to FileFlow + +Thanks for helping improve FileFlow. Bug reports, documentation corrections, tests, and focused fixes are welcome. Please open an issue to discuss large features, architectural changes, or broad scope changes before investing substantial work. + +## Report a bug + +Use the bug report form and include the FileFlow version, Windows version, clear reproduction steps, expected behavior, and actual behavior. Attach logs only when useful, and remove usernames, personal folder names, filenames, and other private data first. + +## Develop locally + +Development requires Windows and the .NET 8 SDK `8.0.400` or newer within the .NET 8.0 SDK line. + +```powershell +dotnet restore FileFlow.sln +dotnet test FileFlow.sln --configuration Release --no-restore +dotnet build FileFlow.sln --configuration Release --no-restore +dotnet run --project src/FileFlow.App/FileFlow.App.csproj --configuration Release +``` + +Keep changes focused, preserve FileFlow's preview-before-execution safety model, and add meaningful tests when behavior changes. Before submitting a pull request, run the Release test and build commands above and describe the behavior change and validation performed. + +By contributing, you agree that your contribution is licensed under the repository's [MIT License](LICENSE). diff --git a/README.md b/README.md index 7ad50b0..25cf7e5 100644 --- a/README.md +++ b/README.md @@ -1,197 +1,93 @@ # FileFlow -A local Windows file automation tool. FileFlow v0.1 is being built around one safety rule: show every planned Move or Copy and require explicit confirmation before execution. +[![CI](https://github.com/CMf-pixel/FileFlow/actions/workflows/ci.yml/badge.svg)](https://github.com/CMf-pixel/FileFlow/actions/workflows/ci.yml) -## Current status +A local Windows file automation tool that previews every planned operation before changing your files. -Milestone 1 provides a rule model, structural validation, extension normalization and matching. Milestone 2 adds local JSON rule persistence. Milestone 3 adds synchronous, read-only preview in Core with isolated filesystem tests. Milestone 4 adds controlled Core execution of an explicitly approved preview. Milestone 5 connects these APIs to a compact dark WPF workflow: saved rules, create/edit/delete, read-only Preview, explicit Execute, busy feedback, and ordered results. Undo and release packaging remain outside this milestone. +FileFlow turns a repeated folder cleanup into a saved rule: choose a source folder, file extensions, a Move or Copy action, and a destination. Every run starts with a reviewable preview and requires an explicit Execute click. -## Build and test +## Screenshot -Requires Windows and the .NET 8 SDK. Run these commands from the repository root: +![FileFlow](docs/images/fileflow-main.png) -```powershell -dotnet build FileFlow.sln -dotnet test tests/FileFlow.Core.Tests/FileFlow.Core.Tests.csproj -dotnet run --project src/FileFlow.App/FileFlow.App.csproj -``` - -## Project structure - -- `src/FileFlow.App`: WPF application, referencing the core library. -- `src/FileFlow.Core`: ordinary .NET library with no WPF or external package dependencies. -- `tests/FileFlow.Core.Tests`: Windows-targeted xUnit tests of Core and App presentation behavior. Core itself remains on `net8.0`; App and tests target `net8.0-windows`. - -## Rule validation and matching - -`RuleValidator.Validate` returns either a normalized rule or field-specific errors. It preserves the rule ID and action, trims the name, normalizes absolute Windows directory paths, and rejects identical source/destination paths. New definitions default to Copy. +## Why FileFlow -Extensions are supplied as separate entries. `png`, `.png`, and `.PNG` normalize to `.png`; duplicates are removed. Empty entries, wildcards, path separators, and compound extensions are rejected. `ExtensionMatcher.Matches` consumes a validated rule and checks the final filename extension, case-insensitively, against any listed extension. For example, `.gz` matches `archive.tar.gz`. +Moving a group of files by hand is tedious, while unattended automation can be hard to trust. FileFlow keeps the rule reusable and the operation visible. You see the exact source and destination of every planned file operation before anything changes. -Validation and matching do not access the filesystem. They use Windows path semantics even though the core has no WPF dependency. Structural validation rejects UNC and device paths, but does not establish that a drive is local, a directory exists, or an operation is permitted. Preview checks existence, mapped network drives, unsupported links/junctions, and conflicts. Execution independently repeats the relevant checks before any mutation. +## Features -## Rule persistence (Milestone 2) +- Save, edit, and delete local Move or Copy rules. +- Use a compact dark Windows desktop interface. +- Match file extensions case-insensitively, such as `.png` and `.jpg`. +- Preview every planned operation in a read-only step. +- Detect missing folders, destination conflicts, unsupported paths, and stale previews. +- Revalidate the complete batch before execution and validate each operation again immediately before it runs. +- Keep rules locally at `%LOCALAPPDATA%\FileFlow\rules.json`. +- Prevent two FileFlow instances from running for the same Windows user. +- Work locally without a cloud service or account. -`FileFlow.Core.Persistence.FileRuleStore` defaults to `%LOCALAPPDATA%\FileFlow\rules.json`. Pass an explicit storage directory to its constructor for isolated storage. All persistence tests use unique OS temporary directories; they never use the default store location. +## How it works -`Load()` and `Save(IReadOnlyList)` return `RuleStoreResult`. Check `IsSuccess` before using `Rules`. Success contains the loaded or saved snapshot, including an empty collection on first launch. Failure contains a `RulePersistenceError` with `InvalidData`, `UnsupportedSchemaVersion`, or `IoError`, a descriptive message, and null rules. Callers must display/handle the error; a failed load must not be treated as an empty rule set. The WPF app displays load failures as an error state with Retry and blocks rule edits until loading succeeds. It publishes create/edit/delete changes only after storage succeeds. +1. **Create a rule.** Choose a name, existing source and destination folders, one or more extensions, and Move or Copy. +2. **Run / Preview.** FileFlow scans only the files directly inside the source folder. Preview is read-only: it does not create directories, write probes, move files, or copy files. +3. **Review.** Check every planned source and destination. A blocking issue prevents the whole batch from executing. +4. **Execute.** FileFlow revalidates the entire preview before the first file operation. A rejected preflight makes zero filesystem mutations. During execution it validates each operation again and stops at the first failure. -The UTF-8 JSON format is: +## Usage -```json -{ - "schemaVersion": 1, - "rules": [ - { - "id": "271df344-1e31-4ea0-b1b8-716cfa9f39e2", - "name": "Sort images", - "sourceDirectory": "C:\\Downloads", - "extensions": [".png", ".jpg"], - "action": "Copy", - "destinationDirectory": "C:\\Pictures" - } - ] -} -``` - -Rule order and every field are preserved, including Copy/Move. All properties are required; unknown/duplicate properties, invalid rule definitions, malformed JSON, and unsupported versions are rejected. Structural validation does not require the source/destination directories to exist and does not normalize away stored values. +For a safe first run, create the empty folders `C:\FileFlowDemo\Inbox` and `C:\FileFlowDemo\Sorted`, then place a few test images in `Inbox`. -Saving validates the existing active document, writes a unique temporary file beside it, and calls `Flush(true)` before closing it. When an active file exists, its exact valid bytes are also staged and flushed, then published as `rules.json.bak`. Only then is the new active file published with a same-directory overwrite rename. The active file is never deleted first. This avoids the partial-failure states documented for [Windows ReplaceFile](https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-replacefilew). A failed active publication can leave the backup equal to the still-active file; it cannot activate the failed save. One previous backup is retained after successful replacement. +1. Choose **Create rule** and enter a name. +2. Set the source to `C:\FileFlowDemo\Inbox` and the destination to `C:\FileFlowDemo\Sorted`. +3. Enter `.png, .jpg` as the extensions. Copy is selected by default; keep it selected for this example. +4. Save the rule and choose **Run**. +5. Review every source and destination in Preview, then choose **Execute**. -An exclusively opened `rules.json.lock` prevents overlapping FileFlow writers; contention returns an I/O error for the caller to handle. The empty lock file stays on disk, but the OS releases its lock when the stream/process closes. This coordinates FileFlow stores, not unrelated programs editing the files externally. +## Download and run -Corrupt/unsupported active files are preserved and block saves, including saves through a new store instance. Backups and abandoned temporary files are never automatically loaded, restored, or promoted. Cleanup only removes temporary files created by that save; if cleanup fails they remain inert. Recovery requires inspecting/preserving the files manually. There is no recovery UI, import/export, or background monitoring in this milestone. Durability still depends on the local filesystem and storage device honoring flushes; the active rename and backup update are separate steps. +Download `FileFlow-v0.1.0-win-x64.zip` from the [latest release](https://github.com/CMf-pixel/FileFlow/releases/latest). -Run the Milestone 2 tests with: +The portable Windows x64 ZIP is self-contained: it is approximately 70 MiB compressed and 160 MiB after extraction, and it does not require a separate .NET installation. -```powershell -dotnet test tests/FileFlow.Core.Tests/FileFlow.Core.Tests.csproj --filter FullyQualifiedName~FileRuleStoreTests -dotnet test FileFlow.sln -dotnet build FileFlow.sln --configuration Release -``` +1. Extract **all** files from the ZIP to a folder. +2. Run `FileFlow.App.exe` from the extracted folder. -## Read-only preview (Milestone 3) - -Pass a rule to `FileFlow.Core.Preview.FileRulePreviewer.CreatePreview(FileRule)`. This synchronous API does not load or save rules, execute operations, create directories, open write streams, or perform write probes. The WPF application invokes it off the UI thread; Core has no background-service architecture. - -```csharp -var preview = new FileRulePreviewer().CreatePreview(rule); -foreach (var operation in preview.Operations) -{ - Console.WriteLine($"{operation.Action}: {operation.SourcePath} -> {operation.DestinationPath}"); -} -foreach (var issue in preview.Issues) -{ - Console.WriteLine($"{issue.Code}: {issue.Path ?? issue.PropertyName}: {issue.Message}"); -} -Console.WriteLine(preview.Status); // NoMatches explicitly means zero files matched. -``` +The application is portable, but saved rules remain in `%LOCALAPPDATA%\FileFlow\rules.json`. -`OperationPreview` contains an immutable normalized rule snapshot, read-only ordered operations, and typed blocking issues. A structurally invalid rule returns validation issues with a null normalized rule. Each `PlannedOperation` preserves the original filename and records full source/destination paths, Copy/Move action, source byte length, and last-write time in UTC. Operations are ordered by ordinal case-insensitive filename, then ordinal filename to break ties. +## Safety model -Status precedence is explicit: +FileFlow never overwrites an existing destination. Preview is read-only, and every preview can be submitted only once. Before execution, FileFlow rechecks the full approved batch; any preflight problem rejects the batch without starting a file operation. It then validates each operation immediately before it runs and uses non-overwriting Move and Copy calls. -1. `Incomplete`: any required scan or inspection failed, even if conflicts were also found. Known operations remain available for diagnosis; the list may be partial. -2. `Blocked`: the completed evaluation found blocking issues, including confirmed validation/preflight rejection. -3. `NoMatches`: the completed evaluation found no issues and zero operations. -4. `Ready`: the completed evaluation found operations and no issues. +If a runtime check or file operation fails, FileFlow stops at that operation. Earlier operations can already have succeeded, so the filesystem may be partially changed. A failed operation can also leave partial output or an uncertain state. FileFlow has no rollback, transaction, or Undo, and execution cannot be cancelled after it starts. -`CanExecute` describes preview eligibility only: it is true exclusively for `Ready` with operations and no issues. It does not record whether execution has already been attempted. All issues block the entire batch; a conflict is never overwritten, renamed, or skipped automatically. Resolve the issue and generate a fresh preview. +These checks reduce filesystem races but cannot eliminate them. FileFlow does not lock paths, reserve destination names, hash file contents, or guarantee that a file cannot change between a check and an operation. Review the results and inspect both folders after any failure. -Preview enumerates immediate source entries only, matches ordinary files using the existing extension matcher, and ignores ordinary subdirectories without opening them. Missing source/destination folders, equivalent normalized directory paths, existing destination entries, and duplicate case-insensitive destination names block the batch. Missing destinations are never created. Access errors, vanished source entries, metadata failures, and interrupted enumeration are reported instead of silently omitted. +## Current limitations -UNC/device paths and mapped network drives are unsupported. Attribute checks reject reparse points in source/destination directory components (root first) and all encountered direct source entries, including nonmatching entries. This deliberately conservative rule also rejects junctions, symbolic links, and other reparse entries such as some cloud placeholders. Preview does not follow links, parse reparse tags, use FSCTL calls, or traverse source subdirectories. +- Windows x64 only. +- Rules match a file's direct extension only: `.gz` matches `archive.tar.gz`; compound patterns such as `.tar.gz`, wildcards, and extensionless matching are unsupported. +- Source and destination directories must already exist. +- UNC paths, mapped network drives, and reparse points are unsupported. Reparse points include junctions, symbolic links, and some cloud-storage placeholders. +- FileFlow scans only the immediate source folder. There is no recursive scan, folder watching, or scheduling. +- There is no Undo, rollback, transaction, conflict auto-resolution, or overwrite mode. +- There are no cloud services, accounts, or telemetry. +- Closing is disabled while execution is active; execution has no cancellation control. -Preview is an observation, not an atomic filesystem snapshot. Paths, metadata, permissions, and destination contents may change during or after inspection. It does not reserve names, test write permissions, or provide execution-time guarantees; the executor independently revalidates. Link and network failure paths are covered with an internal inspection-only test adapter. Real filesystem safety tests use unique OS temporary directories and compare directory inventories and file bytes before and after preview. +## Build from source -Run preview tests and verification with: +Source builds require Windows and the .NET 8 SDK `8.0.400` or newer within the .NET 8.0 SDK line. From the repository root: ```powershell -dotnet test tests/FileFlow.Core.Tests/FileFlow.Core.Tests.csproj --filter FullyQualifiedName~Preview -dotnet test FileFlow.sln -dotnet build FileFlow.sln --configuration Release +dotnet restore FileFlow.sln +dotnet test FileFlow.sln --configuration Release --no-restore +dotnet build FileFlow.sln --configuration Release --no-restore +dotnet run --project src/FileFlow.App/FileFlow.App.csproj --configuration Release ``` -## Controlled execution (Milestone 4) - -`FileFlow.Core.Execution.FileOperationExecutor.Execute(OperationPreview approvedPreview)` executes the exact previously generated preview, synchronously. The caller must show that preview and obtain explicit approval before calling. Core cannot verify human consent. There is no `FileRule` overload, rescan, or automatic preview regeneration; newly added source files are ignored. - -```csharp -// This method is called only after explicit approval of this exact preview. -static ExecutionResult ExecuteApprovedPreview(OperationPreview approvedPreview) -{ - return new FileOperationExecutor().Execute(approvedPreview); -} -``` - -Use the `FileFlow.Core.Preview` and `FileFlow.Core.Execution` namespaces. Milestone 5 calls this API away from the UI thread. Core remains synchronous and has no scheduling or background service. - -Each preview object permits one attempt per process, including rejected attempts. A shared `ConditionalWeakTable` and atomic flag prevent reuse through another executor instance or concurrent submission. Preview data stays immutable; the guard adds no persistence, sessions, messaging, or lifecycle service. After any attempt, generate a fresh preview and obtain approval again. `CanExecute` remains preview eligibility only. Null input and unexpected programming errors throw; an unexpected error does not release a claimed preview. - -Before the first mutation, the executor requires `Ready`, at least one operation, and zero issues. It checks the entire approved batch: canonical local paths and unchanged filename/action, ordinary source files, exact source length and last-write UTC, existing ordinary directory ancestors inspected root-first, supported drives, and absent destination names. Source/destination reparse points, network drives, missing parents, inspection failures, and file or directory conflicts reject the batch. The separate execution filesystem boundary has only three inspection methods and non-overwriting Copy/Move; preview retains its original read-only boundary. - -**Preflight failure means zero mutations.** Every operation is `NotAttempted`, with ordered diagnostics identifying the failing paths and zero-based operation indexes. Repairing a stale condition does not make that preview reusable. Preflight does not create directories or probe write permissions. - -After preflight, operations run sequentially in the exact displayed order. Immediately before each operation, its source metadata, destination, drives, and ancestors are checked again. The adapter calls `File.Copy` or `File.Move` with `overwrite: false`. A destination appearing after the checks still causes the non-overwriting operation to fail. - -Copy success requires a destination file of the expected length and the original source still present with matching preview metadata. Move success requires a destination file of the expected length and confirmed source absence. Cross-volume local Move is allowed using .NET's implementation; it can internally copy/delete. Returning from `File.Move` is therefore followed by final-state verification. If both entries remain, `MoveIncomplete` reports failure and preserves both. Access failures or missing parents cannot be mistaken for confirmed source removal. [Microsoft documents these cross-volume Move semantics](https://learn.microsoft.com/en-us/dotnet/api/system.io.file.move). - -The first per-operation check, mutation, or verification failure stops the batch. Earlier successes remain, that operation is `Failed`, and every later operation is `NotAttempted`. No retry, rollback, overwrite, rename, skip-conflict behavior, or destructive cleanup occurs. A failed mutation may leave partial destination content or an uncertain filesystem state; results do not imply that failure left files unchanged. Expected filesystem failures, including I/O, permission, security, and path errors, become typed diagnostics with readable messages. - -`ExecutionResult` defensively copies its ordered `Operations` and `Errors` collections and derives `SucceededCount`, `FailedCount`, and `NotAttemptedCount` from the operation results. Each `OperationResult` retains the original `PlannedOperation`, its status, and an optional error. Overall outcomes are: - -| Outcome | Meaning | -| --- | --- | -| `Rejected` | The operation loop was never entered; `PreflightRejected` is true and all entries are `NotAttempted`. | -| `Succeeded` | Every approved operation completed and passed final-state verification. | -| `Failed` | Execution stopped without a verified successful operation. | -| `PartiallySucceeded` | At least one operation succeeded before execution stopped. | - -These checks reduce races without making the filesystem transactional. Length and timestamp are not file identity or content fingerprints; same-metadata replacements and changes during or after checks can escape detection. Production does not hash files, reserve names, lock paths, or provide crash recovery. Success describes the observed postconditions, not future state. Tests verify copied bytes directly and simulate cross-volume/incomplete outcomes without depending on a second physical drive. - -All real execution tests use uniquely named, owned OS temporary directories. Representative stale-preview tests compare directory inventories and file bytes after introducing staleness but before execution, then after rejection, while asserting zero mutation calls. Fault injection covers disk-full-style failures, deterministic mid-batch failures, reparse/network changes, and destination races. No tests operate on real user folders or disconnect actual drives. - -Final Milestone 4 verification: - -```powershell -dotnet test FileFlow.sln --configuration Release -dotnet build FileFlow.sln --configuration Release -git diff --check -``` - -Operation history persistence, Undo, rollback, watchers, recursive scanning, transactions, locking infrastructure, remain excluded from Core execution. No new projects or third-party packages are required. - - -## Desktop workflow (Milestone 5) - -Launch FileFlow to see your saved rules. Create a rule with a name, source folder, comma-separated extensions such as `.png, jpg`, a Copy or Move action, and a destination folder. Copy is the default. Both folder fields support manual paths and the native Windows folder picker. Core normalizes valid extensions and paths. A fresh Create dialog hides validation errors until a field is edited or left; Save stays disabled until Core reports the rule structurally valid. Once valid, the editor shows any newly introduced errors, including related-field errors. A direct invalid save attempt reveals all errors without persisting. - -A rule can be saved while a folder is unavailable. Folder existence, accessibility and supported-path checks happen when you choose Run. Only files directly inside the source folder are considered; rules never watch folders or run automatically. - -Run prepares a preview without changing files. Ready previews show every planned source/destination pair and require an explicit Execute action. Blocked, Incomplete and NoMatches previews cannot execute. Incomplete lists are diagnostic and may be partial. Closing an idle preview changes nothing. - -Preview generation and execution run away from the WPF dispatcher. Execution displays an indeterminate busy indicator because Core returns a complete result without progress callbacks. Closing is blocked while work is active. There is no cancellation of Copy/Move. - -Results distinguish Succeeded, Failed and NotAttempted in preview order. Failures may leave filesystem changes, and completed operations are not rolled back. A preflight rejection starts no file operations. After any attempt, close the results and Run again to generate a fresh preview; the previous Execute action cannot be reused. There is no Undo. - -Deleting a rule requires confirmation and affects only the saved definition. It never deletes, moves, restores, or otherwise changes your files. If saving fails, the previous saved list and editor draft remain available. Corrupt or unsupported storage is preserved and never replaced with an empty rule set. FileFlow has no automatic recovery UI. - -Only one FileFlow instance may run for a Windows user, including across sessions. A second launch displays an explanation and exits. Different Windows users have independent mutex names. No IPC or command forwarding is used. - -App uses small constructor-injected view models and service interfaces. Window/dialog APIs stay in views, startup and the WPF dialog adapter. Shared background/surface/text resources allow later appearance changes without changing workflow architecture; Mica/Acrylic and Settings are not implemented. - -FileFlow's WPF windows use native dark captions on Windows 11, with standard system captions on older Windows or in high-contrast mode. Owned application dialogs center on their owner. The compact initial and minimum window sizes are recorded in the verification notes. - -### Milestone 5 verification - -```powershell -dotnet test FileFlow.sln --configuration Release -dotnet build FileFlow.sln --configuration Release -git diff --check -``` +## Contributing -Presentation tests cover startup, persistence failures, normalization, create/edit/delete, duplicate activation, all preview statuses, exact preview identity, one-attempt behavior, ordered results, unexpected failures, background scheduling and mutex contention/recovery. Real integration tests create unique OS temporary folders for persistence, Copy/Move, cancellation and stale-preview rejection. No automated test uses the default user rules store. +Bug reports and focused fixes are welcome. Read [CONTRIBUTING.md](CONTRIBUTING.md) before starting; please discuss large scope changes first. -Interactive smoke checklist (use temporary/sample folders for file mutations): first launch; create/edit/delete and restart; Copy and Move; no matches and conflicts; stale previews and partial failures; long paths; keyboard focus and Escape/Enter; resizing; high DPI; large-list scrolling; closing while busy; second-instance behavior. Validation records distinguish automated/offscreen checks from interactive checks still requiring a Windows desktop session. +## License -See [Milestone 5 verification](docs/milestone-5-verification.md) for executed automated/offscreen checks and the remaining interactive smoke checklist. +FileFlow is available under the [MIT License](LICENSE). diff --git a/docs/images/fileflow-main.png b/docs/images/fileflow-main.png new file mode 100644 index 0000000000000000000000000000000000000000..f616bac9b9acaaffa36c54a8f3d5ee6cf8081a9d GIT binary patch literal 20114 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 AwEzGB literal 0 HcmV?d00001 diff --git a/docs/releasing.md b/docs/releasing.md new file mode 100644 index 0000000..f64c855 --- /dev/null +++ b/docs/releasing.md @@ -0,0 +1,92 @@ +# Releasing FileFlow + +This checklist describes the public `v0.1.0` release. Run commands from the repository root on Windows. Do not treat hosted CI, a real screenshot, or real GUI and no-runtime smoke testing as complete until each has actually been performed and its evidence recorded. + +## 1. Prepare the release commit + +- Confirm the intended version is `0.1.0` everywhere it is exposed in application and package metadata. +- Replace `Unreleased` in `CHANGELOG.md` with the actual release date in `YYYY-MM-DD` format **before** creating the final tag. The dated changelog must be part of the approved commit that receives the tag. +- Update the GitHub repository About description. It currently advertises Undo; replace it with: `A local Windows file automation tool that previews every planned Move or Copy before execution.` +- Optionally add accurate repository topics after reviewing them. +- Add a real application screenshot only after user approval. The image must not expose usernames, personal folders, desktop contents, unrelated applications, or other private data. Save the approved image as `docs/images/fileflow-main.png` and replace the README placeholder. +- Keep runtime-license discovery simple. Use only clear license files or metadata exposed by the SDK and published runtime. If the required notices or redistribution terms are ambiguous, stop and report the uncertainty instead of inventing or inferring a license inventory. +- Review the complete diff, confirm only intended files are present, and ensure the source worktree is clean before tagging. +- Obtain explicit approval before merging a pull request, pushing repository changes, or creating/updating a pull request. + +`SECURITY.md` and a pull request template are deferred beyond this release. + +## 2. Verify source + +```powershell +dotnet restore FileFlow.sln +dotnet test FileFlow.sln --configuration Release --no-restore +dotnet build FileFlow.sln --configuration Release --no-restore +git diff --check +``` + +Record the command output and commit SHA. Confirm the `windows-latest` CI workflow passes for pull requests and `main`; do not mark hosted CI complete until the GitHub run succeeds. + +## 3. Publish the portable archive + +Run: + +```powershell +pwsh -File ./scripts/Publish-Portable.ps1 +``` + +Packaging requires PowerShell 7 or later (`pwsh`) in addition to the .NET 8 SDK. + +The script publishes a clean self-contained Windows x64 application, stages it under `artifacts/staging/`, and creates `artifacts/FileFlow-v0.1.0-win-x64.zip`. It refuses to overwrite an existing archive. If rerunning is necessary, manually preserve the existing artifact outside the output path before rerunning; do not weaken the overwrite guard. + +Verify the publish is clean and contains the executable, required runtime files, application assemblies, and applicable license notices. Confirm the ZIP name and version metadata are exact, inspect its contents, extract it to a different folder, and record file size and SHA-256 evidence. + +## 4. Manual smoke test + +Use a disposable Windows account or VM with fresh FileFlow storage and only test-owned temporary files. Test the application from the published `FileFlow.App.exe`, not a development build. + +- First launch, rule create/edit/delete, restart, and persisted state. +- Copy and Move, checking file bytes and final source/destination state. +- Close an idle preview and confirm zero filesystem mutation. +- Existing destination conflict and zero mutation. +- Stale preview rejection and zero mutation. +- Extension matching, nested directories ignored, and no-match behavior. +- Second-instance handling. +- Extract the ZIP into a different folder and run it there. +- Run on Windows x64 without a separately installed .NET runtime. + +Record OS details, test paths, results, artifact SHA-256, and screenshots or logs after sanitization. Real GUI smoke and no-runtime smoke remain pending until performed on the published executable. + +## 5. Tag and draft the GitHub release + +The tag workflow on `v*` validates that the tag matches the application version and that `CHANGELOG.md` contains a dated release heading before it restores, tests, builds, packages, or creates a draft. It then reruns tests and build, packages the portable ZIP, and creates an **unpublished draft** release with GitHub CLI equivalent to: + +```powershell +gh release create v0.1.0 artifacts/FileFlow-v0.1.0-win-x64.zip --draft --verify-tag --title "FileFlow v0.1.0" --notes-file CHANGELOG.md +``` + +Before creating a draft, the workflow lists releases, including drafts, and stops if the tag already exists or the lookup fails. It never replaces an existing asset. Do not publish automatically. + +After all preparation evidence is reviewed, obtain separate explicit approval to create and push the annotated `v0.1.0` tag pointing to the clean, tested release commit. + +## 6. Review and publish + +- Confirm the draft tag, title, changelog notes, and archive name. Record the size and SHA-256 of the exact workflow-built asset being reviewed; a separate local build need not have an identical ZIP hash. +- Download the draft asset, extract all files, and repeat a focused launch plus Copy/Move/preview safety check from the downloaded artifact. +- Confirm the GitHub About description and approved screenshot are correct. +- Obtain separate explicit approval to publish the public GitHub release, then publish the draft only after all required evidence is complete and reviewed. + +## Recovery + +If the tag workflow fails, inspect the existing release or draft and its assets before taking action. Preserve logs and artifacts. After approval, manually remove only a failed **draft** created by the unsuccessful attempt, then rerun the workflow. Never delete a published release as recovery, never replace published assets, and never move or recreate the tag without an explicit reviewed recovery decision. + +## Local preparation verification — 2026-09-10 + +These checks used Windows x64, .NET SDK 8.0.424, and bundled runtime 8.0.30, with Milestone 6 changes uncommitted on top of `53149ef`. They are preparation evidence, not verification of a final tagged build. + +- Release suite: 329 passed, zero failed or skipped. Release solution build: zero warnings or errors. +- Portable publish: `FileFlow-v0.1.0-win-x64.zip`, 72,086,597 bytes (68.75 MiB); 159.93 MiB extracted, 468 files. +- ZIP SHA-256: `9AAFF5E80974E0E91551376764DCB7F816E81AAE7FC37E9867AE2D16E55BE7A4`. +- Extracted archive inspection verified application version/product metadata, self-contained runtime configuration, and exact license/notice hashes. No tests, source trees, PDBs, development documents, or manual-test data were present. +- Packaging rejected an existing ZIP without changing it and stopped after a simulated native publish failure without producing a ZIP. +- Workflow/issue YAML and embedded PowerShell parsed successfully. Local fixtures checked malformed/mismatched tags, Unreleased/invalid dates, and valid dated headings. The existing-release guard stopped on lookup failure and an existing tag. No real release-creation command was executed. +- Still pending: hosted CI, real GUI and no-runtime smoke tests, approved sanitized screenshot, manual About correction, actual changelog release date, and separately approved publication steps above. diff --git a/global.json b/global.json new file mode 100644 index 0000000..55ccd56 --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "8.0.400", + "rollForward": "latestFeature", + "allowPrerelease": false + } +} diff --git a/scripts/Publish-Portable.ps1 b/scripts/Publish-Portable.ps1 new file mode 100644 index 0000000..ed5ff9c --- /dev/null +++ b/scripts/Publish-Portable.ps1 @@ -0,0 +1,76 @@ +#requires -Version 7.0 +[CmdletBinding()] +param() + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest +if (-not $IsWindows) { throw 'FileFlow must be published on Windows.' } + +$repo = Split-Path $PSScriptRoot -Parent +Push-Location $repo +try { + $project = 'src/FileFlow.App/FileFlow.App.csproj' + $version = (& dotnet msbuild $project -nologo -getProperty:Version) -join "`n" + if ($LASTEXITCODE -ne 0) { throw 'Could not read the application version.' } + $version = $version.Trim() + if ($version -notmatch '^\d+\.\d+\.\d+$') { throw "Unexpected application version: $version" } + + $archive = Join-Path $repo "artifacts/FileFlow-v$version-win-x64.zip" + if (Test-Path -LiteralPath $archive) { throw "Archive already exists; preserve it elsewhere before retrying: $archive" } + $staging = Join-Path $repo ('artifacts/staging/' + [guid]::NewGuid().ToString('N')) + New-Item -ItemType Directory -Path $staging | Out-Null + + & dotnet publish $project --configuration Release --runtime win-x64 --self-contained true ` + -p:PublishSingleFile=false -p:PublishTrimmed=false -p:DebugType=None -p:DebugSymbols=false ` + --output $staging --nologo + if ($LASTEXITCODE -ne 0) { throw "Publish failed. Staging retained for inspection: $staging" } + + # Use the published runtime versions and NuGet's recorded package roots, not machine-specific paths. + $runtime = Get-Content (Join-Path $staging 'FileFlow.App.runtimeconfig.json') -Raw | ConvertFrom-Json + $assets = Get-Content 'src/FileFlow.App/obj/project.assets.json' -Raw | ConvertFrom-Json + $frameworks = @($runtime.runtimeOptions.includedFrameworks) + foreach ($name in @('Microsoft.NETCore.App', 'Microsoft.WindowsDesktop.App')) { + if ($name -notin $frameworks.name) { throw "Self-contained runtime missing: $name" } + } + Copy-Item -LiteralPath (Join-Path $repo 'LICENSE') -Destination (Join-Path $staging 'LICENSE') + foreach ($framework in $frameworks) { + $package = ($framework.name + '.Runtime.win-x64').ToLowerInvariant() + $pack = $assets.packageFolders.PSObject.Properties.Name | + ForEach-Object { Join-Path $_ "$package/$($framework.version)" } | + Where-Object { Test-Path -LiteralPath $_ -PathType Container } | Select-Object -First 1 + if (-not $pack) { throw "Runtime license package not found: $package/$($framework.version). Stop and review; do not guess." } + $notices = @(Get-ChildItem -LiteralPath $pack -File | + Where-Object { $_.Name -in @('LICENSE', 'LICENSE.TXT', 'THIRD-PARTY-NOTICES.TXT') }) + if (-not ($notices.Name -match '^LICENSE(\.TXT)?$')) { throw "Runtime license missing in $pack" } + $noticeDirectory = Join-Path $staging "licenses/$($framework.name)" + New-Item -ItemType Directory -Path $noticeDirectory | Out-Null + $notices | Copy-Item -Destination $noticeDirectory + } + + foreach ($required in @('FileFlow.App.exe', 'FileFlow.App.dll', 'FileFlow.Core.dll', + 'FileFlow.App.deps.json', 'coreclr.dll', 'hostfxr.dll', 'hostpolicy.dll', 'PresentationFramework.dll')) { + if (-not (Test-Path -LiteralPath (Join-Path $staging $required) -PathType Leaf)) { + throw "Required publish file missing: $required" + } + } + $files = @(Get-ChildItem -LiteralPath $staging -File -Recurse -Force) + foreach ($file in $files) { + $relative = [IO.Path]::GetRelativePath($staging, $file.FullName) + if ($relative -match '(^|[\\/])(tests?|src|Source|Destination|obj|docs|\.git)([\\/]|$)' -or + $file.Name -match '(?i)(\.pdb$|\.cs$|\.xaml$|\.csproj$|\.sln$|\.md$|xunit|testhost|FileFlow\..*Tests)') { + throw "Unexpected publish content: $relative" + } + } + + # ZipFile also refuses an existing destination if another publisher created it during our publish. + [IO.Compression.ZipFile]::CreateFromDirectory($staging, $archive, [IO.Compression.CompressionLevel]::Optimal, $false) + + [pscustomobject]@{ + Archive = $archive + Staging = $staging + Files = $files.Count + ZipMiB = [math]::Round((Get-Item -LiteralPath $archive).Length / 1MB, 2) + ExpandedMiB = [math]::Round(($files | Measure-Object Length -Sum).Sum / 1MB, 2) + } +} +finally { Pop-Location } diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 0000000..527a9dd --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,9 @@ + + + 0.1.0 + 0.1.0.0 + 0.1.0.0 + FileFlow + A local Windows file automation tool that previews planned Move and Copy operations before execution. + +