-
Notifications
You must be signed in to change notification settings - Fork 131
/
Copy pathTemporaryFile.swift
416 lines (379 loc) · 19.3 KB
/
TemporaryFile.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
/*
This source file is part of the Swift.org open source project
Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception
See http://swift.org/LICENSE.txt for license information
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
*/
import TSCLibc
import class Foundation.FileHandle
import class Foundation.FileManager
import func Foundation.NSTemporaryDirectory
import protocol Foundation.CustomNSError
import var Foundation.NSLocalizedDescriptionKey
public enum TempFileError: Error {
/// Could not create a unique temporary filename.
case couldNotCreateUniqueName
// FIXME: This should be factored out into a open error enum.
//
/// Some error thrown defined by posix's open().
case other(Int32)
/// Couldn't find a temporary directory.
case couldNotFindTmpDir(String)
}
extension TempFileError: CustomNSError {
public var errorUserInfo: [String : Any] {
return [NSLocalizedDescriptionKey: "\(self)"]
}
}
private extension TempFileError {
init(errno: Int32) {
switch errno {
case TSCLibc.EEXIST:
self = .couldNotCreateUniqueName
default:
self = .other(errno)
}
}
}
/// Determines the directory in which the temporary file should be created. Also makes
/// sure the returning path has a trailing forward slash.
///
/// - Parameters:
/// - dir: If present this will be the temporary directory.
///
/// - Returns: Path to directory in which temporary file should be created.
public func determineTempDirectory(_ dir: AbsolutePath? = nil) throws -> AbsolutePath {
let tmpDir = try dir ?? localFileSystem.tempDirectory
guard localFileSystem.isDirectory(tmpDir) else {
throw TempFileError.couldNotFindTmpDir(tmpDir.pathString)
}
return tmpDir
}
/// The closure argument of the `body` closure of `withTemporaryFile`.
public struct TemporaryFile {
/// If specified during init, the temporary file name begins with this prefix.
let prefix: String
/// If specified during init, the temporary file name ends with this suffix.
let suffix: String
/// The directory in which the temporary file is created.
public let dir: AbsolutePath
/// The full path of the temporary file. For safety file operations should be done via the fileHandle instead of
/// using this path.
public let path: AbsolutePath
/// The file descriptor of the temporary file. It is used to create NSFileHandle which is exposed to clients.
private let fd: Int32
/// FileHandle of the temporary file, can be used to read/write data.
public let fileHandle: FileHandle
fileprivate init(dir: AbsolutePath?, prefix: String, suffix: String) throws {
self.suffix = suffix
self.prefix = prefix
// Determine in which directory to create the temporary file.
self.dir = try determineTempDirectory(dir)
// Construct path to the temporary file.
let path = try AbsolutePath(validating: prefix + ".XXXXXX" + suffix, relativeTo: self.dir)
// Convert path to a C style string terminating with null char to be an valid input
// to mkstemps method. The XXXXXX in this string will be replaced by a random string
// which will be the actual path to the temporary file.
var template = Array(path.pathString.utf8CString)
fd = TSCLibc.mkstemps(&template, Int32(suffix.utf8.count))
// If mkstemps failed then throw error.
if fd == -1 { throw TempFileError(errno: errno) }
self.path = try AbsolutePath(validating: String(cString: template))
fileHandle = FileHandle(fileDescriptor: fd, closeOnDealloc: true)
}
}
extension TemporaryFile: CustomStringConvertible {
public var description: String {
return "<TemporaryFile: \(path)>"
}
}
/// 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`.
public func withTemporaryFile<Result>(
dir: AbsolutePath? = nil, prefix: String = "TemporaryFile", suffix: String = "", _ body: (TemporaryFile, @escaping (TemporaryFile) -> Void) throws -> Result
) throws -> Result {
return try 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 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<Result>(
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.
///
/// 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`.
public func withTemporaryFile<Result>(
dir: AbsolutePath? = nil, prefix: String = "TemporaryFile", suffix: String = "", deleteOnClose: Bool = true, _ body: (TemporaryFile) throws -> Result
) throws -> Result {
try withTemporaryFile(dir: dir, prefix: prefix, suffix: suffix) { tempFile, cleanup in
defer { if (deleteOnClose) { cleanup(tempFile) } }
return try body(tempFile)
}
}
/// 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<Result>(
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.
public enum MakeDirectoryError: Error {
/// The given path already exists as a directory, file or symbolic link.
case pathExists
/// The path provided was too long.
case pathTooLong
/// Process does not have permissions to create directory.
/// Note: Includes read-only filesystems or if file system does not support directory creation.
case permissionDenied
/// The path provided is unresolvable because it has too many symbolic links or a path component is invalid.
case unresolvablePathComponent
/// Exceeded user quota or kernel is out of memory.
case outOfMemory
/// All other system errors with their value.
case other(Int32)
}
private extension MakeDirectoryError {
init(errno: Int32) {
switch errno {
case TSCLibc.EEXIST:
self = .pathExists
case TSCLibc.ENAMETOOLONG:
self = .pathTooLong
case TSCLibc.EACCES, TSCLibc.EFAULT, TSCLibc.EPERM, TSCLibc.EROFS:
self = .permissionDenied
case TSCLibc.ELOOP, TSCLibc.ENOENT, TSCLibc.ENOTDIR:
self = .unresolvablePathComponent
case TSCLibc.ENOMEM:
self = .outOfMemory
#if !os(Windows)
case TSCLibc.EDQUOT:
self = .outOfMemory
#endif
default:
self = .other(errno)
}
}
}
extension MakeDirectoryError: CustomNSError {
public var errorUserInfo: [String : Any] {
return [NSLocalizedDescriptionKey: "\(self)"]
}
}
/// 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`.
public func withTemporaryDirectory<Result>(
dir: AbsolutePath? = nil, prefix: String = "TemporaryDirectory" , _ body: (AbsolutePath, @escaping (AbsolutePath) -> Void) throws -> Result
) 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 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 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<Result>(
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.
///
/// 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`.
public func withTemporaryDirectory<Result>(
dir: AbsolutePath? = nil, prefix: String = "TemporaryDirectory", removeTreeOnDeinit: Bool = false , _ body: (AbsolutePath) throws -> Result
) throws -> Result {
try withTemporaryDirectory(dir: dir, prefix: prefix) { path, cleanup in
defer { if removeTreeOnDeinit { cleanup(path) } }
return try body(path)
}
}
/// 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<Result>(
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
}
}