-
Notifications
You must be signed in to change notification settings - Fork 204
feat: add Docker Cache cleaning category (#1) #36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -33,6 +33,8 @@ actor ScanEngine { | |||||
| return scanBrewCache() | ||||||
| case .nodeCache: | ||||||
| return scanNodeCache() | ||||||
| case .dockerCache: | ||||||
| return scanDockerCache() | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
|
|
@@ -456,6 +458,119 @@ actor ScanEngine { | |||||
| return String(data: data, encoding: .utf8) | ||||||
| } | ||||||
|
|
||||||
| private func scanDockerCache() -> CategoryResult { | ||||||
| var items: [CleanableItem] = [] | ||||||
|
|
||||||
| // Docker Desktop on macOS keeps its VM disk + caches under | ||||||
| // ~/Library/Containers/com.docker.docker/Data. The caches we | ||||||
| // surface here are *recoverable* — they will be regenerated by | ||||||
| // Docker on next pull/build, and `docker system prune` is the | ||||||
| // CLI equivalent of cleaning them. | ||||||
| let dockerDataDirs = [ | ||||||
| // Build cache (BuildKit), per-user | ||||||
| "\(home)/Library/Containers/com.docker.docker/Data/cache", | ||||||
| // Vmnetd / vpnkit log + telemetry caches | ||||||
| "\(home)/Library/Containers/com.docker.docker/Data/log", | ||||||
| "\(home)/Library/Containers/com.docker.docker/Data/tmp", | ||||||
| // Group containers caches (Docker Desktop helper apps) | ||||||
| "\(home)/Library/Group Containers/group.com.docker/Caches", | ||||||
| // CLI plugin download cache | ||||||
| "\(home)/.docker/cli-plugins/.cache", | ||||||
| // Buildx / containerd inline cache | ||||||
| "\(home)/.docker/buildx/cache", | ||||||
| ] | ||||||
|
|
||||||
| for path in dockerDataDirs { | ||||||
| guard fileManager.fileExists(atPath: path) else { continue } | ||||||
| let size = directorySize(path: path) | ||||||
| guard size > 0 else { continue } | ||||||
| items.append(CleanableItem( | ||||||
| name: URL(fileURLWithPath: path).lastPathComponent, | ||||||
| path: path, | ||||||
| size: size, | ||||||
| category: .dockerCache, | ||||||
| isSelected: true, | ||||||
| lastModified: nil | ||||||
| )) | ||||||
| } | ||||||
|
|
||||||
| // If the `docker` CLI is available, surface reclaimable space | ||||||
| // reported by `docker system df` as a single virtual entry. | ||||||
| // We don't try to delete it directly — the user runs | ||||||
| // `docker system prune` themselves, which is the safe path. | ||||||
| // We just show how much they can recover. | ||||||
| let dockerBinPaths = ["/usr/local/bin/docker", "/opt/homebrew/bin/docker"] | ||||||
| for dockerBin in dockerBinPaths where fileManager.fileExists(atPath: dockerBin) { | ||||||
| if let reclaimable = reclaimableDockerSpace(dockerBin: dockerBin), reclaimable > 0 { | ||||||
| items.append(CleanableItem( | ||||||
| name: "Reclaimable (run `docker system prune -af`)", | ||||||
|
||||||
| name: "Reclaimable (run `docker system prune -af`)", | |
| name: String(localized: "Reclaimable (run `docker system prune -af`)"), |
Copilot
AI
Apr 18, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The “Reclaimable …” virtual item uses the docker executable path as its path. In the current cleaning flow, selected items are deleted by item.path (with a safety allowlist), so if this row ends up selected it will attempt to delete /usr/local/bin/docker or /opt/homebrew/bin/docker and then surface a “Skipped … unsafe path” error. This should be modeled as a non-deletable/virtual item (e.g., add an isVirtual/action field and have the UI/cleaner skip it), and avoid using an actual filesystem path that implies it can be removed.
Copilot
AI
Apr 18, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isSelected: false on the reclaimable-space row won’t have the intended effect with the current selection model: AppState defaults to “selected unless in deselectedItems” and clears deselectedItems on every scan, without initializing it from CleanableItem.isSelected. As a result this new virtual row will appear selected by default (and may be included in “Select All” flows). Either wire CleanableItem.isSelected into deselectedItems initialization when storing scan results, or remove isSelected from scan outputs and manage selection consistently in one place.
Copilot
AI
Apr 18, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The item name includes backticks (`docker system prune -af`). SwiftUI Text doesn’t render Markdown, so these backticks will show up literally in the UI. Consider removing the backticks (or presenting the command in a separate UI affordance like a copy button) to avoid confusing display text.
Copilot
AI
Apr 18, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doc comment mismatch: this function says it parses output from docker system df --format json, but the code runs docker system df --format "{{.Reclaimable}}". Update the comment (or the command) so future maintainers don’t assume JSON parsing here.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -123,3 +123,8 @@ | |
| "npm cache" = "npm cache"; | ||
| "yarn classic cache" = "yarn classic cache"; | ||
| "pnpm content-addressable store" = "pnpm content-addressable store"; | ||
|
|
||
| /* Docker Cache */ | ||
| "Docker Cache" = "Docker Cache"; | ||
| "Docker images, containers, and build cache" = "Docker images, containers, and build cache"; | ||
| "Reclaimable (run `docker system prune -af`)" = "Reclaimable (run `docker system prune -af`)"; | ||
|
Comment on lines
+127
to
+130
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -123,3 +123,8 @@ | |||||
| "npm cache" = "npm 缓存"; | ||||||
| "yarn classic cache" = "yarn 经典缓存"; | ||||||
| "pnpm content-addressable store" = "pnpm 内容寻址存储"; | ||||||
|
|
||||||
| /* Docker Cache */ | ||||||
| "Docker Cache" = "Docker 缓存"; | ||||||
| "Docker images, containers, and build cache" = "Docker 镜像、容器和构建缓存"; | ||||||
| "Reclaimable (run `docker system prune -af`)" = "可回收空间(运行 `docker system prune -af`)"; | ||||||
|
||||||
| "Reclaimable (run `docker system prune -af`)" = "可回收空间(运行 `docker system prune -af`)"; | |
| "Reclaimable (run docker system prune -af)" = "可回收空间(运行 docker system prune -af)"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PR description says the CLI check runs when
dockeris “on PATH”, but the implementation only checks two hard-coded locations (/usr/local/bin/docker,/opt/homebrew/bin/docker). If Docker is installed elsewhere (e.g., symlinked into a different directory), reclaimable space won’t be surfaced. Consider resolving viawhich docker//usr/bin/env docker(while still keeping the allowlist if desired).