@@ -1,34 +1,35 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
//#if os(Linux)
|
||||||
|
//import func Glibc.chmod
|
||||||
|
//#endif
|
||||||
|
|
||||||
public extension Path {
|
public extension Path {
|
||||||
//MARK: Filesystem Attributes
|
//MARK: Filesystem Attributes
|
||||||
|
|
||||||
/**
|
/**
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,27 +40,40 @@ public extension Path {
|
|||||||
*/
|
*/
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func chmod(_ octal: Int) throws -> Path {
|
func chmod(_ octal: Int) throws -> Path {
|
||||||
|
// #if os(Linux)
|
||||||
|
// Glibc.chmod(string, __mode_t(octal))
|
||||||
|
// #else
|
||||||
try FileManager.default.setAttributes([.posixPermissions: octal], ofItemAtPath: string)
|
try FileManager.default.setAttributes([.posixPermissions: octal], ofItemAtPath: string)
|
||||||
|
// #endif
|
||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// - Note: If file is already locked, does nothing
|
/**
|
||||||
/// - Note: If file doesn’t exist, throws
|
- 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.
|
||||||
|
*/
|
||||||
@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 +85,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,6 +130,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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -204,4 +204,121 @@ class PathTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testCommonDirectories() {
|
||||||
|
XCTAssertEqual(Path.root.string, "/")
|
||||||
|
XCTAssertEqual(Path.home.string, NSHomeDirectory())
|
||||||
|
XCTAssertEqual(Path.documents.string, NSHomeDirectory() + "/Documents")
|
||||||
|
#if os(macOS)
|
||||||
|
XCTAssertEqual(Path.caches.string, NSHomeDirectory() + "/Library/Caches")
|
||||||
|
XCTAssertEqual(Path.cwd.string, FileManager.default.currentDirectoryPath)
|
||||||
|
XCTAssertEqual(Path.applicationSupport.string, NSHomeDirectory() + "/Library/Application Support")
|
||||||
|
#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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
XCTAssertTrue(Bundle.main.path.isDirectory)
|
||||||
|
XCTAssertTrue(Bundle.main.resources.isDirectory)
|
||||||
|
|
||||||
|
// doesn’t exist in tests
|
||||||
|
_ = Bundle.main.sharedFrameworks
|
||||||
|
}
|
||||||
|
|
||||||
|
func testFileHandleExtensions() throws {
|
||||||
|
_ = try FileHandle(forReadingAt: Path(#file)!)
|
||||||
|
_ = try FileHandle(forWritingAt: Path(#file)!)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,30 @@ 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),
|
||||||
|
("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),
|
||||||
("testMkpathIfExists", testMkpathIfExists),
|
("testMkpathIfExists", testMkpathIfExists),
|
||||||
("testMktemp", testMktemp),
|
("testMktemp", testMktemp),
|
||||||
("testMoveInto", testMoveInto),
|
("testMoveInto", testMoveInto),
|
||||||
|
("testRelativeCodable", testRelativeCodable),
|
||||||
("testRelativePathCodable", testRelativePathCodable),
|
("testRelativePathCodable", testRelativePathCodable),
|
||||||
("testRelativeTo", testRelativeTo),
|
("testRelativeTo", testRelativeTo),
|
||||||
("testRename", testRename),
|
("testRename", testRename),
|
||||||
|
("testStringConvertibles", testStringConvertibles),
|
||||||
|
("testTimes", testTimes),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user