Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ All notable changes to DiskSage are documented in this file.
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and released versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Unreleased entries describe integrated source changes only; they are not release evidence until the repository's review, CI, security, packaging, provenance, and release-acceptance gates pass on the exact tagged commit.

## [Unreleased]
- Make Apple Photos duplicate inventory resumable and cancellable between native-completed assets,
with visible progress and no arbitrary whole-library timeout.
- Connect exact decoded-pixel duplicate groups to the existing reversible photo quarantine engine;
forged or stale audits, ambiguous keepers without an explicit selection, and changed roots fail closed.
The review screen shows measured evidence and blockers, requires direct typed approval, and reports
Expand Down Expand Up @@ -315,6 +317,9 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
- Restored the cloud-copy public documentation regression contract after a temporary repair path removed it, so CI continues to fail when the new Rust or TypeScript approval surfaces lose beginner-readable documentation.
- Align release artifact verification with the pinned `windows-2022` build matrix name, and make the container-capacity regression fixture satisfy the same runtime-health probe required in production.
- Require standalone-clone cleanup to bind a real in-root Git directory, complete audit evidence, and an external safe journal before an approved Trash move.
- Add a native Apple Photos duplicate workflow that preserves iCloud-only originals, groups exact
local content, requires an explicit keeper and fresh approval, revalidates PhotoKit identifiers,
and delegates deletion confirmation and Recently Deleted behavior to Photos.

### Security

Expand Down
59 changes: 59 additions & 0 deletions docs/architecture/adr/0023-apple-photos-photokit-boundary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# ADR 0023: Use PhotoKit rather than Photos library package traversal

Status: Accepted

## Context

Apple Photos libraries can include locally materialized originals and iCloud-only assets. Treating
the `.photoslibrary` package as an ordinary directory bypasses Photos authorization, relationship,
change-notification, and system deletion semantics. A file path is therefore not deletion authority.

## Decision

DiskSage uses a macOS-native PhotoKit boundary and never descends into or mutates a managed Photos
library package. Read/write authorization is requested only after the customer selects **Connect
Photos**. Inventory is bounded to 10,000 image assets and 512 MiB per locally available original;
resource requests disable network access, so iCloud-only originals remain unmaterialized and block
all destructive planning.

Exact groups require SHA-256 content identity. Width, height, pixel count, encoded bytes, resource
type, and UTI remain separate measured evidence; no composite score or arbitrary weight chooses a
keeper. Near-duplicate deletion remains unavailable until measured equivalence evidence exists.
The customer must select one keeper per exact group, enter the fresh exact approval phrase and a
rationale, and then accept Photos' own deletion confirmation. Immediately before the change,
DiskSage re-fetches local identifiers and re-reads local content without network access. The change
uses `PHPhotoLibrary.performChanges`/`PHAssetChangeRequest.deleteAssets`; an immutable, create-new
receipt records only successful completion. Non-macOS builds fail closed.

## Consequences

- iCloud-only assets are preserved without implicit download.
- Changes made by Photos, another device, or another app invalidate the reviewed evidence at the
identifier, metadata, or content recheck.
- Deleted assets follow Photos' Recently Deleted and system-confirmation behavior; DiskSage never
directly unlinks a managed original.
- Limited Photos access inventories only the assets Apple exposes, and the UI states the next action.

## Rejected alternatives

- Direct `.photoslibrary` traversal or deletion: bypasses PhotoKit authority and can corrupt the library.
- Filesystem Trash for managed originals: bypasses Photos' change transaction and confirmation.
- Automatically downloading iCloud originals: creates disk pressure and expands mutation scope.
- Weighted “best photo” scoring: no calibrated model supports an arbitrary cross-metric weight.

## References

Apple. (n.d.). *Delivering an enhanced privacy experience in your Photos app*. Apple Developer.
https://developer.apple.com/documentation/photokit/delivering-an-enhanced-privacy-experience-in-your-photos-app

Apple. (n.d.). *Observing changes in the photo library*. Apple Developer.
https://developer.apple.com/documentation/photokit/observing-changes-in-the-photo-library

Apple. (n.d.). *PHAssetChangeRequest*. Apple Developer.
https://developer.apple.com/documentation/photos/phassetchangerequest

Apple. (n.d.). *PHPhotoLibrary*. Apple Developer.
https://developer.apple.com/documentation/photos/phphotolibrary

Apple. (n.d.). *Requesting changes to the photo library*. Apple Developer.
https://developer.apple.com/documentation/photokit/requesting-changes-to-the-photo-library
38 changes: 38 additions & 0 deletions docs/architecture/adr/0024-photokit-checkpointed-inventory.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# ADR-0024: Checkpoint PhotoKit inventory at native completion boundaries

- Status: Accepted
- Date: 2026-08-30

## Context

Reading locally available originals can take materially different time per asset. A single large
PhotoKit request kept the customer waiting without progress or cancellation, while an arbitrary
wall-clock timeout discarded completed evidence.

## Decision

DiskSage requests one `PHAsset` per native page. A page is accepted only after PhotoKit's resource
completion handler, and records its measured duration rather than using it as a guessed cutoff.
Rust rejects gaps, repeated offsets, missing completion evidence, and inconsistent totals. The UI
checkpoints after every page, yields for rendering, and stops between pages when requested. A
checkpoint is resumable; it never authorizes deletion. Network access remains disabled and no
PhotoKit change request is made during inventory.

## Consequences

Large libraries take as long as their locally available originals require, but the customer sees
progress, may stop safely, and can resume without repeating accepted pages. Destructive planning
still requires a complete inventory and the existing fresh re-fetch and exact approval contract.

## Rejected alternatives

An arbitrary whole-library timeout was rejected because elapsed wall time is not evidence that
PhotoKit failed. Large fixed pages were rejected because they cannot yield promptly between assets.

## References

Apple. (n.d.). *PHAssetResourceManager*. Apple Developer Documentation.
https://developer.apple.com/documentation/photokit/phassetresourcemanager

Apple. (n.d.). *Fetching and caching assets and thumbnails*. Apple Developer Documentation.
https://developer.apple.com/documentation/photokit/fetching-and-caching-assets-and-thumbnails
2 changes: 2 additions & 0 deletions docs/architecture/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ new numbered record rather than rewriting history.
| [0020](0020-podman-native-storage-repair.md) | Use machine-scoped native Podman storage repair without force | Accepted |
| [0021](0021-perceptual-photo-candidates.md) | Require measured evidence and a selected survivor for perceptual photo candidates | Accepted |
| [0022](0022-photo-duplicate-evidence-without-composite-scoring.md) | Separate photo-duplicate and keeper evidence without composite scoring | Accepted |
| [0023](0023-apple-photos-photokit-boundary.md) | Use PhotoKit rather than Photos library package traversal | Accepted |
| [0024](0024-photokit-checkpointed-inventory.md) | Checkpoint PhotoKit inventory at native completion boundaries | Accepted |

New records must state context, decision, consequences, rejected alternatives, and the evidence or
standard that led to the decision. A record never grants cloud-write or source-eviction authority;
Expand Down
4 changes: 4 additions & 0 deletions docs/doctoring/perceptual-photo-evidence.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ placeholders are rejected before decoding so an audit cannot hydrate them. Execu
the same report, checks active use and exact filesystem identity, then uses DiskSage's existing atomic
OS-Trash boundary and append-only journal. There is no permanent-delete mode.

Apple Photos libraries use the separate PhotoKit boundary documented in ADR 0023. PhotoKit asset
identifiers and resource reads replace package paths; network access is disabled during evidence
collection, so an iCloud-only original is neither downloaded nor admitted to a deletion plan.

## References

pHash. (2010). *pHash design*. https://www.phash.org/docs/design.html
Expand Down
Binary file added docs/evidence/photos-library-review.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions docs/product-technical-gap-baseline.md
Original file line number Diff line number Diff line change
Expand Up @@ -1228,6 +1228,20 @@ runner's private workspace temp root instead of weakening the shared production
cannot form a deletion cluster with an external file. The 44 external Pictures images currently
have unique exact-content digests; perceptual comparison and measured quality-survivor selection
remain an open product Gap and no non-identical photo was deleted.
- The managed Apple Photos gap now has a separate macOS-native PhotoKit path. It requests read/write
access only from the customer's connect action, inventories local identifiers and measured
resource evidence without allowing network download, groups only exact SHA-256 content matches,
and requires one explicit keeper per group. iCloud-only originals block deletion planning and
remain unmaterialized. Execution re-fetches every identifier, metadata fingerprint, and local
content digest before invoking Photos' own deletion transaction and confirmation; a create-new
receipt follows success. Near-duplicate managed assets remain unavailable rather than receiving
an uncalibrated score, so that Gap is explicit and non-destructive.
- A 5,000-item read-only audit did not return customer-visible progress before an external
60-second observation was stopped. The native bridge now returns exactly one asset only after
PhotoKit's completion callback; Rust persists gap-free checkpoint evidence, and the UI renders
progress and accepts cancellation between completed assets. Resuming uses the accepted
checkpoint instead of restarting. No elapsed-time threshold is interpreted as failure and no
photo mutation is introduced by this audit path.

- A fresh Naruon audit proved exactly one removable worktree: PR #1429 was merged, its detached
head was retained by current `origin/develop`, the checkout was clean and inactive, and no open
Expand Down
1 change: 1 addition & 0 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ required-features = ["archive-cli"]

[build-dependencies]
tauri-build = { version = "2", features = [] }
cc = "1"

[dependencies]
tauri = { version = "2", features = [] }
Expand Down
2 changes: 2 additions & 0 deletions src-tauri/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@
<string>DiskSage는 클라우드 보관 계획을 만들기 전에 Downloads 파일의 메타데이터와 크기를 읽습니다.</string>
<key>NSFileProviderDomainUsageDescription</key>
<string>DiskSage는 선택한 iCloud, OneDrive, Google Drive 대상의 접근 및 동기화 상태를 검증합니다.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>DiskSage는 사진 앱에서 정확히 같은 사본을 찾고, 사용자가 선택한 사본만 사진 앱의 확인을 거쳐 삭제합니다.</string>
</dict>
</plist>
10 changes: 10 additions & 0 deletions src-tauri/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,15 @@ fn generate_cloud_plan_implementation() {

fn main() {
generate_cloud_plan_implementation();
#[cfg(target_os = "macos")]
{
println!("cargo:rerun-if-changed=native/photos_bridge.m");
cc::Build::new()
.file("native/photos_bridge.m")
.flag("-fobjc-arc")
.compile("disksage_photos_bridge");
println!("cargo:rustc-link-lib=framework=Photos");
println!("cargo:rustc-link-lib=framework=Foundation");
}
tauri_build::build()
}
Loading