diff --git a/Sources/TSCBasic/TemporaryFile.swift b/Sources/TSCBasic/TemporaryFile.swift index 94843f58..76cddf75 100644 --- a/Sources/TSCBasic/TemporaryFile.swift +++ b/Sources/TSCBasic/TemporaryFile.swift @@ -141,6 +141,39 @@ public func withTemporaryFile( } } +/// Creates a temporary file and evaluates a closure with the temporary file as an argument. +/// The temporary file will live on disk while the closure is evaluated and will be deleted when +/// the cleanup block is called. +/// +/// This function is basically a wrapper over posix's mkstemps() function to create disposable files. +/// +/// - Parameters: +/// - dir: If specified the temporary file will be created in this directory otherwise environment variables +/// TMPDIR, TEMP and TMP will be checked for a value (in that order). If none of the env variables are +/// set, dir will be set to `/tmp/`. +/// - prefix: The prefix to the temporary file name. +/// - suffix: The suffix to the temporary file name. +/// - body: A closure to execute that receives the TemporaryFile as an argument. +/// If `body` has a return value, that value is also used as the +/// return value for the `withTemporaryFile` function. +/// The cleanup block should be called when the temporary file is no longer needed. +/// +/// - Throws: TempFileError and rethrows all errors from `body`. +@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +public func withTemporaryFile( + dir: AbsolutePath? = nil, prefix: String = "TemporaryFile", suffix: String = "", _ body: (TemporaryFile, @escaping (TemporaryFile) async -> Void) async throws -> Result +) async throws -> Result { + return try await body(TemporaryFile(dir: dir, prefix: prefix, suffix: suffix)) { tempFile in +#if os(Windows) + _ = tempFile.path.pathString.withCString(encodedAs: UTF16.self) { + _wunlink($0) + } +#else + unlink(tempFile.path.pathString) +#endif + } +} + /// Creates a temporary file and evaluates a closure with the temporary file as an argument. /// The temporary file will live on disk while the closure is evaluated and will be deleted afterwards. /// @@ -167,6 +200,40 @@ public func withTemporaryFile( } } +/// Creates a temporary file and evaluates a closure with the temporary file as an argument. +/// The temporary file will live on disk while the closure is evaluated and will be deleted afterwards. +/// +/// This function is basically a wrapper over posix's mkstemps() function to create disposable files. +/// +/// - Parameters: +/// - dir: If specified the temporary file will be created in this directory otherwise environment variables +/// TMPDIR, TEMP and TMP will be checked for a value (in that order). If none of the env variables are +/// set, dir will be set to `/tmp/`. +/// - prefix: The prefix to the temporary file name. +/// - suffix: The suffix to the temporary file name. +/// - deleteOnClose: Whether the file should get deleted after the call of `body` +/// - body: A closure to execute that receives the TemporaryFile as an argument. +/// If `body` has a return value, that value is also used as the +/// return value for the `withTemporaryFile` function. +/// +/// - Throws: TempFileError and rethrows all errors from `body`. +@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +public func withTemporaryFile( + dir: AbsolutePath? = nil, prefix: String = "TemporaryFile", suffix: String = "", deleteOnClose: Bool = true, _ body: (TemporaryFile) async throws -> Result +) async throws -> Result { + try await withTemporaryFile(dir: dir, prefix: prefix, suffix: suffix) { tempFile, cleanup in + let result: Result + do { + result = try await body(tempFile) + if (deleteOnClose) { await cleanup(tempFile) } + } catch { + if (deleteOnClose) { await cleanup(tempFile) } + throw error + } + return result + } +} + // FIXME: This isn't right place to declare this, probably POSIX or merge with FileSystemError? // /// Contains the error which can be thrown while creating a directory using POSIX's mkdir. @@ -252,6 +319,44 @@ public func withTemporaryDirectory( } } +/// Creates a temporary directory and evaluates a closure with the directory path as an argument. +/// The temporary directory will live on disk while the closure is evaluated and will be deleted when +/// the cleanup closure is called. This allows the temporary directory to have an arbitrary lifetime. +/// +/// This function is basically a wrapper over posix's mkdtemp() function. +/// +/// - Parameters: +/// - dir: If specified the temporary directory will be created in this directory otherwise environment +/// variables TMPDIR, TEMP and TMP will be checked for a value (in that order). If none of the env +/// variables are set, dir will be set to `/tmp/`. +/// - prefix: The prefix to the temporary file name. +/// - body: A closure to execute that receives the absolute path of the directory as an argument. +/// If `body` has a return value, that value is also used as the +/// return value for the `withTemporaryDirectory` function. +/// The cleanup block should be called when the temporary directory is no longer needed. +/// +/// - Throws: MakeDirectoryError and rethrows all errors from `body`. +@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +public func withTemporaryDirectory( + dir: AbsolutePath? = nil, prefix: String = "TemporaryDirectory" , _ body: (AbsolutePath, @escaping (AbsolutePath) async -> Void) async throws -> Result +) async throws -> Result { + // Construct path to the temporary directory. + let templatePath = try AbsolutePath(validating: prefix + ".XXXXXX", relativeTo: determineTempDirectory(dir)) + + // Convert templatePath to a C style string terminating with null char to be an valid input + // to mkdtemp method. The XXXXXX in this string will be replaced by a random string + // which will be the actual path to the temporary directory. + var template = [UInt8](templatePath.pathString.utf8).map({ Int8($0) }) + [Int8(0)] + + if TSCLibc.mkdtemp(&template) == nil { + throw MakeDirectoryError(errno: errno) + } + + return try await body(AbsolutePath(validating: String(cString: template))) { path in + _ = try? FileManager.default.removeItem(atPath: path.pathString) + } +} + /// Creates a temporary directory and evaluates a closure with the directory path as an argument. /// The temporary directory will live on disk while the closure is evaluated and will be deleted afterwards. /// @@ -277,3 +382,35 @@ public func withTemporaryDirectory( } } +/// Creates a temporary directory and evaluates a closure with the directory path as an argument. +/// The temporary directory will live on disk while the closure is evaluated and will be deleted afterwards. +/// +/// This function is basically a wrapper over posix's mkdtemp() function. +/// +/// - Parameters: +/// - dir: If specified the temporary directory will be created in this directory otherwise environment +/// variables TMPDIR, TEMP and TMP will be checked for a value (in that order). If none of the env +/// variables are set, dir will be set to `/tmp/`. +/// - prefix: The prefix to the temporary file name. +/// - removeTreeOnDeinit: If enabled try to delete the whole directory tree otherwise remove only if its empty. +/// - body: A closure to execute that receives the absolute path of the directory as an argument. +/// If `body` has a return value, that value is also used as the +/// return value for the `withTemporaryDirectory` function. +/// +/// - Throws: MakeDirectoryError and rethrows all errors from `body`. +@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +public func withTemporaryDirectory( + dir: AbsolutePath? = nil, prefix: String = "TemporaryDirectory", removeTreeOnDeinit: Bool = false , _ body: (AbsolutePath) async throws -> Result +) async throws -> Result { + try await withTemporaryDirectory(dir: dir, prefix: prefix) { path, cleanup in + let result: Result + do { + result = try await body(path) + if removeTreeOnDeinit { await cleanup(path) } + } catch { + if removeTreeOnDeinit { await cleanup(path) } + throw error + } + return result + } +}