import Foundation import Capacitor import IONFilesystemLib typealias FileService = any IONFILEDirectoryManager & IONFILEFileManager /** * Please read the Capacitor iOS Plugin Development Guide * here: https://capacitorjs.com/docs/plugins/ios */ @objc(FilesystemPlugin) public class FilesystemPlugin: CAPPlugin, CAPBridgedPlugin { public let identifier = "FilesystemPlugin" public let jsName = "Filesystem" public let pluginMethods: [CAPPluginMethod] = [ CAPPluginMethod(name: "readFile", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "readFileInChunks", returnType: CAPPluginReturnCallback), CAPPluginMethod(name: "writeFile", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "appendFile", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "deleteFile", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "mkdir", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "rmdir", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "readdir", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "getUri", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "stat", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "rename", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "copy", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "checkPermissions", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "requestPermissions", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "downloadFile", returnType: CAPPluginReturnPromise) ] private let legacyImplementation = LegacyFilesystemImplementation() private var fileService: FileService? override public func load() { self.fileService = IONFILEManager() } func getService() -> Result { if fileService == nil { load() } return fileService.map(Result.success) ?? .failure(.bridgeNotInitialised) } @objc override public func checkPermissions(_ call: CAPPluginCall) { call.handlePermissionSuccess() } @objc override public func requestPermissions(_ call: CAPPluginCall) { call.handlePermissionSuccess() } } // MARK: - Public API Methods private extension FilesystemPlugin { /** * Read a file from the filesystem. */ @objc func readFile(_ call: CAPPluginCall) { let encoding = call.getEncoding(Constants.MethodParameter.encoding) performSinglePathOperation(call) { .readFile(url: $0, encoding: encoding) } } @objc func readFileInChunks(_ call: CAPPluginCall) { let encoding = call.getEncoding(Constants.MethodParameter.encoding) guard let chunkSize = call.getInt(Constants.MethodParameter.chunkSize) else { return call.handleError(.invalidInput(method: call.getIONFileMethod())) } performSinglePathOperation(call) { .readFileInChunks(url: $0, encoding: encoding, chunkSize: chunkSize) } } /** * Write a file to the filesystem. */ @objc func writeFile(_ call: CAPPluginCall) { guard let encodingMapper = call.getEncodingMapper() else { return call.handleError(.invalidInput(method: call.getIONFileMethod())) } let recursive = call.getBool(Constants.MethodParameter.recursive, false) performSinglePathOperation(call) { .write(url: $0, encodingMapper: encodingMapper, recursive: recursive) } } /** * Append to a file. */ @objc func appendFile(_ call: CAPPluginCall) { guard let encodingMapper = call.getEncodingMapper() else { return call.handleError(.invalidInput(method: call.getIONFileMethod())) } let recursive = call.getBool(Constants.MethodParameter.recursive, false) performSinglePathOperation(call) { .append(url: $0, encodingMapper: encodingMapper, recursive: recursive) } } /** * Delete a file. */ @objc func deleteFile(_ call: CAPPluginCall) { performSinglePathOperation(call) { .delete(url: $0) } } /** * Make a new directory, optionally creating parent folders first. */ @objc func mkdir(_ call: CAPPluginCall) { let recursive = call.getBool(Constants.MethodParameter.recursive, false) performSinglePathOperation(call) { .mkdir(url: $0, recursive: recursive) } } /** * Remove a directory. */ @objc func rmdir(_ call: CAPPluginCall) { let recursive = call.getBool(Constants.MethodParameter.recursive, false) performSinglePathOperation(call) { .rmdir(url: $0, recursive: recursive) } } /** * Read the contents of a directory. */ @objc func readdir(_ call: CAPPluginCall) { performSinglePathOperation(call) { .readdir(url: $0) } } @objc func stat(_ call: CAPPluginCall) { performSinglePathOperation(call) { .stat(url: $0) } } @objc func getUri(_ call: CAPPluginCall) { performSinglePathOperation(call) { .getUri(url: $0) } } /** * Rename a file or directory. */ @objc func rename(_ call: CAPPluginCall) { performDualPathOperation(call) { .rename(source: $0, destination: $1) } } /** * Copy a file or directory. */ @objc func copy(_ call: CAPPluginCall) { performDualPathOperation(call) { .copy(source: $0, destination: $1) } } /** * [DEPRECATED] Download a file */ @available(*, deprecated, message: "Use @capacitor/file-transfer plugin instead.") @objc func downloadFile(_ call: CAPPluginCall) { guard let url = call.getString("url") else { return call.reject("Must provide a URL") } let progressEmitter: LegacyFilesystemImplementation.ProgressEmitter = { bytes, contentLength in self.notifyListeners("progress", data: [ "url": url, "bytes": bytes, "contentLength": contentLength ]) } do { try legacyImplementation.downloadFile(call: call, emitter: progressEmitter, config: bridge?.config) } catch let error { call.reject(error.localizedDescription) } } } // MARK: - Operation Execution private extension FilesystemPlugin { func performSinglePathOperation(_ call: CAPPluginCall, operationBuilder: (URL) -> FilesystemOperation) { executeOperation(call) { service in FilesystemLocationResolver(service: service) .resolveSinglePath(from: call) .map { operationBuilder($0) } } } func performDualPathOperation(_ call: CAPPluginCall, operationBuilder: (URL, URL) -> FilesystemOperation) { executeOperation(call) { service in FilesystemLocationResolver(service: service) .resolveDualPaths(from: call) .map { operationBuilder($0.source, $0.destination) } } } func executeOperation(_ call: CAPPluginCall, operationProvider: (FileService) -> Result) { switch getService() { case .success(let service): switch operationProvider(service) { case .success(let operation): let executor = FilesystemOperationExecutor(service: service) executor.execute(operation, call) case .failure(let error): call.handleError(error) } case .failure(let error): call.handleError(error) } } }