Skip to content
Open
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
14 changes: 14 additions & 0 deletions Sources/ContainerPersistence/PathUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,18 @@ public enum PathUtils {
}
}
}

/// Marks an existing directory so Time Machine skips it.
///
/// The exclusion is the sticky item attribute, not a path-list entry, and
/// applying it again is safe. Call it whenever the directory is ensured so an
/// install that already has the directory is excluded on the next start. Use it
/// for data that can be recreated, such as image snapshots and container root
/// filesystems, not for volume data.
public static func excludeFromBackup(_ url: URL) throws {
var values = URLResourceValues()
values.isExcludedFromBackup = true
var mutableURL = url
try mutableURL.setResourceValues(values)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ public actor ContainersService {
) throws {
let containerRoot = appRoot.appendingPathComponent("containers")
try FileManager.default.createDirectory(at: containerRoot, withIntermediateDirectories: true)
try PathUtils.excludeFromBackup(containerRoot)
self.exitMonitor = ExitMonitor(log: log)
self.lock = AsyncLock(log: log)
self.containerRoot = containerRoot
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
//===----------------------------------------------------------------------===//

import ContainerAPIClient
import ContainerPersistence
import ContainerResource
import Containerization
import ContainerizationEXT4
Expand Down Expand Up @@ -63,6 +64,7 @@ public actor SnapshotStore {
self.unpackStrategy = unpackStrategy
self.log = log
try self.fm.createDirectory(at: root, withIntermediateDirectories: true)
try PathUtils.excludeFromBackup(root)
try self.fm.createDirectory(at: self.ingestDir, withIntermediateDirectories: true)
}

Expand Down
25 changes: 25 additions & 0 deletions Tests/ContainerPersistenceTests/PathUtilsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,29 @@ struct PathUtilsTests {
let path = PathUtils.BaseConfigPath.installRoot.basePath(env: ["CONTAINER_INSTALL_ROOT": "/usr/local"])
#expect(path == FilePath("/usr/local"))
}

@Test func testExcludeFromBackupMarksDirectoryAndLeavesSiblingAlone() throws {
let parent = FileManager.default.temporaryDirectory
.appendingPathComponent("container-backup-exclusion-\(UUID().uuidString)", isDirectory: true)
let snapshots = parent.appendingPathComponent("snapshots", isDirectory: true)
let volumes = parent.appendingPathComponent("volumes", isDirectory: true)
try FileManager.default.createDirectory(at: snapshots, withIntermediateDirectories: true)
try FileManager.default.createDirectory(at: volumes, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: parent) }

try PathUtils.excludeFromBackup(snapshots)
try PathUtils.excludeFromBackup(snapshots)
FileManager.default.createFile(
atPath: snapshots.appendingPathComponent("snapshot").path,
contents: Data("sparse".utf8)
)

#expect(try Self.isExcludedFromBackup(snapshots))
#expect(try !Self.isExcludedFromBackup(volumes))
}

private static func isExcludedFromBackup(_ url: URL) throws -> Bool {
let values = try url.resourceValues(forKeys: [.isExcludedFromBackupKey])
return values.isExcludedFromBackup == true
}
}
4 changes: 4 additions & 0 deletions docs/resource-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ data, these images grow; when a container process deletes a file, the freed bloc
aren't automatically returned to the host filesystem, so image size doesn't shrink on
its own.

Image snapshots and container root filesystems are excluded from Time Machine.
Both are large sparse files that can be recreated by pulling the image or creating
the container again. Named volume data under `volumes/` stays in backups.

Check overall usage with:

```bash
Expand Down
Loading