// Copyright 2024-present 650 Industries. All rights reserved. import ExpoModulesCore @available(iOS 14, tvOS 14, *) public final class FileSystemModule: Module { #if os(iOS) private lazy var filePickingHandler = FilePickingHandler(module: self) #endif private let downloadStore = DownloadTaskStore() var documentDirectory: URL? { return appContext?.config.documentDirectory } var cacheDirectory: URL? { return appContext?.config.cacheDirectory } var totalDiskSpace: Int64? { guard let path = documentDirectory?.path, let attributes = try? FileManager.default.attributesOfFileSystem(forPath: path) else { return nil } return attributes[.systemSize] as? Int64 } var availableDiskSpace: Int64? { guard let path = documentDirectory?.path, let attributes = try? FileManager.default.attributesOfFileSystem(forPath: path) else { return nil } return attributes[.systemFreeSize] as? Int64 } public func definition() -> ModuleDefinition { Name("FileSystem") Events("downloadProgress") Constant("documentDirectory") { return documentDirectory?.absoluteString } Constant("cacheDirectory") { return cacheDirectory?.absoluteString } Constant("bundleDirectory") { return Bundle.main.bundlePath } Constant("appleSharedContainers") { return getAppleSharedContainers() } Property("totalDiskSpace") { return totalDiskSpace } Property("availableDiskSpace") { return availableDiskSpace } AsyncFunction("downloadFileAsync") { (url: URL, to: FileSystemPath, options: DownloadOptions?, downloadUuid: String?, promise: Promise) in try downloadFileWithStore( url: url, to: to, options: options, downloadUuid: downloadUuid, downloadStore: self.downloadStore, promise: promise, sendEvent: { [weak self] name, body in self?.sendEvent(name, body) } ) } Function("cancelDownloadAsync") { (downloadUuid: String) in self.downloadStore.cancel(uuid: downloadUuid) } AsyncFunction("pickDirectoryAsync") { (initialUri: URL?, promise: Promise) in #if os(iOS) filePickingHandler.presentDocumentPicker( picker: createDirectoryPicker(initialUri: initialUri), isDirectory: true, initialUri: initialUri, mimeTypes: [], multipleDocuments: false, promise: promise ) #else promise.reject(FeatureNotAvailableOnPlatformException()) #endif }.runOnQueue(.main) AsyncFunction("pickFileAsync") { (options: FilePickingOptions?, promise: Promise) in #if os(iOS) filePickingHandler.presentDocumentPicker( picker: createFilePicker(initialUri: options?.initialUri, mimeTypes: options?.mimeTypes ?? []), isDirectory: false, initialUri: options?.initialUri, mimeTypes: options?.mimeTypes ?? [], multipleDocuments: options?.multipleFiles ?? false, promise: promise ) #else promise.reject(FeatureNotAvailableOnPlatformException()) #endif }.runOnQueue(.main) Function("info") { (url: URL) in let output = PathInfo() output.exists = false output.isDirectory = nil guard let fileSystemManager = appContext?.fileSystem else { return output } if fileSystemManager.getPathPermissions(url.path).contains(.read) { var isDirectory: ObjCBool = false if FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory) { output.exists = true output.isDirectory = isDirectory.boolValue return output } } return output } // swiftlint:disable:next closure_body_length Class(FileSystemFile.self) { Constructor { (url: URL) in return FileSystemFile(url: url.standardizedFileURL) } // we can't throw in a constructor, so this is a workaround Function("validatePath") { file in try file.validatePath() } // maybe asString, readAsString, readAsText, readText, ect. AsyncFunction("text") { file in return try file.text() } Function("textSync") { file in return try file.text() } AsyncFunction("base64") { file in return try file.base64() } Function("base64Sync") { file in return try file.base64() } AsyncFunction("bytes") { file in return try file.bytes() } Function("bytesSync") { file in return try file.bytes() } Function("open") { (file: FileSystemFile, mode: FileMode?) in return try FileSystemFileHandle(file: file, mode: mode) } Function("info") { (file: FileSystemFile, options: InfoOptions?) in return try file.info(options: options ?? InfoOptions()) } Function("write") { (file: FileSystemFile, content: Either, options: WriteOptions?) in let append = options?.append ?? false if let content: String = content.get() { if options?.encoding == WriteEncoding.base64 { guard let data = Data(base64Encoded: content, options: .ignoreUnknownCharacters) else { throw UnableToWriteBase64DataException(file.url.absoluteString) } try file.write(data, append: append) } else { try file.write(content, append: append) } } if let content: TypedArray = content.get() { try file.write(content, append: append) } } Property("size") { file in try? file.size } Property("md5") { file in try? file.md5 } Property("modificationTime") { file in try? file.modificationTime } Property("lastModified") { file in try? file.modificationTime } Property("creationTime") { file in try? file.creationTime } Property("type") { file in file.type } Function("delete") { file in try file.delete() } Property("exists") { file in return file.exists } Function("create") { (file, options: CreateOptions?) in try file.create(options ?? CreateOptions()) } AsyncFunction("copy") { (file, to: FileSystemPath, options: RelocationOptions?) in try file.copy(to: to, options: options ?? RelocationOptions()) } Function("copySync") { (file, to: FileSystemPath, options: RelocationOptions?) in try file.copy(to: to, options: options ?? RelocationOptions()) } AsyncFunction("move") { (file, to: FileSystemPath, options: RelocationOptions?) in try file.move(to: to, options: options ?? RelocationOptions()) } Function("moveSync") { (file, to: FileSystemPath, options: RelocationOptions?) in try file.move(to: to, options: options ?? RelocationOptions()) } Function("rename") { (file, newName: String) in try file.rename(newName) } Property("uri") { file in return file.url.absoluteString } } Class(FileSystemFileHandle.self) { Function("readBytes") { (fileHandle, bytes: Int) in try fileHandle.read(bytes) } Function("writeBytes") { (fileHandle, bytes: Data) in try fileHandle.write(bytes) } Function("close") { fileHandle in try fileHandle.close() } Property("offset") { fileHandle in fileHandle.offset }.set { (fileHandle, volume: UInt64) in fileHandle.offset = volume } Property("size") { fileHandle in fileHandle.size } } // swiftlint:disable:next closure_body_length Class(FileSystemDirectory.self) { Constructor { (url: URL) in return FileSystemDirectory(url: url.standardizedFileURL) } Function("info") { directory in try directory.info() } // we can't throw in a constructor, so this is a workaround Function("validatePath") { directory in try directory.validatePath() } Function("delete") { directory in try directory.delete() } Property("exists") { directory in return directory.exists } Function("create") { (directory, options: CreateOptions?) in try directory.create(options ?? CreateOptions()) } AsyncFunction("copy") { (directory, to: FileSystemPath, options: RelocationOptions?) in try directory.copy(to: to, options: options ?? RelocationOptions()) } Function("copySync") { (directory, to: FileSystemPath, options: RelocationOptions?) in try directory.copy(to: to, options: options ?? RelocationOptions()) } AsyncFunction("move") { (directory, to: FileSystemPath, options: RelocationOptions?) in try directory.move(to: to, options: options ?? RelocationOptions()) } Function("moveSync") { (directory, to: FileSystemPath, options: RelocationOptions?) in try directory.move(to: to, options: options ?? RelocationOptions()) } Function("rename") { (directory, newName: String) in try directory.rename(newName) } // this function is internal and will be removed in the future (when returning arrays of shared objects is supported) Function("listAsRecords") { directory in try directory.listAsRecords() } Function("createFile") { (directory, name: String, content: String?) in let file = FileSystemFile(url: directory.url.appendingPathComponent(name)) try file.create(CreateOptions()) return file } Function("createDirectory") { (directory, name: String) in let newDirectory = FileSystemDirectory(url: directory.url.appendingPathComponent(name)) try newDirectory.create(CreateOptions()) return newDirectory } Property("uri") { directory in return directory.url.absoluteString } Property("size") { directory in return try? directory.size } } Class(FileSystemUploadTask.self) { Constructor { return FileSystemUploadTask() } AsyncFunction("start") { (task: FileSystemUploadTask, url: URL, file: FileSystemFile, options: UploadTaskOptions, promise: Promise) in task.start(url: url, file: file, options: options, promise: promise) } Function("cancel") { (task: FileSystemUploadTask) in task.cancel() } } Class(FileSystemDownloadTask.self) { Constructor { return FileSystemDownloadTask() } AsyncFunction("start") { (task: FileSystemDownloadTask, url: URL, to: FileSystemPath, options: DownloadTaskOptions?, promise: Promise) in try to.validatePermission(.write) task.start(url: url, to: to, options: options, promise: promise) } AsyncFunction("pause") { (task: FileSystemDownloadTask) -> [String: String?] in return await task.pause() } AsyncFunction("resume") { (task: FileSystemDownloadTask, url: URL, to: FileSystemPath, resumeData: String, options: DownloadTaskOptions?, promise: Promise) in try to.validatePermission(.write) task.resume(url: url, to: to, resumeData: resumeData, options: options, promise: promise) } Function("cancel") { (task: FileSystemDownloadTask) in task.cancel() } } Class(FileSystemWatcher.self) { Constructor { (path: URL, options: WatchOptions?) in try FileSystemWatcher(path: path, options: options) } Function("start") { watcher in watcher.start() } Function("stop") { watcher in watcher.stop() } } } private func getAppleSharedContainers() -> [String: String] { guard let appContext else { return [:] } var result: [String: String] = [:] for appGroup in appContext.appCodeSignEntitlements.appGroups ?? [] { if let directory = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) { result[appGroup] = directory.standardizedFileURL.path } } return result } }