Skip to content

Create helper functions for temporary directories which supports new concurrency features #5532

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jun 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions Sources/Basics/TemporaryFile.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2022 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 the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import Foundation
import TSCBasic

/// 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.
///
/// - Parameters:
/// - fileSystem: `FileSystem` which is used to construct temporary directory.
/// - 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: An error when creating directory and rethrows all errors from `body`.
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
public func withTemporaryDirectory<Result>(
fileSystem: FileSystem = localFileSystem,
dir: AbsolutePath? = nil,
prefix: String = "TemporaryDirectory",
_ body: @escaping (AbsolutePath, @escaping (AbsolutePath) -> Void) async throws -> Result
) throws -> Task<Result, Error> {
let temporaryDirectory = try createTemporaryDirectory(fileSystem: fileSystem, dir: dir, prefix: prefix)

let task: Task<Result, Error> = Task {
try await withTaskCancellationHandler {
try await body(temporaryDirectory) { path in
try? fileSystem.removeFileTree(path)
}
} onCancel: {
try? fileSystem.removeFileTree(temporaryDirectory)
}

}

return task
}

/// 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.
///
/// - Parameters:
/// - fileSystem: `FileSystem` which is used to construct temporary directory.
/// - 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: An error when creating directory and rethrows all errors from `body`.
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
public func withTemporaryDirectory<Result>(
fileSystem: FileSystem = localFileSystem,
dir: AbsolutePath? = nil,
prefix: String = "TemporaryDirectory",
removeTreeOnDeinit: Bool = false,
_ body: @escaping (AbsolutePath) async throws -> Result
) throws -> Task<Result, Error> {
try withTemporaryDirectory(fileSystem: fileSystem, dir: dir, prefix: prefix) { path, cleanup in
defer { if removeTreeOnDeinit { cleanup(path) } }
return try await body(path)
}
}

private func createTemporaryDirectory(fileSystem: FileSystem, dir: AbsolutePath?, prefix: String) throws -> AbsolutePath {
// This random generation is needed so that
// it is more or less equal to generation using `mkdtemp` function
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

let randomSuffix = String((0..<6).map { _ in letters.randomElement()! })

let tempDirectory = dir ?? fileSystem.tempDirectory
guard fileSystem.isDirectory(tempDirectory) else {
throw TempFileError.couldNotFindTmpDir(tempDirectory.pathString)
}

// Construct path to the temporary directory.
let templatePath = AbsolutePath(prefix + ".\(randomSuffix)", relativeTo: tempDirectory)

try fileSystem.createDirectory(templatePath, recursive: true)
return templatePath
}
92 changes: 92 additions & 0 deletions Tests/BasicsTests/TemporaryFileTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
This source file is part of the Swift.org open source project

Copyright (c) 2022 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 XCTest

import TSCBasic

import Basics

@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
class TemporaryAsyncFileTests: XCTestCase {
func testBasicTemporaryDirectory() async throws {
// Test can create and remove temp directory.
let path1: AbsolutePath = try await withTemporaryDirectory(removeTreeOnDeinit: true) { tempDirPath in
// Do some async task
try await Task.sleep(nanoseconds: 1_000)

XCTAssertTrue(localFileSystem.isDirectory(tempDirPath))
return tempDirPath
}.value
XCTAssertFalse(localFileSystem.isDirectory(path1))

// Test temp directory is not removed when its not empty.
let path2: AbsolutePath = try await withTemporaryDirectory { tempDirPath in
XCTAssertTrue(localFileSystem.isDirectory(tempDirPath))
// Create a file inside the temp directory.
let filePath = tempDirPath.appending(component: "somefile")
// Do some async task
try await Task.sleep(nanoseconds: 1_000)

try localFileSystem.writeFileContents(filePath, bytes: ByteString())
return tempDirPath
}.value
XCTAssertTrue(localFileSystem.isDirectory(path2))
// Cleanup.
try localFileSystem.removeFileTree(path2)
XCTAssertFalse(localFileSystem.isDirectory(path2))

// Test temp directory is removed when its not empty and removeTreeOnDeinit is enabled.
let path3: AbsolutePath = try await withTemporaryDirectory(removeTreeOnDeinit: true) { tempDirPath in
XCTAssertTrue(localFileSystem.isDirectory(tempDirPath))
let filePath = tempDirPath.appending(component: "somefile")
// Do some async task
try await Task.sleep(nanoseconds: 1_000)

try localFileSystem.writeFileContents(filePath, bytes: ByteString())
return tempDirPath
}.value
XCTAssertFalse(localFileSystem.isDirectory(path3))
}

func testCanCreateUniqueTempDirectories() async throws {
let (pathOne, pathTwo): (AbsolutePath, AbsolutePath) = try await withTemporaryDirectory(removeTreeOnDeinit: true) { pathOne in
let pathTwo: AbsolutePath = try await withTemporaryDirectory(removeTreeOnDeinit: true) { pathTwo in
// Do some async task
try await Task.sleep(nanoseconds: 1_000)

XCTAssertTrue(localFileSystem.isDirectory(pathOne))
XCTAssertTrue(localFileSystem.isDirectory(pathTwo))
// Their paths should be different.
XCTAssertTrue(pathOne != pathTwo)
return pathTwo
}.value
return (pathOne, pathTwo)
}.value
XCTAssertFalse(localFileSystem.isDirectory(pathOne))
XCTAssertFalse(localFileSystem.isDirectory(pathTwo))
}

func testCancelOfTask() async throws {
let task: Task<AbsolutePath, Error> = try withTemporaryDirectory { path in

try await Task.sleep(nanoseconds: 1_000_000_000)
XCTAssertTrue(Task.isCancelled)
XCTAssertFalse(localFileSystem.isDirectory(path))
return path
}
task.cancel()
do {
// The correct path is to throw an error here
let _ = try await task.value
XCTFail("The correct path here is to throw an error")
} catch {}
}
}