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

/// Creates `url` and excludes that new directory from backups.
///
/// Backup software that reads the item's exclusion attribute, including Time Machine,
/// skips the directory. The attribute is set only when this call creates `url`.
/// An existing directory is left unchanged, so a person who includes it in backups
/// again keeps that choice across later starts. Use this for data that can be
/// recreated, such as image snapshots and container root filesystems, not for volume data.
public static func createDirectoryExcludingFromBackup(_ url: URL, fileManager: FileManager = .default) throws {
let alreadyExists = fileManager.fileExists(atPath: url.path)
try fileManager.createDirectory(at: url, withIntermediateDirectories: true)
guard !alreadyExists else {
return
}
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 @@ -73,7 +73,7 @@ public actor ContainersService {
debugHelpers: Bool = false
) throws {
let containerRoot = appRoot.appendingPathComponent("containers")
try FileManager.default.createDirectory(at: containerRoot, withIntermediateDirectories: true)
try PathUtils.createDirectoryExcludingFromBackup(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 @@ -62,7 +63,7 @@ public actor SnapshotStore {
self.ingestDir = self.path.appendingPathComponent(Self.ingestDirName)
self.unpackStrategy = unpackStrategy
self.log = log
try self.fm.createDirectory(at: root, withIntermediateDirectories: true)
try PathUtils.createDirectoryExcludingFromBackup(root, fileManager: self.fm)
try self.fm.createDirectory(at: self.ingestDir, withIntermediateDirectories: true)
}

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

@Test func testNewDirectoryIsExcludedAndExistingChoiceIsKept() 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)
defer { try? FileManager.default.removeItem(at: parent) }

try PathUtils.createDirectoryExcludingFromBackup(snapshots)
try FileManager.default.createDirectory(at: volumes, withIntermediateDirectories: true)
FileManager.default.createFile(
atPath: snapshots.appendingPathComponent("snapshot").path,
contents: Data("sparse".utf8)
)
#expect(try Self.isExcludedFromBackup(snapshots))
#expect(try !Self.isExcludedFromBackup(volumes))

var included = URLResourceValues()
included.isExcludedFromBackup = false
var mutableSnapshots = snapshots
try mutableSnapshots.setResourceValues(included)
try PathUtils.createDirectoryExcludingFromBackup(snapshots)
#expect(try !Self.isExcludedFromBackup(snapshots))
}

private static func isExcludedFromBackup(_ url: URL) throws -> Bool {
let values = try url.resourceValues(forKeys: [.isExcludedFromBackupKey])
return values.isExcludedFromBackup == true
}
}
7 changes: 7 additions & 0 deletions docs/resource-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,13 @@ 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 backups when
those directories are first created. Backup software that honors this exclusion,
including Time Machine, skips them. Both are large sparse files that can be
recreated by pulling the image or creating the container again. Named volume data
under `volumes/` stays included. An exclusion removed from an existing directory
is left as it is.

Check overall usage with:

```bash
Expand Down
Loading