Skip to content

Commit d56abe9

Browse files
authored
Create helper functions for temporary directories which supports new concurrency features (#5532)
* Create helper functions for temporary directories which supports new concurrency features * Change generation of temporaryDirectory using fileSystem API * Removed dependency on TSCLibc
1 parent 1c2564a commit d56abe9

File tree

2 files changed

+193
-0
lines changed

2 files changed

+193
-0
lines changed

Sources/Basics/TemporaryFile.swift

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2022 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Foundation
14+
import TSCBasic
15+
16+
/// Creates a temporary directory and evaluates a closure with the directory path as an argument.
17+
/// The temporary directory will live on disk while the closure is evaluated and will be deleted when
18+
/// the cleanup closure is called. This allows the temporary directory to have an arbitrary lifetime.
19+
///
20+
/// - Parameters:
21+
/// - fileSystem: `FileSystem` which is used to construct temporary directory.
22+
/// - dir: If specified the temporary directory will be created in this directory otherwise environment
23+
/// variables TMPDIR, TEMP and TMP will be checked for a value (in that order). If none of the env
24+
/// variables are set, dir will be set to `/tmp/`.
25+
/// - prefix: The prefix to the temporary file name.
26+
/// - body: A closure to execute that receives the absolute path of the directory as an argument.
27+
/// If `body` has a return value, that value is also used as the
28+
/// return value for the `withTemporaryDirectory` function.
29+
/// The cleanup block should be called when the temporary directory is no longer needed.
30+
///
31+
/// - Throws: An error when creating directory and rethrows all errors from `body`.
32+
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
33+
public func withTemporaryDirectory<Result>(
34+
fileSystem: FileSystem = localFileSystem,
35+
dir: AbsolutePath? = nil,
36+
prefix: String = "TemporaryDirectory",
37+
_ body: @escaping (AbsolutePath, @escaping (AbsolutePath) -> Void) async throws -> Result
38+
) throws -> Task<Result, Error> {
39+
let temporaryDirectory = try createTemporaryDirectory(fileSystem: fileSystem, dir: dir, prefix: prefix)
40+
41+
let task: Task<Result, Error> = Task {
42+
try await withTaskCancellationHandler {
43+
try await body(temporaryDirectory) { path in
44+
try? fileSystem.removeFileTree(path)
45+
}
46+
} onCancel: {
47+
try? fileSystem.removeFileTree(temporaryDirectory)
48+
}
49+
50+
}
51+
52+
return task
53+
}
54+
55+
/// Creates a temporary directory and evaluates a closure with the directory path as an argument.
56+
/// The temporary directory will live on disk while the closure is evaluated and will be deleted afterwards.
57+
///
58+
/// - Parameters:
59+
/// - fileSystem: `FileSystem` which is used to construct temporary directory.
60+
/// - dir: If specified the temporary directory will be created in this directory otherwise environment
61+
/// variables TMPDIR, TEMP and TMP will be checked for a value (in that order). If none of the env
62+
/// variables are set, dir will be set to `/tmp/`.
63+
/// - prefix: The prefix to the temporary file name.
64+
/// - removeTreeOnDeinit: If enabled try to delete the whole directory tree otherwise remove only if its empty.
65+
/// - body: A closure to execute that receives the absolute path of the directory as an argument.
66+
/// If `body` has a return value, that value is also used as the
67+
/// return value for the `withTemporaryDirectory` function.
68+
///
69+
/// - Throws: An error when creating directory and rethrows all errors from `body`.
70+
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
71+
public func withTemporaryDirectory<Result>(
72+
fileSystem: FileSystem = localFileSystem,
73+
dir: AbsolutePath? = nil,
74+
prefix: String = "TemporaryDirectory",
75+
removeTreeOnDeinit: Bool = false,
76+
_ body: @escaping (AbsolutePath) async throws -> Result
77+
) throws -> Task<Result, Error> {
78+
try withTemporaryDirectory(fileSystem: fileSystem, dir: dir, prefix: prefix) { path, cleanup in
79+
defer { if removeTreeOnDeinit { cleanup(path) } }
80+
return try await body(path)
81+
}
82+
}
83+
84+
private func createTemporaryDirectory(fileSystem: FileSystem, dir: AbsolutePath?, prefix: String) throws -> AbsolutePath {
85+
// This random generation is needed so that
86+
// it is more or less equal to generation using `mkdtemp` function
87+
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
88+
89+
let randomSuffix = String((0..<6).map { _ in letters.randomElement()! })
90+
91+
let tempDirectory = dir ?? fileSystem.tempDirectory
92+
guard fileSystem.isDirectory(tempDirectory) else {
93+
throw TempFileError.couldNotFindTmpDir(tempDirectory.pathString)
94+
}
95+
96+
// Construct path to the temporary directory.
97+
let templatePath = AbsolutePath(prefix + ".\(randomSuffix)", relativeTo: tempDirectory)
98+
99+
try fileSystem.createDirectory(templatePath, recursive: true)
100+
return templatePath
101+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2022 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See http://swift.org/LICENSE.txt for license information
8+
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import XCTest
12+
13+
import TSCBasic
14+
15+
import Basics
16+
17+
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
18+
class TemporaryAsyncFileTests: XCTestCase {
19+
func testBasicTemporaryDirectory() async throws {
20+
// Test can create and remove temp directory.
21+
let path1: AbsolutePath = try await withTemporaryDirectory(removeTreeOnDeinit: true) { tempDirPath in
22+
// Do some async task
23+
try await Task.sleep(nanoseconds: 1_000)
24+
25+
XCTAssertTrue(localFileSystem.isDirectory(tempDirPath))
26+
return tempDirPath
27+
}.value
28+
XCTAssertFalse(localFileSystem.isDirectory(path1))
29+
30+
// Test temp directory is not removed when its not empty.
31+
let path2: AbsolutePath = try await withTemporaryDirectory { tempDirPath in
32+
XCTAssertTrue(localFileSystem.isDirectory(tempDirPath))
33+
// Create a file inside the temp directory.
34+
let filePath = tempDirPath.appending(component: "somefile")
35+
// Do some async task
36+
try await Task.sleep(nanoseconds: 1_000)
37+
38+
try localFileSystem.writeFileContents(filePath, bytes: ByteString())
39+
return tempDirPath
40+
}.value
41+
XCTAssertTrue(localFileSystem.isDirectory(path2))
42+
// Cleanup.
43+
try localFileSystem.removeFileTree(path2)
44+
XCTAssertFalse(localFileSystem.isDirectory(path2))
45+
46+
// Test temp directory is removed when its not empty and removeTreeOnDeinit is enabled.
47+
let path3: AbsolutePath = try await withTemporaryDirectory(removeTreeOnDeinit: true) { tempDirPath in
48+
XCTAssertTrue(localFileSystem.isDirectory(tempDirPath))
49+
let filePath = tempDirPath.appending(component: "somefile")
50+
// Do some async task
51+
try await Task.sleep(nanoseconds: 1_000)
52+
53+
try localFileSystem.writeFileContents(filePath, bytes: ByteString())
54+
return tempDirPath
55+
}.value
56+
XCTAssertFalse(localFileSystem.isDirectory(path3))
57+
}
58+
59+
func testCanCreateUniqueTempDirectories() async throws {
60+
let (pathOne, pathTwo): (AbsolutePath, AbsolutePath) = try await withTemporaryDirectory(removeTreeOnDeinit: true) { pathOne in
61+
let pathTwo: AbsolutePath = try await withTemporaryDirectory(removeTreeOnDeinit: true) { pathTwo in
62+
// Do some async task
63+
try await Task.sleep(nanoseconds: 1_000)
64+
65+
XCTAssertTrue(localFileSystem.isDirectory(pathOne))
66+
XCTAssertTrue(localFileSystem.isDirectory(pathTwo))
67+
// Their paths should be different.
68+
XCTAssertTrue(pathOne != pathTwo)
69+
return pathTwo
70+
}.value
71+
return (pathOne, pathTwo)
72+
}.value
73+
XCTAssertFalse(localFileSystem.isDirectory(pathOne))
74+
XCTAssertFalse(localFileSystem.isDirectory(pathTwo))
75+
}
76+
77+
func testCancelOfTask() async throws {
78+
let task: Task<AbsolutePath, Error> = try withTemporaryDirectory { path in
79+
80+
try await Task.sleep(nanoseconds: 1_000_000_000)
81+
XCTAssertTrue(Task.isCancelled)
82+
XCTAssertFalse(localFileSystem.isDirectory(path))
83+
return path
84+
}
85+
task.cancel()
86+
do {
87+
// The correct path is to throw an error here
88+
let _ = try await task.value
89+
XCTFail("The correct path here is to throw an error")
90+
} catch {}
91+
}
92+
}

0 commit comments

Comments
 (0)