-
Notifications
You must be signed in to change notification settings - Fork 119
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduce "tart prune" command (#164)
- Loading branch information
Showing
13 changed files
with
243 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
import ArgumentParser | ||
import Dispatch | ||
import SwiftUI | ||
import SwiftDate | ||
|
||
struct Prune: AsyncParsableCommand { | ||
static var configuration = CommandConfiguration(abstract: "Prune OCI and IPSW caches") | ||
|
||
@Option(help: ArgumentHelp("Remove cache entries last accessed more than n days ago", | ||
discussion: "For example, --older-than=7 will remove entries that weren't accessed by Tart in the last 7 days.", | ||
valueName: "n")) | ||
var olderThan: UInt? | ||
|
||
@Option(help: ArgumentHelp("Remove least recently used cache entries that do not fit the specified cache size budget n, expressed in gigabytes", | ||
discussion: "For example, --cache-budget=50 will effectively shrink all caches to a total size of 50 gigabytes.", | ||
valueName: "n")) | ||
var cacheBudget: UInt? | ||
|
||
func validate() throws { | ||
if olderThan == nil && cacheBudget == nil { | ||
throw ValidationError("at least one criteria must be specified") | ||
} | ||
} | ||
|
||
func run() async throws { | ||
do { | ||
// Clean up cache entries based on last accessed date | ||
if let olderThan = olderThan { | ||
let olderThanInterval = Int(exactly: olderThan)!.days.timeInterval | ||
let olderThanDate = Date().addingTimeInterval(olderThanInterval) | ||
|
||
try Prune.pruneOlderThan(olderThanDate: olderThanDate) | ||
} | ||
|
||
// Clean up cache entries based on imposed cache size limit and entry's last accessed date | ||
if let cacheBudget = cacheBudget { | ||
try Prune.pruneCacheBudget(cacheBudgetBytes: UInt64(cacheBudget) * 1024 * 1024 * 1024) | ||
} | ||
|
||
Foundation.exit(0) | ||
} catch { | ||
print(error) | ||
|
||
Foundation.exit(1) | ||
} | ||
} | ||
|
||
static func pruneOlderThan(olderThanDate: Date) throws { | ||
let prunableStorages: [PrunableStorage] = [VMStorageOCI(), try IPSWCache()] | ||
let prunables: [Prunable] = try prunableStorages.flatMap { try $0.prunables() } | ||
|
||
try prunables.filter { try $0.accessDate() <= olderThanDate }.forEach { try $0.delete() } | ||
} | ||
|
||
static func pruneCacheBudget(cacheBudgetBytes: UInt64) throws { | ||
let prunableStorages: [PrunableStorage] = [VMStorageOCI(), try IPSWCache()] | ||
let prunables: [Prunable] = try prunableStorages | ||
.flatMap { try $0.prunables() } | ||
.sorted { try $0.accessDate() < $1.accessDate() } | ||
|
||
let cacheUsedBytes = try prunables.map { try $0.sizeBytes() }.reduce(0, +) | ||
var cacheReclaimedBytes: Int = 0 | ||
|
||
var it = prunables.makeIterator() | ||
|
||
while (cacheUsedBytes - cacheReclaimedBytes) > cacheBudgetBytes { | ||
guard let prunable = it.next() else { | ||
break | ||
} | ||
|
||
cacheReclaimedBytes -= try prunable.sizeBytes() | ||
try prunable.delete() | ||
} | ||
} | ||
|
||
static func pruneReclaim(reclaimBytes: UInt64) throws { | ||
let prunableStorages: [PrunableStorage] = [VMStorageOCI(), try IPSWCache()] | ||
let prunables: [Prunable] = try prunableStorages | ||
.flatMap { try $0.prunables() } | ||
.sorted { try $0.accessDate() < $1.accessDate() } | ||
|
||
// Does it even make sense to start? | ||
let cacheUsedBytes = try prunables.map { try $0.sizeBytes() }.reduce(0, +) | ||
if cacheUsedBytes < reclaimBytes { | ||
return | ||
} | ||
|
||
var cacheReclaimedBytes: Int = 0 | ||
|
||
var it = prunables.makeIterator() | ||
|
||
while cacheReclaimedBytes <= reclaimBytes { | ||
guard let prunable = it.next() else { | ||
break | ||
} | ||
|
||
cacheReclaimedBytes -= try prunable.sizeBytes() | ||
try prunable.delete() | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import Foundation | ||
import Virtualization | ||
|
||
class IPSWCache: PrunableStorage { | ||
let baseURL: URL | ||
|
||
init() throws { | ||
baseURL = Config().tartCacheDir.appendingPathComponent("IPSWs", isDirectory: true) | ||
try FileManager.default.createDirectory(at: baseURL, withIntermediateDirectories: true) | ||
} | ||
|
||
func locationFor(image: VZMacOSRestoreImage) -> URL { | ||
baseURL.appendingPathComponent("\(image.buildVersion).ipsw", isDirectory: false) | ||
} | ||
|
||
func prunables() throws -> [Prunable] { | ||
try FileManager.default.contentsOfDirectory(at: baseURL, includingPropertiesForKeys: nil) | ||
.filter { $0.lastPathComponent.hasSuffix(".ipsw")} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import Foundation | ||
|
||
protocol PrunableStorage { | ||
func prunables() throws -> [Prunable] | ||
} | ||
|
||
protocol Prunable { | ||
func delete() throws | ||
func accessDate() throws -> Date | ||
func sizeBytes() throws -> Int | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,6 +16,7 @@ struct Root: AsyncParsableCommand { | |
IP.self, | ||
Pull.self, | ||
Push.self, | ||
Prune.self, | ||
Delete.self, | ||
]) | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import Foundation | ||
|
||
extension URL { | ||
func accessDate() throws -> Date { | ||
let attrs = try resourceValues(forKeys: [.contentAccessDateKey]) | ||
return attrs.contentAccessDate! | ||
} | ||
|
||
func updateAccessDate(_ accessDate: Date = Date()) throws { | ||
let attrs = try resourceValues(forKeys: [.contentAccessDateKey]) | ||
let modificationDate = attrs.contentAccessDate! | ||
|
||
let times = [accessDate.asTimeval(), modificationDate.asTimeval()] | ||
let ret = utimes(path, times) | ||
if ret != 0 { | ||
throw RuntimeError("utimes(2) failed: \(ret.explanation())") | ||
} | ||
} | ||
} | ||
|
||
extension Date { | ||
func asTimeval() -> timeval { | ||
timeval(tv_sec: timeIntervalSince1970.toUnit(.second)!, tv_usec: 0) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import Foundation | ||
|
||
extension URL: Prunable { | ||
func delete() throws { | ||
try FileManager.default.removeItem(at: self) | ||
} | ||
|
||
func sizeBytes() throws -> Int { | ||
try resourceValues(forKeys: [.totalFileAllocatedSizeKey]).totalFileAllocatedSize! | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import XCTest | ||
@testable import tart | ||
|
||
final class URLAccessDateTests: XCTestCase { | ||
func testGetAndSetAccessTime() throws { | ||
// Create a temporary file | ||
let tmpDir = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) | ||
var tmpFile = tmpDir.appendingPathComponent(UUID().uuidString) | ||
FileManager.default.createFile(atPath: tmpFile.path, contents: nil) | ||
|
||
// Ensure it's access date is different than our desired access date | ||
let arbitraryDate = Date.init(year: 2008, month: 09, day: 28, hour: 23, minute: 15) | ||
XCTAssertNotEqual(arbitraryDate, try tmpFile.accessDate()) | ||
|
||
// Set our desired access date for a file | ||
try tmpFile.updateAccessDate(arbitraryDate) | ||
|
||
// Ensure the access date has changed to our value | ||
tmpFile.removeCachedResourceValue(forKey: .contentAccessDateKey) | ||
XCTAssertEqual(arbitraryDate, try tmpFile.accessDate()) | ||
} | ||
} |