Skip to content

Commit 62354a7

Browse files
committed
Create helper functions for temporary directories which supports new concurrency features
1 parent 09d78a3 commit 62354a7

File tree

2 files changed

+196
-0
lines changed

2 files changed

+196
-0
lines changed

Sources/Basics/TemporaryFile.swift

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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+
import TSCLibc
16+
17+
/// Creates a temporary directory and evaluates a closure with the directory path as an argument.
18+
/// The temporary directory will live on disk while the closure is evaluated and will be deleted when
19+
/// the cleanup closure is called. This allows the temporary directory to have an arbitrary lifetime.
20+
///
21+
/// This function is basically a wrapper over posix's mkdtemp() function.
22+
///
23+
/// - Parameters:
24+
/// - dir: If specified the temporary directory will be created in this directory otherwise environment
25+
/// variables TMPDIR, TEMP and TMP will be checked for a value (in that order). If none of the env
26+
/// variables are set, dir will be set to `/tmp/`.
27+
/// - prefix: The prefix to the temporary file name.
28+
/// - body: A closure to execute that receives the absolute path of the directory as an argument.
29+
/// If `body` has a return value, that value is also used as the
30+
/// return value for the `withTemporaryDirectory` function.
31+
/// The cleanup block should be called when the temporary directory is no longer needed.
32+
///
33+
/// - Throws: `MakeDirectoryError` and rethrows all errors from `body`.
34+
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
35+
public func withTemporaryDirectory<Result>(
36+
dir: AbsolutePath? = nil, prefix: String = "TemporaryDirectory" , _ body: (AbsolutePath, @escaping (AbsolutePath) -> Void) async throws -> Result
37+
) async throws -> Result {
38+
let template = try createTemporaryDirectoryTemplate(dir: dir, prefix: prefix)
39+
return try await body(AbsolutePath(String(cString: template))) { path in
40+
_ = try? FileManager.default.removeItem(atPath: path.pathString)
41+
}
42+
}
43+
44+
/// Creates a temporary directory and evaluates a closure with the directory path as an argument.
45+
/// The temporary directory will live on disk while the closure is evaluated and will be deleted afterwards.
46+
///
47+
/// This function is basically a wrapper over posix's mkdtemp() function.
48+
///
49+
/// - Parameters:
50+
/// - dir: If specified the temporary directory will be created in this directory otherwise environment
51+
/// variables TMPDIR, TEMP and TMP will be checked for a value (in that order). If none of the env
52+
/// variables are set, dir will be set to `/tmp/`.
53+
/// - prefix: The prefix to the temporary file name.
54+
/// - removeTreeOnDeinit: If enabled try to delete the whole directory tree otherwise remove only if its empty.
55+
/// - body: A closure to execute that receives the absolute path of the directory as an argument.
56+
/// If `body` has a return value, that value is also used as the
57+
/// return value for the `withTemporaryDirectory` function.
58+
///
59+
/// - Throws: `MakeDirectoryError` and rethrows all errors from `body`.
60+
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
61+
public func withTemporaryDirectory<Result>(
62+
dir: AbsolutePath? = nil, prefix: String = "TemporaryDirectory", removeTreeOnDeinit: Bool = false , _ body: (AbsolutePath) async throws -> Result
63+
) async throws -> Result {
64+
try await withTemporaryDirectory(dir: dir, prefix: prefix) { path, cleanup in
65+
defer { if removeTreeOnDeinit { cleanup(path) } }
66+
return try await body(path)
67+
}
68+
}
69+
70+
private func createTemporaryDirectoryTemplate(dir: AbsolutePath?, prefix: String) throws -> [Int8] {
71+
// Construct path to the temporary directory.
72+
let templatePath = try AbsolutePath(prefix + ".XXXXXX", relativeTo: determineTempDirectory(dir))
73+
74+
// Convert templatePath to a C style string terminating with null char to be an valid input
75+
// to mkdtemp method. The XXXXXX in this string will be replaced by a random string
76+
// which will be the actual path to the temporary directory.
77+
var template = [UInt8](templatePath.pathString.utf8).map({ Int8($0) }) + [Int8(0)]
78+
79+
if TSCLibc.mkdtemp(&template) == nil {
80+
throw MakeDirectoryError(errno: errno)
81+
}
82+
return template
83+
}
84+
85+
private extension MakeDirectoryError {
86+
init(errno: Int32) {
87+
switch errno {
88+
case TSCLibc.EEXIST:
89+
self = .pathExists
90+
case TSCLibc.ENAMETOOLONG:
91+
self = .pathTooLong
92+
case TSCLibc.EACCES, TSCLibc.EFAULT, TSCLibc.EPERM, TSCLibc.EROFS:
93+
self = .permissionDenied
94+
case TSCLibc.ELOOP, TSCLibc.ENOENT, TSCLibc.ENOTDIR:
95+
self = .unresolvablePathComponent
96+
case TSCLibc.ENOMEM:
97+
self = .outOfMemory
98+
#if !os(Windows)
99+
case TSCLibc.EDQUOT:
100+
self = .outOfMemory
101+
#endif
102+
default:
103+
self = .other(errno)
104+
}
105+
}
106+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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 class Foundation.FileManager
14+
15+
import TSCBasic
16+
17+
import Basics
18+
19+
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
20+
class TemporaryAsyncFileTests: XCTestCase {
21+
func testBasicTemporaryDirectory() async throws {
22+
// Test can create and remove temp directory.
23+
let path1: AbsolutePath = try await withTemporaryDirectory(removeTreeOnDeinit: true) { tempDirPath in
24+
// Do some async task
25+
let task = Task {
26+
return
27+
}
28+
await task.value
29+
30+
XCTAssertTrue(localFileSystem.isDirectory(tempDirPath))
31+
return tempDirPath
32+
}
33+
XCTAssertFalse(localFileSystem.isDirectory(path1))
34+
35+
// Test temp directory is not removed when its not empty.
36+
let path2: AbsolutePath = try await withTemporaryDirectory { tempDirPath in
37+
XCTAssertTrue(localFileSystem.isDirectory(tempDirPath))
38+
// Create a file inside the temp directory.
39+
let filePath = tempDirPath.appending(component: "somefile")
40+
// Do some async task
41+
let task = Task {
42+
return
43+
}
44+
await task.value
45+
46+
try localFileSystem.writeFileContents(filePath, bytes: ByteString())
47+
return tempDirPath
48+
}
49+
XCTAssertTrue(localFileSystem.isDirectory(path2))
50+
// Cleanup.
51+
try FileManager.default.removeItem(atPath: path2.pathString)
52+
XCTAssertFalse(localFileSystem.isDirectory(path2))
53+
54+
// Test temp directory is removed when its not empty and removeTreeOnDeinit is enabled.
55+
let path3: AbsolutePath = try await withTemporaryDirectory(removeTreeOnDeinit: true) { tempDirPath in
56+
XCTAssertTrue(localFileSystem.isDirectory(tempDirPath))
57+
let filePath = tempDirPath.appending(component: "somefile")
58+
// Do some async task
59+
let task = Task {
60+
return
61+
}
62+
await task.value
63+
64+
try localFileSystem.writeFileContents(filePath, bytes: ByteString())
65+
return tempDirPath
66+
}
67+
XCTAssertFalse(localFileSystem.isDirectory(path3))
68+
}
69+
70+
func testCanCreateUniqueTempDirectories() async throws {
71+
let (pathOne, pathTwo): (AbsolutePath, AbsolutePath) = try await withTemporaryDirectory(removeTreeOnDeinit: true) { pathOne in
72+
let pathTwo: AbsolutePath = try await withTemporaryDirectory(removeTreeOnDeinit: true) { pathTwo in
73+
// Do some async task
74+
let task = Task {
75+
return
76+
}
77+
await task.value
78+
79+
XCTAssertTrue(localFileSystem.isDirectory(pathOne))
80+
XCTAssertTrue(localFileSystem.isDirectory(pathTwo))
81+
// Their paths should be different.
82+
XCTAssertTrue(pathOne != pathTwo)
83+
return pathTwo
84+
}
85+
return (pathOne, pathTwo)
86+
}
87+
XCTAssertFalse(localFileSystem.isDirectory(pathOne))
88+
XCTAssertFalse(localFileSystem.isDirectory(pathTwo))
89+
}
90+
}

0 commit comments

Comments
 (0)