最近在给Filebox添加了nfs的功能,苦于没有现成的轮子可用,于是就参考 AMSMB2 自己撸了一个库: NFSKit

由于尝试使用脚本将libnfs打包为libnfs.a失败,索性就直接复制源代码做成一个nfs的Swift Package. 然后在Package 中定义一下宏,当然可能有一些重复的宏定义,等有时间的可以优化一下。 甚至可以写一个bash脚本,自动从libnfs 拉取代码,复制到Sources目录下。


import PackageDescription

let package = Package(
    name: "nfs",
    products: [
        // Products define the executables and libraries a package produces, and make them visible to other packages.
        .library(
            name: "nfs",
            targets: ["nfs"]),
    ],
    dependencies: [
        // Dependencies declare other packages that this package depends on.
        // .package(url: /* package url */, from: "1.0.0"),
    ],
    targets: [
        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
        // Targets can depend on other targets in this package, and on products in packages this package depends on.
        .target(
            name: "nfs",
            dependencies: [], cSettings: [
                .headerSearchPath("include/nfsc"),
                .headerSearchPath("include"),
                .headerSearchPath("mount"),
                .headerSearchPath("nfs"),
                .headerSearchPath("nfs4"),
                .headerSearchPath("nlm"),
                .headerSearchPath("portmap"),
                .define("HAVE_CONFIG_H", to: "1"),
                .define("_U_", to: "__attribute__((unused))"),
                .define("HAVE_GETPWNAM", to: "1"),
                .define("HAVE_SOCKADDR_LEN", to: "1"),
                .define("HAVE_SOCKADDR_STORAGE", to: "1"),
                .define("HAVE_TALLOC_TEVENT", to: "1")
            ]),
        .testTarget(
            name: "nfsTests",
            dependencies: ["nfs"]),
    ]
)

由于nfs 中全是c代码,在实际项目中使用的时候肯定不方便,需要给nfs再包装一层:NFSKit。代码很多直接从 AMSMB2 复制而来,就不多说了,直接看源代码。

import NFSKit

class NFSBrowser {
    
    var client: NFSClient?
    
    // url: nfs://xxx.xxx.xxx.xxx
    init?(url: URL) throws {
        client = try NFSClient(url: url)
    }
    
    func listExports(handler: @escaping (Result<[String], Error>) -> Void) {
        client?.listExports(completionHandler: handler)
    }
    
    func mount(export: String, completion: @escaping (Result<Void, Error>) -> Void) {
        client?.connect(export: export) { error in
            DispatchQueue.main.async {
                if let error = error {
                    completion(.failure(error))
                } else {
                    completion(.success(()))
                }
            }
        }
    }

    func listDirectory(at path: String) {
        client?.contentsOfDirectory(atPath: path) { result in
            switch result {
            case .success(let items):
                for entry in items {
                    print("name:", entry[.nameKey] as! String,
                          ", path:", entry[.pathKey] as! String,
                          ", type:", entry[.fileResourceTypeKey] as! URLFileResourceType,
                          ", size:", entry[.fileSizeKey] as! Int64,
                          ", modified:", entry[.contentModificationDateKey] as! Date,
                          ", created:", entry[.creationDateKey] as! Date)
                }
            case .failure(let error):
                print(error)
            }
        }
    }

    func moveItem(atPath path: String, to toPath: String) {
        client?.moveItem(atPath: path, toPath: toPath) { error in
            if let error = error {
                print(error)
            }
        }
    }

    func removeItem(atPath path: String) {
        client?.removeItem(atPath: path) { error in
            if let error = error {
                print(error)
            }
        }
    }
    
    func downloadItem(atPath path: String) {
        let filePath = NSTemporaryDirectory().appending("temp.data")
        let fileURL = URL(fileURLWithPath: filePath)
        let progress = Progress(totalUnitCount: 0)
        client?.downloadItem(atPath: path, to: fileURL) { bytes, total in
            progress.totalUnitCount = total
            progress.completedUnitCount = bytes
            print(progress.fractionCompleted)
            return true
        } completionHandler: { error in
            if let error = error {
                print(error)
            }
        }
    }
    
    func upload(data: Data, toPath: String) {
        let progress = Progress(totalUnitCount: Int64(data.count))
        client?.write(data: data, toPath: toPath) { (uploaded) -> Bool in
            progress.completedUnitCount = uploaded
            print(progress.fractionCompleted)
            return true
        } completionHandler: { error in
            if let error = error {
                print(error)
            }
        }
    }
}