Pathman

Build Status codecov Maintainability Current Version Supported Platforms Language Language Version License
一个为 Apple 的 Swift 语言设计的类型安全的路径库。

动机

我一直不太喜欢 Foundation 的 FileManager。一般来说,Foundation 在跨平台使用时结果并不一致(Linux 支持/稳定性对于我使用 Swift 的大多数场景来说很重要),并且 FileManager 本身缺乏类型安全性和易用性,而这些正是大多数 Swift API 应该具备的(FileAttributeKey 有人喜欢吗?)。

所以我构建了 Pathman! 这是第一个围绕底层 C API 构建的类型安全的 Swift 路径库(其他所有库都只是 FileManager 的包装器,使其在 Swift 中使用起来更方便)。

目标

安装

兼容性

Swift Package Manager

将其添加到您的 Package.swift 依赖项中

.package(url: "https://github.com/Ponyboy47/Pathman.git", from: "0.20.1")

用法

路径

目前有 3 种不同的路径类型:GenericPath、FilePath 和 DirectoryPath

// Paths can be initialized from Strings, Arrays, or Slices
let genericString = GenericPath("/tmp")
let genericArray = GenericPath(["/", "tmp"])
let genericSlice = GenericPath(["/", "tmp", "test"].dropLast())

// FilePaths and DirectoryPaths can be initialized the same as a GenericPath

// Beware that you do your own validation that the path matches it's type.
// Things like this are possible and will lead to errors:
let file = FilePath("/tmp/")

let directory = DirectoryPath("/tmp/")

路径信息

// Paths conform to the StatDelegate protocol, which means that they use the
// `stat` utility to gather information about the file (ie: size, ownership,
// modify time, etc)
// NOTE: Certain properties are only available for paths that exist

/// The system id of the path
var id: DeviceID

/// The inode of the path
var inode: Inode

/// The type of the path, if it exists
var type: PathType

/// Whether the path exists
var exists: Bool

/// Whether the path exists and is a file
var isFile: Bool

/// Whether the path exists and is a directory
var isDirectory: Bool

/// Whether the path exists and is a link
var isLink: Bool

/// The URL representation of the path
var url: URL

/// The permissions of the path
var permissions: FileMode

/// The user id of the user that owns the path
var owner: UID

// The name of the user that owns the path
var ownerName: String?

/// The group id of the user that owns the path
var group: GID

/// The name of the group that owns the path
var groupName: String?

/// The device id (if special file)
var device: DeviceID

/// The total size, in bytes
var size: OSOffsetInt
// macOS -> Int64
// Linux -> Int

/// The blocksize for filesystem I/O
var blockSize: BlockSize

/// The number of 512B block allocated
var blocks: OSOffsetInt
// macOS -> Int64
// Linux -> Int

/// The parent directory of the path
var parent: DirectoryPath

/// The pieces that make up the path
var components: [String]

/// The final piece of the path (filename or directory name)
var lastComponent: String?

/// The final piece of the path with the extension stripped off
var lastComponentWithoutExtension: String?

/// The extension of the path
var extension: String?

/// The last time the path was accessed
var lastAccess: Date

/// The last time the path was modified
var lastModified: Date

/// The last time the path had a status change
var lastAttributeChange: Date

/// The time when the path was created (macOS only)
var creation: Date

打开路径

FilePath

let file = FilePath("/tmp/test")
let openFile: OpenFile = try file.open(mode: "r+")

// Open files can be written to or read from (depending on the permissions used above)
let content: String = try openFile.read()
try openFile.write(content)

DirectoryPath

let dir = DirectoryPath("/tmp")
let openDir: OpenDirectory = try dir.open()

// Open directories can be traversed
let children = openDir.children()

// Recursively traversing directories requires opening sub-directories and may throw errors
let recursiveChildren = try openDir.recursiveChildren()

使用闭包

路径也可以在提供的闭包的持续时间内打开

let dir = DirectoryPath("/tmp")

try dir.open() { openDirectory in
    let children = openDirectory.children()
    print(children)
}

创建路径

任何符合 Openable 协议的路径

var file = FilePath("/tmp/test")

// Creates a file with the write permissions and returns the opened file
let openFile: OpenFile = try file.create(mode: FileMode(owner: .readWriteExecute, group: .readWrite, other: .none))

创建中间目录

如果您还需要创建中间路径

var file = FilePath("/tmp/testdir/test")

let openFile: OpenFile = try file.create(options: .createIntermediates)

包含内容

其 Open<...> 变体符合 Writable 的路径可以使用预定的内容创建

var file = FilePath("/tmp/test")

try file.create(contents: "Hello World")
print(try file.read()) // "Hello World"

使用闭包

路径也可以在提供的闭包的持续时间内打开

var file = FilePath("/tmp/test")

try file.create() { openFile in
    try openFile.write("Hello world")
    let contents: String = try openFile.read(from: .beginning)
    print(contents) // Hello World
}

删除路径

仅删除当前路径

这对于所有路径都是一样的

var file = FilePath("/tmp/test")

try file.delete()

递归删除目录

var dir = DirectoryPath("/tmp/test")

try dir.recursiveDelete()

注意:对此要非常小心,因为它无法撤消(就像 rm -rf 一样)。

读取文件

let file = FilePath("/tmp/test")

// All of the following operations are available on both a FilePath and an OpenFile

// Read the whole file
let contents: String = try file.read()

// Read up to 1024 bytes
let contents: String = try file.read(bytes: 1024)

// Read content as ascii characters instead of utf8
let contents: String = try file.read(encoding: .ascii)

// Read to the end, but starting at 1024 bytes from the beginning of the file
let contents: String = try file.read(from: Offset(from: .beginning, bytes: 1024))

// Read the last 1024 bytes from of the file using the ascii encoding
let contents: String = try file.read(from: Offset(from: .end, bytes: -1024), bytes: 1024, encoding: .ascii)

注意
FilePath 读取仅用于对文件执行单个读取操作,因为它会打开文件,从中读取,然后关闭文件。如果您要多次读取文件,最好使用 try file.open(permissions: .read) 打开它,然后根据需要多次读取。
每次读取后,文件偏移量都会更新。如果您希望再次从头开始读取,请传递偏移量 Offset(from: .beginning, bytes: 0)
如果文件是使用 .append 标志打开的,那么任何传递的偏移量都将被忽略,并且文件偏移量在任何写入操作之前都会移动到文件末尾。
每个读取操作都可能返回 StringData,因此请确保您要存储到的对象是显式类型的,否则,您将遇到歧义的用例。

写入文件

let file = FilePath("/tmp/test")

// All of the following operations are available on both a FilePath and an OpenFile

// Write a string at the current file position
try file.write("Hello world")

// Write an ascii string at the end of the file
try file.write("Goodbye", at: Offset(from: .end, bytes: 0), using: .ascii)

注意:您也可以将 Data 实例传递给 write 函数,而不是带有编码的 String

缓冲文件写入

// Writing files is buffered by default. If you expect to use a file
// immediately after writing to it then be sure to flush the buffer
let file = FilePath("/tmp/test")

let openFile = try file.open(mode: "w+")
try openFile.write("Hello world!")
try openFile.flush()
try openFile.rewind()
let contents = openFile.read()

// You may also change the buffering mode for the file
try openFile.setBuffer(mode: .line) // Flushes after each newline
try openFile.setBuffer(mode: .none) // Flushes immediately
try openFile.setBuffer(mode: .full(size: 1024)) // Flushes after 1024 bytes are written

注意:默认缓冲是基于您操作系统 BUFSIZ 变量的完全缓冲

获取目录内容

直接子项

let dir = DirectoryPath("/tmp")

let children = try dir.children()

// This same operation is safe, assuming you've already opened the directory
let openDir = try dir.open()
let children = openDir.children()

print(children.files)
print(children.directories)
print(children.other)

递归子项

let dir = DirectoryPath("/tmp")

let children = try dir.recursiveChildren()

// This operation is still unsafe, even if the directory is already opened (Because you still might have to open sub-directories, which is unsafe)
let openDir = try dir.open()
let children = try openDir.recursiveChildren()

print(children.files)
print(children.directories)
print(children.other)

// You can optionally specify a depth to only get so many directories
// This will go no more than 5 directories deep before returning
let children = try dir.recursiveChildren(depth: 5)

隐藏文件

// Both .children() and .recursiveChildren() support getting hidden files/directories (files that begin with a '.')
let children = try dir.children(options: .includeHidden)
let recursiveChildren = try dir.recursiveChildren(depth: 5, options: .includeHidden)

更改路径元数据

所有权

var path = GenericPath("/tmp")

// Owner/Group can be changed separately or together
try path.change(owner: "ponyboy47")
try path.change(group: "ponyboy47")
try path.change(owner: "ponyboy47", group: "ponyboy47")

// You can also set them through the corresponding properties:
// NOTE: Setting them this way is NOT guarenteed to succeed and any errors
// thrown are ignored. If you need a reliant way to set path ownership then you
// should call the `change` method directly
path.owner = 0
path.group = 1000
path.ownerName = "root"
path.groupName = "wheel"

// If you have a DirectoryPath, then changes can be made recursively:
var dir = DirectoryPath(path)

try dir.recursiveChange(owner: "ponyboy47")

权限

var path = GenericPath("/tmp")

// Owner/Group/Others permissions can each be changed separately or in any combination (permissions that are not specified are not changed)
try path.change(owner: [.read, .write, .execute]) // Only changes the owner's permissions
try path.change(group: .readWrite) // Only changes the group's permissions
try path.change(others: .none) // Only changes other's permissions
try path.change(ownerGroup: .all) // Only changes owner's and group's permissions
try path.change(groupOthers: .read) // Only changes group's and other's permissions
try path.change(ownerOthers: .writeExecute) // Only changes owner's and other's permissions
try path.change(ownerGroupOthers: .all) // Changes all permissions

// You can also change the uid, gid, and sticky bits
try path.change(bits: .uid)
try path.change(bits: .gid)
try path.change(bits: .sticky)
try path.change(bits: [.uid, .sticky])
try path.change(bits: .all)

// You can also set them through the permissions property:
// NOTE: Setting them this way is NOT guarenteed to succeed and any errors
// thrown are ignored. If you need a reliant way to set path ownership then you
// should call the `change` method directly
path.permissions = FileMode(owner: .readWriteExecute, group: .readWrite, others: .read)
path.permissions.owner = .readWriteExecute
path.permissions.group = .readWrite
path.permissions.others = .read
path.permissions.bits = .none

// If you have a DirectoryPath, then changes can be made recursively:
var dir = DirectoryPath(path)

try dir.recursiveChange(owner: .readWriteExecute, group: .readWrite, others: .read)

移动路径

var path = GenericPath("/tmp/testFile")

// Both of these things will move testFile from /tmp/testFile to ~/testFile
try path.move(to: DirectoryPath.home! + "testFile")
try path.move(into: DirectoryPath.home!)

// This renames a file in place
try path.rename(to: "newTestFile")

Globbing

let globData = try glob(pattern: "/tmp/*")

// Just like getting a directories children:
print(globData.files)
print(globData.directories)
print(globData.other)

// You can also glob from a DirectoryPath
let home = DirectoryPath.home

let globData = try home.glob("*.swift")

print(globData.files)
print(globData.directories)
print(globData.other)

临时路径

创建临时路径

let tmpFile = try FilePath.temporary()
// /tmp/vDjKM1C

let tmpDir = try DirectoryPath.temporary()
// /tmp/rYcznHQ

// You can optionally specify a prefix for the path name
let tmpFile = try FilePath.temporary(prefix: "com.pathman.")
// /tmp/com.pathman.gHyiZq

// You can optionally specify a base directory where the temporary path will be stored
let tmpDirectory = try DirectoryPath.temporary(base: DirectoryPath("/path/to/my/tmp")!, prefix: "com.pathman.")
// /path/to/my/tmp/com.pathman.2eH4iB

使用闭包

// When creating a temporary path with a closure, the path of the temporary
// file is returned instead of an Opened path
let tmpFile: FilePath = try FilePath.temporary() { openFile in
    try openFile.write("Hello World")
}

// You can also pass the .deleteOnCompletion option to the .temporary()
// function in order to delete the temporary path after the closure exits
// NOTE: This will recursively delete the temporary path if it is a DirectoryPath
try FilePath.temporary(options: .deleteOnCompletion) { openFile in
    try openFile.write("Hello World")
}

链接

从目标到目标

// You can link to an existing path
let dir = DirectoryPath("/tmp")

// Creates a soft/symbolic link to dir at the specified path
// All 3 of the following lines produce the same type of link
let link = try dir.link(at: "~/tmpDir.link")
let link = try dir.link(at: "~/tmpDir.symbolic", type: .symbolic)
let link = try dir.link(at: "~/tmpDir.soft", type: .soft)

// Creates a hard link to dir at the specified path
let link = try dir.link(at: "~/tmpDir.hard", type: .hard)

从目标到目标

let linkedFile = FilePath("/path/to/link/location")

// Creates a soft/symbolic link to dir at the specified path
// All 3 of the following lines produce the same type of link
let link = try linkedFile.link(from: "/path/to/link/target")
let link = try linkedFile.link(from: "/path/to/link/target", type: .symbolic)
let link = try linkedFile.link(from: "/path/to/link/target", type: .soft)

// Creates a hard link to dir at the specified path
let link = try linkedFile.link(from: "/path/to/link/target", type: .hard)

更改默认链接类型

Pathman 使用 .symbolic/.soft 链接作为默认值,但可以更改此设置。

Pathman.defaultLinkType = .hard

复制路径

FilePath

let file = FilePath("/path/to/file")

let copyPath = FilePath("/path/to/copy")

// Both these lines would result in the same thing
try file.copy(to: copyPath)
try file.copy(to: "/path/to/copy")

DirectoryPath

let dir = DirectoryPath("/path/to/directory")

let copyPath = DirectoryPath("/path/to/copy")

// Both these lines would result in the same thing
try dir.copy(to: copyPath)
try dir.copy(to: "/path/to/copy")

// NOTE: Copying directories will fail if the directory is not empty, so pass
// the recursive option to the copy call in order to sucessfully copy non empty
// directories
try dir.copy(to: copyPath, options: .recursive)

// NOTE: You may also include hidden files with the includeHidden option
try dir.copy(to: copyPath, options: [.recursive, .includeHidden])