Files
xtool-mirror/Sources/XUtils/TemporaryDirectory.swift
Kabir Oberai 836fca8daf Improve tmpdir management (#147)
- We now have a single tmpdir "root" that can be recreated at launch to
clean up old stragglers
- The location of this tmpdir root can be controlled by `XTL_TMPDIR` or
`TMPDIR`. In general the path is `$TMPDIR/sh.xtool`.

With this change it should be possible to `export XTL_TMPDIR=/var/tmp`
if `/tmp` doesn't have enough space, which fixes #23.
2025-08-10 01:43:09 -04:00

98 lines
3.1 KiB
Swift

import Foundation
package struct TemporaryDirectory: ~Copyable {
private static let debugTmp = ProcessInfo.processInfo.environment["XTL_DEBUG_TMP"] != nil
private var shouldDelete: Bool
package let url: URL
/// Prepares a fresh tmpdir root.
///
/// Optional, but try calling this at launch to clean up old resources.
package static func prepare() {
_ = TemporaryDirectoryRoot.shared
}
/// Creates a temporary directory where `lastPathComponent` is exactly `name`.
///
/// The directory is deleted on deinit or (if the object never deinits) on next launch.
/// To save the contents, move them elsewhere with ``persist(at:)``.
package init(name: String) throws {
do {
let basename = name.replacingOccurrences(of: ".", with: "_")
self.url = try TemporaryDirectoryRoot.shared.url
// ensures uniqueness
.appendingPathComponent("tmp-\(basename)-\(UUID().uuidString)")
.appendingPathComponent(name, isDirectory: true)
self.shouldDelete = true
} catch {
// non-copyable types can't be partially initialized so we need a stub value
self.url = URL(fileURLWithPath: "")
self.shouldDelete = false
throw error
}
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
if Self.debugTmp {
stderrPrint("Created TemporaryDirectory: \(url.path)")
}
}
private func _delete() {
guard !Self.debugTmp else { return }
try? FileManager.default.removeItem(at: url)
}
package consuming func persist(at location: URL) throws {
try FileManager.default.moveItem(at: url, to: location)
// we do this after moving, so that if the move fails we clean up
shouldDelete = false
}
deinit {
if shouldDelete { _delete() }
}
}
private struct TemporaryDirectoryRoot {
static let shared = TemporaryDirectoryRoot()
private let _url: Result<URL, Errors>
var url: URL {
get throws(Errors) {
try _url.get()
}
}
private init() {
let base: URL
let env = ProcessInfo.processInfo.environment
if let tmpdir = env["XTL_TMPDIR"] ?? env["TMPDIR"] {
base = URL(fileURLWithPath: tmpdir)
} else {
base = FileManager.default.temporaryDirectory
}
let url = base.appendingPathComponent("sh.xtool")
try? FileManager.default.removeItem(at: url)
do {
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
} catch {
self._url = .failure(Errors.tmpdirCreationFailed(url, error))
return
}
self._url = .success(url)
}
enum Errors: Error, CustomStringConvertible {
case tmpdirCreationFailed(URL, Error)
var description: String {
switch self {
case let .tmpdirCreationFailed(url, error):
"Could not create temporary directory at '\(url.path)': \(error)"
}
}
}
}