Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c84754ad8 | ||
|
|
8469565b06 | ||
|
|
ed45d10179 | ||
|
|
8033ae49b4 | ||
|
|
b290173486 | ||
|
|
8248354a80 | ||
|
|
14963e48f5 | ||
|
|
7f5340bc19 | ||
|
|
74074c634f | ||
|
|
74656bbfcd | ||
|
|
cced2af2cd | ||
|
|
b9abd07318 | ||
|
|
6b52932e7b | ||
|
|
c456081e65 | ||
|
|
ec6c0113f9 | ||
|
|
7970c4d8a7 | ||
|
|
e342da8644 | ||
|
|
9aa91c649e | ||
|
|
62ea0d47b3 | ||
|
|
ab9a70e947 | ||
|
|
49ef073e34 |
1
.github/ranger.yml
vendored
1
.github/ranger.yml
vendored
@@ -1,3 +1,2 @@
|
|||||||
merges:
|
merges:
|
||||||
- action: delete_branch
|
- action: delete_branch
|
||||||
- action: tag
|
|
||||||
|
|||||||
20
.travis.yml
20
.travis.yml
@@ -90,12 +90,15 @@ jobs:
|
|||||||
tags: true
|
tags: true
|
||||||
|
|
||||||
- name: CocoaPods
|
- name: CocoaPods
|
||||||
before_install: |
|
before_install: export TRAVIS_REPO_NAME=${TRAVIS_REPO_SLUG#*/}
|
||||||
|
install: gem install cocoapods
|
||||||
|
before_script: |
|
||||||
export DESCRIPTION=$(swift - <<\ \ EOF
|
export DESCRIPTION=$(swift - <<\ \ EOF
|
||||||
import Foundation
|
import Foundation
|
||||||
struct Response: Decodable { let description: String }
|
struct Response: Decodable { let description: String }
|
||||||
let token = ProcessInfo.processInfo.environment["GITHUB_TOKEN"]!
|
let token = ProcessInfo.processInfo.environment["GITHUB_TOKEN"]!
|
||||||
let url = URL(string: "https://api.github.com/repos/mxcl/Path.swift")!
|
let slug = ProcessInfo.processInfo.environment["TRAVIS_REPO_SLUG"]!
|
||||||
|
let url = URL(string: "https://api.github.com/repos/\(slug)")!
|
||||||
var rq = URLRequest(url: url)
|
var rq = URLRequest(url: url)
|
||||||
rq.setValue("token \(token)", forHTTPHeaderField: "Authorization")
|
rq.setValue("token \(token)", forHTTPHeaderField: "Authorization")
|
||||||
let semaphore = DispatchSemaphore(value: 0)
|
let semaphore = DispatchSemaphore(value: 0)
|
||||||
@@ -108,15 +111,15 @@ jobs:
|
|||||||
let rsp = try JSONDecoder().decode(Response.self, from: data)
|
let rsp = try JSONDecoder().decode(Response.self, from: data)
|
||||||
print(rsp.description, terminator: "")
|
print(rsp.description, terminator: "")
|
||||||
EOF)
|
EOF)
|
||||||
cat <<\ \ EOF> Path.swift.podspec
|
cat <<\ \ EOF> $TRAVIS_REPO_NAME.podspec
|
||||||
Pod::Spec.new do |s|
|
Pod::Spec.new do |s|
|
||||||
s.name = 'Path.swift'
|
s.name = ENV['TRAVIS_REPO_NAME']
|
||||||
s.version = ENV['TRAVIS_TAG']
|
s.version = ENV['TRAVIS_TAG']
|
||||||
s.summary = ENV['DESCRIPTION']
|
s.summary = ENV['DESCRIPTION']
|
||||||
s.homepage = 'https://github.com/mxcl/Path.swift'
|
s.homepage = "https://github.com/#{ENV['TRAVIS_REPO_SLUG']}"
|
||||||
s.license = { :type => 'Unlicense', :file => 'LICENSE.md' }
|
s.license = { type: 'Unlicense', file: 'LICENSE.md' }
|
||||||
s.author = { 'mxcl' => 'mxcl@me.com' }
|
s.author = { mxcl: 'mxcl@me.com' }
|
||||||
s.source = { :git => 'https://github.com/mxcl/Path.swift.git', :tag => s.version.to_s }
|
s.source = { git: "https://github.com/#{ENV['TRAVIS_REPO_SLUG']}.git", tag: s.version }
|
||||||
s.social_media_url = 'https://twitter.com/mxcl'
|
s.social_media_url = 'https://twitter.com/mxcl'
|
||||||
s.osx.deployment_target = '10.10'
|
s.osx.deployment_target = '10.10'
|
||||||
s.ios.deployment_target = '8.0'
|
s.ios.deployment_target = '8.0'
|
||||||
@@ -126,5 +129,4 @@ jobs:
|
|||||||
s.swift_version = '4.2'
|
s.swift_version = '4.2'
|
||||||
end
|
end
|
||||||
EOF
|
EOF
|
||||||
install: gem install cocoapods --pre
|
|
||||||
script: pod trunk push
|
script: pod trunk push
|
||||||
|
|||||||
35
README.md
35
README.md
@@ -1,4 +1,4 @@
|
|||||||
# Path.swift ![badge-platforms] ![badge-languages][] [](https://travis-ci.com/mxcl/Path.swift)
|
# Path.swift ![badge-platforms][] ![badge-languages][] [![badge-ci][]][travis] [![badge-jazzy][]][docs] [![badge-codecov][]][codecov] [![badge-version][]][cocoapods]
|
||||||
|
|
||||||
A file-system pathing library focused on developer experience and robust end
|
A file-system pathing library focused on developer experience and robust end
|
||||||
results.
|
results.
|
||||||
@@ -36,7 +36,7 @@ print(foo.isFile) // => true
|
|||||||
let prefs = Path.home.Library.Preferences // => /Users/mxcl/Library/Preferences
|
let prefs = Path.home.Library.Preferences // => /Users/mxcl/Library/Preferences
|
||||||
|
|
||||||
// a practical example: installing a helper executable
|
// a practical example: installing a helper executable
|
||||||
try Bundle.resources.join("helper").copy(into: Path.home.join(".local/bin").mkdir(.p)).chmod(0o500)
|
try Bundle.resources.helper.copy(into: Path.root.usr.local.bin).chmod(0o500)
|
||||||
```
|
```
|
||||||
|
|
||||||
We emphasize safety and correctness, just like Swift, and also (again like
|
We emphasize safety and correctness, just like Swift, and also (again like
|
||||||
@@ -57,7 +57,8 @@ help me continue my work, I appreciate it x
|
|||||||
|
|
||||||
# Handbook
|
# Handbook
|
||||||
|
|
||||||
Our [online API documentation] is automatically updated for new releases.
|
Our [online API documentation][docs] covers 100% of our public API and is
|
||||||
|
automatically updated for new releases.
|
||||||
|
|
||||||
## Codable
|
## Codable
|
||||||
|
|
||||||
@@ -168,6 +169,20 @@ let files = Path.home.ls().files
|
|||||||
let swiftFiles = Path.home.ls().files(withExtension: "swift")
|
let swiftFiles = Path.home.ls().files(withExtension: "swift")
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# `Path.swift` is robust
|
||||||
|
|
||||||
|
Some parts of `FileManager` are not exactly idiomatic. For example
|
||||||
|
`isExecutableFile` returns `true` even if there is no file there, it is instead
|
||||||
|
telling you that *if* you made a file there it *could* be executable. Thus we
|
||||||
|
check the POSIX permissions of the file first, before returning the result of
|
||||||
|
`isExecutableFile`. `Path.swift` has done the leg-work for you so you can get on
|
||||||
|
with your work without worries.
|
||||||
|
|
||||||
|
# `Path.swift` is properly cross-platform
|
||||||
|
|
||||||
|
`FileManager` on Linux is full of holes. We have found the holes and worked
|
||||||
|
round them where necessary.
|
||||||
|
|
||||||
# Rules & Caveats
|
# Rules & Caveats
|
||||||
|
|
||||||
Paths are just string representations, there *might not* be a real file there.
|
Paths are just string representations, there *might not* be a real file there.
|
||||||
@@ -200,7 +215,7 @@ Path("~/foo")! // => /Users/mxcl/foo
|
|||||||
Path("~foo") // => nil
|
Path("~foo") // => nil
|
||||||
```
|
```
|
||||||
|
|
||||||
*Path.swift* has the general policty that if the desired end result preexists,
|
*Path.swift* has the general policy that if the desired end result preexists,
|
||||||
then it’s a noop:
|
then it’s a noop:
|
||||||
|
|
||||||
* If you try to delete a file, but the file doesn't exist, we do nothing.
|
* If you try to delete a file, but the file doesn't exist, we do nothing.
|
||||||
@@ -215,7 +230,7 @@ for that as the check was deemed too expensive to be worthwhile.
|
|||||||
SwiftPM:
|
SwiftPM:
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
package.append(.package(url: "https://github.com/mxcl/Path.swift", from: "0.5.0"))
|
package.append(.package(url: "https://github.com/mxcl/Path.swift.git", from: "0.5.0"))
|
||||||
```
|
```
|
||||||
|
|
||||||
CocoaPods:
|
CocoaPods:
|
||||||
@@ -239,6 +254,7 @@ https://codebasesaga.com/canopy/
|
|||||||
|
|
||||||
# Alternatives
|
# Alternatives
|
||||||
|
|
||||||
|
* [Pathos](https://github.com/dduan/Pathos) by Daniel Duan
|
||||||
* [PathKit](https://github.com/kylef/PathKit) by Kyle Fuller
|
* [PathKit](https://github.com/kylef/PathKit) by Kyle Fuller
|
||||||
* [Files](https://github.com/JohnSundell/Files) by John Sundell
|
* [Files](https://github.com/JohnSundell/Files) by John Sundell
|
||||||
* [Utility](https://github.com/apple/swift-package-manager) by Apple
|
* [Utility](https://github.com/apple/swift-package-manager) by Apple
|
||||||
@@ -246,4 +262,11 @@ https://codebasesaga.com/canopy/
|
|||||||
|
|
||||||
[badge-platforms]: https://img.shields.io/badge/platforms-macOS%20%7C%20Linux%20%7C%20iOS%20%7C%20tvOS%20%7C%20watchOS-lightgrey.svg
|
[badge-platforms]: https://img.shields.io/badge/platforms-macOS%20%7C%20Linux%20%7C%20iOS%20%7C%20tvOS%20%7C%20watchOS-lightgrey.svg
|
||||||
[badge-languages]: https://img.shields.io/badge/swift-4.2%20%7C%205.0-orange.svg
|
[badge-languages]: https://img.shields.io/badge/swift-4.2%20%7C%205.0-orange.svg
|
||||||
[online API documentation]: https://mxcl.github.io/Path.swift/Structs/Path.html
|
[docs]: https://mxcl.github.io/Path.swift/Structs/Path.html
|
||||||
|
[badge-jazzy]: https://raw.githubusercontent.com/mxcl/Path.swift/gh-pages/badge.svg?sanitize=true
|
||||||
|
[badge-codecov]: https://codecov.io/gh/mxcl/Path.swift/branch/master/graph/badge.svg
|
||||||
|
[badge-ci]: https://travis-ci.com/mxcl/Path.swift.svg
|
||||||
|
[travis]: https://travis-ci.com/mxcl/Path.swift
|
||||||
|
[codecov]: https://codecov.io/gh/mxcl/Path.swift
|
||||||
|
[badge-version]: https://img.shields.io/cocoapods/v/Path.swift.svg?label=version
|
||||||
|
[cocoapods]: https://cocoapods.org/pods/Path.swift
|
||||||
|
|||||||
@@ -9,32 +9,25 @@ public extension Bundle {
|
|||||||
return str.flatMap(Path.init)
|
return str.flatMap(Path.init)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the path for the shared-frameworks directory in this bundle.
|
/**
|
||||||
|
Returns the path for the shared-frameworks directory in this bundle.
|
||||||
|
- Note: This is typically `ShareFrameworks`
|
||||||
|
*/
|
||||||
var sharedFrameworks: Path {
|
var sharedFrameworks: Path {
|
||||||
var `default`: Path {
|
return sharedFrameworksPath.flatMap(Path.init) ?? defaultSharedFrameworksPath
|
||||||
#if os(macOS)
|
}
|
||||||
return path.join("Contents/Frameworks")
|
|
||||||
#elseif os(Linux)
|
/**
|
||||||
return path.join("lib")
|
Returns the path for the private-frameworks directory in this bundle.
|
||||||
#else
|
- Note: This is typically `Frameworks`
|
||||||
return path.join("Frameworks")
|
*/
|
||||||
#endif
|
var privateFrameworks: Path {
|
||||||
}
|
return privateFrameworksPath.flatMap(Path.init) ?? defaultSharedFrameworksPath
|
||||||
return sharedFrameworksPath.flatMap(Path.init) ?? `default`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the path for the resources directory in this bundle.
|
/// Returns the path for the resources directory in this bundle.
|
||||||
var resources: Path {
|
var resources: Path {
|
||||||
var `default`: Path {
|
return resourcePath.flatMap(Path.init) ?? defaultResourcesPath
|
||||||
#if os(macOS)
|
|
||||||
return path.join("Contents/Resources")
|
|
||||||
#elseif os(Linux)
|
|
||||||
return path.join("share")
|
|
||||||
#else
|
|
||||||
return path
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
return resourcePath.flatMap(Path.init) ?? `default`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the path for this bundle.
|
/// Returns the path for this bundle.
|
||||||
@@ -107,3 +100,25 @@ public extension FileHandle {
|
|||||||
try self.init(forUpdating: path.url)
|
try self.init(forUpdating: path.url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal extension Bundle {
|
||||||
|
var defaultSharedFrameworksPath: Path {
|
||||||
|
#if os(macOS)
|
||||||
|
return path.join("Contents/Frameworks")
|
||||||
|
#elseif os(Linux)
|
||||||
|
return path.join("lib")
|
||||||
|
#else
|
||||||
|
return path.join("Frameworks")
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultResourcesPath: Path {
|
||||||
|
#if os(macOS)
|
||||||
|
return path.join("Contents/Resources")
|
||||||
|
#elseif os(Linux)
|
||||||
|
return path.join("share")
|
||||||
|
#else
|
||||||
|
return path
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,30 +5,28 @@ public extension Path {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
Returns the creation-time of the file.
|
Returns the creation-time of the file.
|
||||||
- Note: Returns UNIX-time-zero if there is no creation-time, this should only happen if the file doesn’t exist.
|
- Note: Returns `nil` if there is no creation-time, this should only happen if the file doesn’t exist.
|
||||||
|
- Important: On Linux this is filesystem dependendent and may not exist.
|
||||||
*/
|
*/
|
||||||
var ctime: Date {
|
var ctime: Date? {
|
||||||
do {
|
do {
|
||||||
let attrs = try FileManager.default.attributesOfItem(atPath: string)
|
let attrs = try FileManager.default.attributesOfItem(atPath: string)
|
||||||
return attrs[.creationDate] as? Date ?? Date(timeIntervalSince1970: 0)
|
return attrs[.creationDate] as? Date
|
||||||
} catch {
|
} catch {
|
||||||
//TODO log error
|
return nil
|
||||||
return Date(timeIntervalSince1970: 0)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Returns the modification-time of the file.
|
Returns the modification-time of the file.
|
||||||
- Note: Returns the creation time if there is no modification time.
|
- Note: If this returns `nil` and the file exists, something is very wrong.
|
||||||
- Note: Returns UNIX-time-zero if neither are available, this should only happen if the file doesn’t exist.
|
|
||||||
*/
|
*/
|
||||||
var mtime: Date {
|
var mtime: Date? {
|
||||||
do {
|
do {
|
||||||
let attrs = try FileManager.default.attributesOfItem(atPath: string)
|
let attrs = try FileManager.default.attributesOfItem(atPath: string)
|
||||||
return attrs[.modificationDate] as? Date ?? ctime
|
return attrs[.modificationDate] as? Date
|
||||||
} catch {
|
} catch {
|
||||||
//TODO log error
|
return nil
|
||||||
return Date(timeIntervalSince1970: 0)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,23 +41,34 @@ public extension Path {
|
|||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// - Note: If file is already locked, does nothing
|
/**
|
||||||
/// - Note: If file doesn’t exist, throws
|
Applies the macOS filesystem “lock” attribute.
|
||||||
|
- Note: If file is already locked, does nothing.
|
||||||
|
- Note: If file doesn’t exist, throws.
|
||||||
|
- Important: On Linux does nothing.
|
||||||
|
*/
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func lock() throws -> Path {
|
func lock() throws -> Path {
|
||||||
|
#if !os(Linux)
|
||||||
var attrs = try FileManager.default.attributesOfItem(atPath: string)
|
var attrs = try FileManager.default.attributesOfItem(atPath: string)
|
||||||
let b = attrs[.immutable] as? Bool ?? false
|
let b = attrs[.immutable] as? Bool ?? false
|
||||||
if !b {
|
if !b {
|
||||||
attrs[.immutable] = true
|
attrs[.immutable] = true
|
||||||
try FileManager.default.setAttributes(attrs, ofItemAtPath: string)
|
try FileManager.default.setAttributes(attrs, ofItemAtPath: string)
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// - Note: If file isn‘t locked, does nothing
|
/**
|
||||||
/// - Note: If file doesn’t exist, does nothing
|
- Note: If file isn‘t locked, does nothing.
|
||||||
|
- Note: If file doesn’t exist, does nothing.
|
||||||
|
- Important: On Linux does nothing.
|
||||||
|
- SeeAlso: `lock()`
|
||||||
|
*/
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func unlock() throws -> Path {
|
func unlock() throws -> Path {
|
||||||
|
#if !os(Linux)
|
||||||
var attrs: [FileAttributeKey: Any]
|
var attrs: [FileAttributeKey: Any]
|
||||||
do {
|
do {
|
||||||
attrs = try FileManager.default.attributesOfItem(atPath: string)
|
attrs = try FileManager.default.attributesOfItem(atPath: string)
|
||||||
@@ -71,6 +80,7 @@ public extension Path {
|
|||||||
attrs[.immutable] = false
|
attrs[.immutable] = false
|
||||||
try FileManager.default.setAttributes(attrs, ofItemAtPath: string)
|
try FileManager.default.setAttributes(attrs, ofItemAtPath: string)
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,18 +47,7 @@ extension Path {
|
|||||||
fatalError()
|
fatalError()
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
guard let pathString = FileManager.default.urls(for: searchPath, in: .userDomainMask).first?.path else {
|
guard let pathString = FileManager.default.urls(for: searchPath, in: .userDomainMask).first?.path else { return defaultUrl(for: searchPath) }
|
||||||
switch searchPath {
|
|
||||||
case .documentDirectory:
|
|
||||||
return Path.home/"Documents"
|
|
||||||
case .applicationSupportDirectory:
|
|
||||||
return Path.home/"Library/Application Support"
|
|
||||||
case .cachesDirectory:
|
|
||||||
return Path.home/"Library/Caches"
|
|
||||||
default:
|
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Path(string: pathString)
|
return Path(string: pathString)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -90,3 +79,19 @@ extension Path {
|
|||||||
return path(for: .applicationSupportDirectory)
|
return path(for: .applicationSupportDirectory)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if !os(Linux)
|
||||||
|
func defaultUrl(for searchPath: FileManager.SearchPathDirectory) -> Path {
|
||||||
|
switch searchPath {
|
||||||
|
case .documentDirectory:
|
||||||
|
return Path.home/"Documents"
|
||||||
|
case .applicationSupportDirectory:
|
||||||
|
return Path.home/"Library/Application Support"
|
||||||
|
case .cachesDirectory:
|
||||||
|
return Path.home/"Library/Caches"
|
||||||
|
default:
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
#if os(Linux)
|
||||||
|
import Glibc
|
||||||
|
#endif
|
||||||
|
|
||||||
public extension Path {
|
public extension Path {
|
||||||
//MARK: File Management
|
//MARK: File Management
|
||||||
@@ -25,6 +28,11 @@ public extension Path {
|
|||||||
if overwrite, to.isFile, isFile {
|
if overwrite, to.isFile, isFile {
|
||||||
try FileManager.default.removeItem(at: to.url)
|
try FileManager.default.removeItem(at: to.url)
|
||||||
}
|
}
|
||||||
|
#if os(Linux) && !swift(>=5.1) // check if fixed
|
||||||
|
if !overwrite, to.isFile {
|
||||||
|
throw CocoaError.error(.fileWriteFileExists)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
try FileManager.default.copyItem(atPath: string, toPath: to.string)
|
try FileManager.default.copyItem(atPath: string, toPath: to.string)
|
||||||
return to
|
return to
|
||||||
}
|
}
|
||||||
@@ -38,7 +46,8 @@ public extension Path {
|
|||||||
// Create ~/.local/bin, copy `ls` there and make the new copy executable
|
// Create ~/.local/bin, copy `ls` there and make the new copy executable
|
||||||
try Path.root.join("bin/ls").copy(into: Path.home.join(".local/bin").mkdir(.p)).chmod(0o500)
|
try Path.root.join("bin/ls").copy(into: Path.home.join(".local/bin").mkdir(.p)).chmod(0o500)
|
||||||
|
|
||||||
If the destination does not exist, this function creates the directory first.
|
If the destination does not exist, this function creates the directory
|
||||||
|
(including intermediary directories if necessary) first.
|
||||||
|
|
||||||
- Parameter into: Destination directory
|
- Parameter into: Destination directory
|
||||||
- Parameter overwrite: If true overwrites any file that already exists at `into`.
|
- Parameter overwrite: If true overwrites any file that already exists at `into`.
|
||||||
@@ -48,25 +57,21 @@ public extension Path {
|
|||||||
`self` because even though *Path.swift’s* policy is to noop if the desired
|
`self` because even though *Path.swift’s* policy is to noop if the desired
|
||||||
end result preexists, checking for this condition is too expensive a
|
end result preexists, checking for this condition is too expensive a
|
||||||
trade-off.
|
trade-off.
|
||||||
- SeeAlso: `copy(into:overwrite:)`
|
- SeeAlso: `copy(to:overwrite:)`
|
||||||
*/
|
*/
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func copy(into: Path, overwrite: Bool = false) throws -> Path {
|
func copy(into: Path, overwrite: Bool = false) throws -> Path {
|
||||||
if !into.exists {
|
if !into.exists {
|
||||||
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
|
try into.mkdir(.p)
|
||||||
}
|
}
|
||||||
let rv = into/basename()
|
let rv = into/basename()
|
||||||
if overwrite, rv.isFile {
|
if overwrite, rv.isFile {
|
||||||
try rv.delete()
|
try rv.delete()
|
||||||
}
|
}
|
||||||
#if os(Linux)
|
#if os(Linux) && !swift(>=5.1) // check if fixed
|
||||||
#if swift(>=5.1)
|
|
||||||
// check if fixed
|
|
||||||
#else
|
|
||||||
if !overwrite, rv.isFile {
|
if !overwrite, rv.isFile {
|
||||||
throw CocoaError.error(.fileWriteFileExists)
|
throw CocoaError.error(.fileWriteFileExists)
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
#endif
|
#endif
|
||||||
try FileManager.default.copyItem(at: url, to: rv.url)
|
try FileManager.default.copyItem(at: url, to: rv.url)
|
||||||
return rv
|
return rv
|
||||||
@@ -86,7 +91,7 @@ public extension Path {
|
|||||||
`self` because even though *Path.swift’s* policy is to noop if the desired
|
`self` because even though *Path.swift’s* policy is to noop if the desired
|
||||||
end result preexists, checking for this condition is too expensive a
|
end result preexists, checking for this condition is too expensive a
|
||||||
trade-off.
|
trade-off.
|
||||||
- SeeAlso: move(into:overwrite:)
|
- SeeAlso: `move(into:overwrite:)`
|
||||||
*/
|
*/
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func move(to: Path, overwrite: Bool = false) throws -> Path {
|
func move(to: Path, overwrite: Bool = false) throws -> Path {
|
||||||
@@ -103,13 +108,14 @@ public extension Path {
|
|||||||
try Path.root.join("bar").move(into: .home)
|
try Path.root.join("bar").move(into: .home)
|
||||||
// => "/Users/mxcl/bar"
|
// => "/Users/mxcl/bar"
|
||||||
|
|
||||||
If the destination does not exist, this function creates the directory first.
|
If the destination does not exist, this function creates the directory
|
||||||
|
(including intermediary directories if necessary) first.
|
||||||
|
|
||||||
- Parameter into: Destination directory
|
- Parameter into: Destination directory
|
||||||
- Parameter overwrite: If true *overwrites* any file that already exists at `into`.
|
- Parameter overwrite: If true *overwrites* any file that already exists at `into`.
|
||||||
- Note: `throws` if `into` is a file.
|
- Note: `throws` if `into` is a file.
|
||||||
- Returns: The `Path` of destination filename.
|
- Returns: The `Path` of destination filename.
|
||||||
- SeeAlso: move(into:overwrite:)
|
- SeeAlso: `move(to:overwrite:)`
|
||||||
*/
|
*/
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func move(into: Path, overwrite: Bool = false) throws -> Path {
|
func move(into: Path, overwrite: Bool = false) throws -> Path {
|
||||||
@@ -130,6 +136,9 @@ public extension Path {
|
|||||||
Deletes the path, recursively if a directory.
|
Deletes the path, recursively if a directory.
|
||||||
- Note: noop: if the path doesn’t exist
|
- Note: noop: if the path doesn’t exist
|
||||||
∵ *Path.swift* doesn’t error if desired end result preexists.
|
∵ *Path.swift* doesn’t error if desired end result preexists.
|
||||||
|
- Note: On UNIX will this function will succeed if the parent directory is writable and the current user has permission.
|
||||||
|
- Note: This function will fail if the file or directory is “locked”
|
||||||
|
- SeeAlso: `lock()`
|
||||||
*/
|
*/
|
||||||
@inlinable
|
@inlinable
|
||||||
func delete() throws {
|
func delete() throws {
|
||||||
@@ -139,13 +148,26 @@ public extension Path {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Creates an empty file at this path.
|
Creates an empty file at this path or if the file exists, updates its modification time.
|
||||||
- Returns: `self` to allow chaining.
|
- Returns: `self` to allow chaining.
|
||||||
*/
|
*/
|
||||||
@inlinable
|
@inlinable
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func touch() throws -> Path {
|
func touch() throws -> Path {
|
||||||
return try "".write(to: self)
|
if !exists {
|
||||||
|
guard FileManager.default.createFile(atPath: string, contents: nil) else {
|
||||||
|
throw CocoaError.error(.fileWriteUnknown)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
#if os(Linux)
|
||||||
|
let fd = open(string, O_WRONLY)
|
||||||
|
defer { close(fd) }
|
||||||
|
futimens(fd, nil)
|
||||||
|
#else
|
||||||
|
try FileManager.default.setAttributes([.modificationDate: Date()], ofItemAtPath: string)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -175,8 +197,16 @@ public extension Path {
|
|||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Renames the file at path.
|
||||||
|
|
||||||
|
Path.root.foo.bar.rename(to: "baz") // => /foo/baz
|
||||||
|
|
||||||
|
- Parameter to: the new basename for the file
|
||||||
|
- Returns: The renamed path.
|
||||||
|
*/
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func rename(_ newname: String) throws -> Path {
|
func rename(to newname: String) throws -> Path {
|
||||||
let newpath = parent/newname
|
let newpath = parent/newname
|
||||||
try FileManager.default.moveItem(atPath: string, toPath: newpath.string)
|
try FileManager.default.moveItem(atPath: string, toPath: newpath.string)
|
||||||
return newpath
|
return newpath
|
||||||
|
|||||||
@@ -10,6 +10,6 @@ extension Path: CustomStringConvertible {
|
|||||||
extension Path: CustomDebugStringConvertible {
|
extension Path: CustomDebugStringConvertible {
|
||||||
/// Returns eg. `Path(string: "/foo")`
|
/// Returns eg. `Path(string: "/foo")`
|
||||||
public var debugDescription: String {
|
public var debugDescription: String {
|
||||||
return "Path(string: \(string))"
|
return "Path(\(string))"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
#if os(Linux)
|
||||||
|
import func Glibc.access
|
||||||
|
#else
|
||||||
|
import func Darwin.access
|
||||||
|
#endif
|
||||||
|
|
||||||
public extension Path {
|
public extension Path {
|
||||||
//MARK: Filesystem Properties
|
//MARK: Filesystem Properties
|
||||||
@@ -32,11 +37,22 @@ public extension Path {
|
|||||||
|
|
||||||
/// Returns true if the path represents an actual file that is also deletable by the current user.
|
/// Returns true if the path represents an actual file that is also deletable by the current user.
|
||||||
var isDeletable: Bool {
|
var isDeletable: Bool {
|
||||||
return FileManager.default.isDeletableFile(atPath: string)
|
#if os(Linux) && !swift(>=5.1)
|
||||||
|
return exists && access(parent.string, W_OK) == 0
|
||||||
|
#else
|
||||||
|
// FileManager.isDeletableFile returns true if there is *not* a file there
|
||||||
|
return exists && FileManager.default.isDeletableFile(atPath: string)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if the path represents an actual file that is also executable by the current user.
|
/// Returns true if the path represents an actual file that is also executable by the current user.
|
||||||
var isExecutable: Bool {
|
var isExecutable: Bool {
|
||||||
return FileManager.default.isExecutableFile(atPath: string)
|
if access(string, X_OK) == 0 {
|
||||||
|
// FileManager.isExxecutableFile returns true even if there is *not*
|
||||||
|
// a file there *but* if there was it could be *made* executable
|
||||||
|
return FileManager.default.isExecutableFile(atPath: string)
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import Foundation
|
|||||||
`Path` supports `Codable`, and can be configured to
|
`Path` supports `Codable`, and can be configured to
|
||||||
[encode paths *relatively*](https://github.com/mxcl/Path.swift/#codable).
|
[encode paths *relatively*](https://github.com/mxcl/Path.swift/#codable).
|
||||||
|
|
||||||
Sorting a `Sequence` of `Path`s will return the locale-aware sort order, which
|
Sorting a `Sequence` of paths will return the locale-aware sort order, which
|
||||||
will give you the same order as Finder.
|
will give you the same order as Finder.
|
||||||
|
|
||||||
Converting from a `String` is a common first step, here are the recommended
|
Converting from a `String` is a common first step, here are the recommended
|
||||||
@@ -20,13 +20,12 @@ import Foundation
|
|||||||
let p3 = Path.cwd/relativePathString
|
let p3 = Path.cwd/relativePathString
|
||||||
let p4 = Path(userInput) ?? Path.cwd/userInput
|
let p4 = Path(userInput) ?? Path.cwd/userInput
|
||||||
|
|
||||||
If you are constructing Paths from static-strings we provide support for
|
If you are constructing paths from static-strings we provide support for
|
||||||
dynamic members:
|
dynamic members:
|
||||||
|
|
||||||
let p1 = Path.root.usr.bin.ls // => /usr/bin/ls
|
let p1 = Path.root.usr.bin.ls // => /usr/bin/ls
|
||||||
|
|
||||||
- Note: There may not be an actual filesystem entry at the path. The underlying
|
- Note: A `Path` does not necessarily represent an actual filesystem entry.
|
||||||
representation for `Path` is `String`.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@dynamicMemberLookup
|
@dynamicMemberLookup
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
@testable import Path
|
||||||
import XCTest
|
import XCTest
|
||||||
import Path
|
|
||||||
|
|
||||||
class PathTests: XCTestCase {
|
class PathTests: XCTestCase {
|
||||||
func testConcatenation() {
|
func testConcatenation() {
|
||||||
@@ -164,6 +164,16 @@ class PathTests: XCTestCase {
|
|||||||
XCTAssertEqual(a.Documents, Path.home/"foo/Documents")
|
XCTAssertEqual(a.Documents, Path.home/"foo/Documents")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testCopyTo() throws {
|
||||||
|
try Path.mktemp { root in
|
||||||
|
try root.foo.touch().copy(to: root.bar)
|
||||||
|
XCTAssert(root.foo.isFile)
|
||||||
|
XCTAssert(root.bar.isFile)
|
||||||
|
XCTAssertThrowsError(try root.foo.copy(to: root.bar))
|
||||||
|
try root.foo.copy(to: root.bar, overwrite: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func testCopyInto() throws {
|
func testCopyInto() throws {
|
||||||
try Path.mktemp { root1 in
|
try Path.mktemp { root1 in
|
||||||
let bar1 = try root1.join("bar").touch()
|
let bar1 = try root1.join("bar").touch()
|
||||||
@@ -174,6 +184,25 @@ class PathTests: XCTestCase {
|
|||||||
XCTAssertTrue(bar1.exists)
|
XCTAssertTrue(bar1.exists)
|
||||||
XCTAssertTrue(bar2.exists)
|
XCTAssertTrue(bar2.exists)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// test creates intermediary directories
|
||||||
|
try bar1.copy(into: root1.create.directories)
|
||||||
|
|
||||||
|
// test doesn’t replace file if “copy into” a file
|
||||||
|
let d = try root1.fuz.touch()
|
||||||
|
XCTAssertThrowsError(try root1.baz.touch().copy(into: d))
|
||||||
|
XCTAssert(d.isFile)
|
||||||
|
XCTAssert(root1.baz.isFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testMoveTo() throws {
|
||||||
|
try Path.mktemp { tmpdir in
|
||||||
|
try tmpdir.foo.touch().move(to: tmpdir.bar)
|
||||||
|
XCTAssertFalse(tmpdir.foo.exists)
|
||||||
|
XCTAssert(tmpdir.bar.isFile)
|
||||||
|
XCTAssertThrowsError(try tmpdir.foo.touch().move(to: tmpdir.bar))
|
||||||
|
try tmpdir.foo.move(to: tmpdir.bar, overwrite: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,6 +216,17 @@ class PathTests: XCTestCase {
|
|||||||
XCTAssertFalse(bar1.exists)
|
XCTAssertFalse(bar1.exists)
|
||||||
XCTAssertTrue(bar2.exists)
|
XCTAssertTrue(bar2.exists)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// test creates intermediary directories
|
||||||
|
try root1.baz.touch().move(into: root1.create.directories)
|
||||||
|
XCTAssertFalse(root1.baz.exists)
|
||||||
|
XCTAssert(root1.create.directories.baz.isFile)
|
||||||
|
|
||||||
|
// test doesn’t replace file if “move into” a file
|
||||||
|
let d = try root1.fuz.touch()
|
||||||
|
XCTAssertThrowsError(try root1.baz.touch().move(into: d))
|
||||||
|
XCTAssert(d.isFile)
|
||||||
|
XCTAssert(root1.baz.isFile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,14 +234,194 @@ class PathTests: XCTestCase {
|
|||||||
try Path.mktemp { root in
|
try Path.mktemp { root in
|
||||||
do {
|
do {
|
||||||
let file = try root.bar.touch()
|
let file = try root.bar.touch()
|
||||||
let foo = try file.rename("foo")
|
let foo = try file.rename(to: "foo")
|
||||||
XCTAssertFalse(file.exists)
|
XCTAssertFalse(file.exists)
|
||||||
XCTAssertTrue(foo.isFile)
|
XCTAssertTrue(foo.isFile)
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
let file = try root.bar.touch()
|
let file = try root.bar.touch()
|
||||||
XCTAssertThrowsError(try file.rename("foo"))
|
XCTAssertThrowsError(try file.rename(to: "foo"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testCommonDirectories() {
|
||||||
|
XCTAssertEqual(Path.root.string, "/")
|
||||||
|
XCTAssertEqual(Path.home.string, NSHomeDirectory())
|
||||||
|
XCTAssertEqual(Path.documents.string, NSHomeDirectory() + "/Documents")
|
||||||
|
#if !os(Linux)
|
||||||
|
XCTAssertEqual(Path.caches.string, NSHomeDirectory() + "/Library/Caches")
|
||||||
|
XCTAssertEqual(Path.cwd.string, FileManager.default.currentDirectoryPath)
|
||||||
|
XCTAssertEqual(Path.applicationSupport.string, NSHomeDirectory() + "/Library/Application Support")
|
||||||
|
|
||||||
|
_ = defaultUrl(for: .documentDirectory)
|
||||||
|
_ = defaultUrl(for: .cachesDirectory)
|
||||||
|
_ = defaultUrl(for: .applicationSupportDirectory)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
func testStringConvertibles() {
|
||||||
|
XCTAssertEqual(Path.root.description, "/")
|
||||||
|
XCTAssertEqual(Path.root.debugDescription, "Path(/)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testFilesystemAttributes() throws {
|
||||||
|
XCTAssert(Path(#file)!.isFile)
|
||||||
|
XCTAssert(Path(#file)!.isReadable)
|
||||||
|
XCTAssert(Path(#file)!.isWritable)
|
||||||
|
XCTAssert(Path(#file)!.isDeletable)
|
||||||
|
XCTAssert(Path(#file)!.parent.isDirectory)
|
||||||
|
|
||||||
|
try Path.mktemp { tmpdir in
|
||||||
|
XCTAssertTrue(tmpdir.isDirectory)
|
||||||
|
XCTAssertFalse(tmpdir.isFile)
|
||||||
|
|
||||||
|
let bar = try tmpdir.bar.touch().chmod(0o000)
|
||||||
|
XCTAssertFalse(bar.isReadable)
|
||||||
|
XCTAssertFalse(bar.isWritable)
|
||||||
|
XCTAssertFalse(bar.isDirectory)
|
||||||
|
XCTAssertFalse(bar.isExecutable)
|
||||||
|
XCTAssertTrue(bar.isFile)
|
||||||
|
XCTAssertTrue(bar.isDeletable) // can delete even if no read permissions
|
||||||
|
|
||||||
|
try bar.chmod(0o777)
|
||||||
|
XCTAssertTrue(bar.isReadable)
|
||||||
|
XCTAssertTrue(bar.isWritable)
|
||||||
|
XCTAssertTrue(bar.isDeletable)
|
||||||
|
XCTAssertTrue(bar.isExecutable)
|
||||||
|
|
||||||
|
try bar.delete()
|
||||||
|
XCTAssertFalse(bar.exists)
|
||||||
|
XCTAssertFalse(bar.isReadable)
|
||||||
|
XCTAssertFalse(bar.isWritable)
|
||||||
|
XCTAssertFalse(bar.isExecutable)
|
||||||
|
XCTAssertFalse(bar.isDeletable)
|
||||||
|
|
||||||
|
let nonExistantFile = tmpdir.baz
|
||||||
|
XCTAssertFalse(nonExistantFile.exists)
|
||||||
|
XCTAssertFalse(nonExistantFile.isExecutable)
|
||||||
|
XCTAssertFalse(nonExistantFile.isReadable)
|
||||||
|
XCTAssertFalse(nonExistantFile.isWritable)
|
||||||
|
XCTAssertFalse(nonExistantFile.isDeletable)
|
||||||
|
XCTAssertFalse(nonExistantFile.isDirectory)
|
||||||
|
XCTAssertFalse(nonExistantFile.isFile)
|
||||||
|
|
||||||
|
let baz = try tmpdir.baz.touch()
|
||||||
|
XCTAssertTrue(baz.isDeletable)
|
||||||
|
try tmpdir.chmod(0o500) // remove write permission on directory
|
||||||
|
XCTAssertFalse(baz.isDeletable) // this is how deletion is prevented on UNIX
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTimes() throws {
|
||||||
|
try Path.mktemp { tmpdir in
|
||||||
|
let foo = try tmpdir.foo.touch()
|
||||||
|
let now1 = Date().timeIntervalSince1970.rounded(.down)
|
||||||
|
#if !os(Linux)
|
||||||
|
XCTAssertEqual(foo.ctime?.timeIntervalSince1970.rounded(.down), now1) //FIXME flakey
|
||||||
|
#endif
|
||||||
|
XCTAssertEqual(foo.mtime?.timeIntervalSince1970.rounded(.down), now1) //FIXME flakey
|
||||||
|
sleep(1)
|
||||||
|
try foo.touch()
|
||||||
|
let now2 = Date().timeIntervalSince1970.rounded(.down)
|
||||||
|
XCTAssertNotEqual(now1, now2)
|
||||||
|
XCTAssertEqual(foo.mtime?.timeIntervalSince1970.rounded(.down), now2) //FIXME flakey
|
||||||
|
|
||||||
|
XCTAssertNil(tmpdir.void.mtime)
|
||||||
|
XCTAssertNil(tmpdir.void.ctime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDelete() throws {
|
||||||
|
try Path.mktemp { tmpdir in
|
||||||
|
try tmpdir.bar1.delete()
|
||||||
|
try tmpdir.bar2.touch().delete()
|
||||||
|
try tmpdir.bar3.touch().chmod(0o000).delete()
|
||||||
|
#if !os(Linux)
|
||||||
|
XCTAssertThrowsError(try tmpdir.bar3.touch().lock().delete())
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRelativeCodable() throws {
|
||||||
|
let path = Path.home.foo
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
encoder.userInfo[.relativePath] = Path.home
|
||||||
|
let data = try encoder.encode([path])
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
decoder.userInfo[.relativePath] = Path.home
|
||||||
|
XCTAssertEqual(try decoder.decode([Path].self, from: data), [path])
|
||||||
|
decoder.userInfo[.relativePath] = Path.documents
|
||||||
|
XCTAssertEqual(try decoder.decode([Path].self, from: data), [Path.documents.foo])
|
||||||
|
XCTAssertThrowsError(try JSONDecoder().decode([Path].self, from: data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testBundleExtensions() throws {
|
||||||
|
try Path.mktemp { tmpdir in
|
||||||
|
let bndl = Bundle(path: tmpdir.string)!
|
||||||
|
XCTAssertEqual(bndl.path, tmpdir)
|
||||||
|
XCTAssertEqual(bndl.sharedFrameworks, tmpdir.SharedFrameworks)
|
||||||
|
XCTAssertEqual(bndl.privateFrameworks, tmpdir.Frameworks)
|
||||||
|
XCTAssertEqual(bndl.resources, tmpdir)
|
||||||
|
XCTAssertNil(bndl.path(forResource: "foo", ofType: "bar"))
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
XCTAssertEqual(bndl.defaultSharedFrameworksPath, tmpdir.Contents.Frameworks)
|
||||||
|
XCTAssertEqual(bndl.defaultResourcesPath, tmpdir.Contents.Resources)
|
||||||
|
#elseif os(tvOS) || os(iOS)
|
||||||
|
XCTAssertEqual(bndl.defaultSharedFrameworksPath, tmpdir.Frameworks)
|
||||||
|
XCTAssertEqual(bndl.defaultResourcesPath, tmpdir)
|
||||||
|
#else
|
||||||
|
XCTAssertEqual(bndl.defaultSharedFrameworksPath, tmpdir.lib)
|
||||||
|
XCTAssertEqual(bndl.defaultResourcesPath, tmpdir.share)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDataExtensions() throws {
|
||||||
|
let data = try Data(contentsOf: Path(#file)!)
|
||||||
|
try Path.mktemp { tmpdir in
|
||||||
|
_ = try data.write(to: tmpdir.foo)
|
||||||
|
_ = try data.write(to: tmpdir.foo, atomically: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testStringExtensions() throws {
|
||||||
|
let string = try String(contentsOf: Path(#file)!)
|
||||||
|
try Path.mktemp { tmpdir in
|
||||||
|
_ = try string.write(to: tmpdir.foo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testFileHandleExtensions() throws {
|
||||||
|
_ = try FileHandle(forReadingAt: Path(#file)!)
|
||||||
|
_ = try FileHandle(forWritingAt: Path(#file)!)
|
||||||
|
_ = try FileHandle(forUpdatingAt: Path(#file)!)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSort() {
|
||||||
|
XCTAssertEqual([Path.root.a, Path.root.c, Path.root.b].sorted(), [Path.root.a, Path.root.b, Path.root.c])
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLock() throws {
|
||||||
|
#if !os(Linux)
|
||||||
|
try Path.mktemp { tmpdir in
|
||||||
|
let bar = try tmpdir.bar.touch()
|
||||||
|
try bar.lock()
|
||||||
|
XCTAssertThrowsError(try bar.touch())
|
||||||
|
try bar.unlock()
|
||||||
|
try bar.touch()
|
||||||
|
|
||||||
|
// a non existant file is already “unlocked”
|
||||||
|
try tmpdir.nonExit.unlock()
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTouchThrowsIfCannotWrite() throws {
|
||||||
|
try Path.mktemp { tmpdir in
|
||||||
|
try tmpdir.chmod(0o000)
|
||||||
|
XCTAssertThrowsError(try tmpdir.bar.touch())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,11 @@ class TemporaryDirectory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
_ = try? FileManager.default.removeItem(at: url)
|
do {
|
||||||
|
try path.chmod(0o777).delete()
|
||||||
|
} catch {
|
||||||
|
//TODO log
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,22 +3,37 @@ import XCTest
|
|||||||
extension PathTests {
|
extension PathTests {
|
||||||
static let __allTests = [
|
static let __allTests = [
|
||||||
("testBasename", testBasename),
|
("testBasename", testBasename),
|
||||||
|
("testBundleExtensions", testBundleExtensions),
|
||||||
("testCodable", testCodable),
|
("testCodable", testCodable),
|
||||||
|
("testCommonDirectories", testCommonDirectories),
|
||||||
("testConcatenation", testConcatenation),
|
("testConcatenation", testConcatenation),
|
||||||
("testCopyInto", testCopyInto),
|
("testCopyInto", testCopyInto),
|
||||||
|
("testCopyTo", testCopyTo),
|
||||||
|
("testDataExtensions", testDataExtensions),
|
||||||
|
("testDelete", testDelete),
|
||||||
("testDynamicMember", testDynamicMember),
|
("testDynamicMember", testDynamicMember),
|
||||||
("testEnumeration", testEnumeration),
|
("testEnumeration", testEnumeration),
|
||||||
("testEnumerationSkippingHiddenFiles", testEnumerationSkippingHiddenFiles),
|
("testEnumerationSkippingHiddenFiles", testEnumerationSkippingHiddenFiles),
|
||||||
("testExists", testExists),
|
("testExists", testExists),
|
||||||
("testExtension", testExtension),
|
("testExtension", testExtension),
|
||||||
|
("testFileHandleExtensions", testFileHandleExtensions),
|
||||||
|
("testFilesystemAttributes", testFilesystemAttributes),
|
||||||
("testIsDirectory", testIsDirectory),
|
("testIsDirectory", testIsDirectory),
|
||||||
("testJoin", testJoin),
|
("testJoin", testJoin),
|
||||||
|
("testLock", testLock),
|
||||||
("testMkpathIfExists", testMkpathIfExists),
|
("testMkpathIfExists", testMkpathIfExists),
|
||||||
("testMktemp", testMktemp),
|
("testMktemp", testMktemp),
|
||||||
("testMoveInto", testMoveInto),
|
("testMoveInto", testMoveInto),
|
||||||
|
("testMoveTo", testMoveTo),
|
||||||
|
("testRelativeCodable", testRelativeCodable),
|
||||||
("testRelativePathCodable", testRelativePathCodable),
|
("testRelativePathCodable", testRelativePathCodable),
|
||||||
("testRelativeTo", testRelativeTo),
|
("testRelativeTo", testRelativeTo),
|
||||||
("testRename", testRename),
|
("testRename", testRename),
|
||||||
|
("testSort", testSort),
|
||||||
|
("testStringConvertibles", testStringConvertibles),
|
||||||
|
("testStringExtensions", testStringExtensions),
|
||||||
|
("testTimes", testTimes),
|
||||||
|
("testTouchThrowsIfCannotWrite", testTouchThrowsIfCannotWrite),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user