1 import ABI49_0_0ExpoModulesCore 2 import UIKit 3 import MobileCoreServices 4 5 struct PickingContext { 6 let promise: Promise 7 let options: DocumentPickerOptions 8 let delegate: DocumentPickingDelegate 9 } 10 11 public class DocumentPickerModule: Module, PickingResultHandler { 12 private var pickingContext: PickingContext? 13 14 public func definition() -> ModuleDefinition { 15 Name("ExpoDocumentPicker") 16 17 AsyncFunction("getDocumentAsync") { (options: DocumentPickerOptions, promise: Promise) in 18 if pickingContext != nil { 19 throw PickingInProgressException() 20 } 21 22 guard let currentVc = appContext?.utilities?.currentViewController() else { 23 throw MissingViewControllerException() 24 } 25 26 let documentPickerVC = createDocumentPicker(with: options) 27 let pickerDelegate = DocumentPickingDelegate(resultHandler: self) 28 29 pickingContext = PickingContext(promise: promise, options: options, delegate: pickerDelegate) 30 31 documentPickerVC.delegate = pickerDelegate 32 documentPickerVC.presentationController?.delegate = pickerDelegate 33 documentPickerVC.allowsMultipleSelection = options.multiple 34 35 if UIDevice.current.userInterfaceIdiom == .pad { 36 let viewFrame = currentVc.view.frame 37 documentPickerVC.popoverPresentationController?.sourceRect = CGRect( 38 x: viewFrame.midX, 39 y: viewFrame.maxY, 40 width: 0, 41 height: 0 42 ) 43 documentPickerVC.popoverPresentationController?.sourceView = currentVc.view 44 documentPickerVC.modalPresentationStyle = .pageSheet 45 } 46 currentVc.present(documentPickerVC, animated: true) 47 }.runOnQueue(.main) 48 } 49 50 func didPickDocumentsAt(urls: [URL]) { 51 guard let options = self.pickingContext?.options, 52 let promise = self.pickingContext?.promise else { 53 log.error("Picking context has been lost.") 54 return 55 } 56 pickingContext = nil 57 58 do { 59 if options.multiple { 60 let assets = try urls.map { 61 try readDocumentDetails( 62 documentUrl: $0, 63 copy: options.copyToCacheDirectory 64 ) 65 } 66 promise.resolve(DocumentPickerResponse(assets: assets)) 67 } else { 68 let asset = try readDocumentDetails( 69 documentUrl: urls[0], 70 copy: options.copyToCacheDirectory 71 ) 72 promise.resolve(DocumentPickerResponse(assets: [asset])) 73 } 74 } catch { 75 promise.reject(error) 76 } 77 } 78 79 func didCancelPicking() { 80 guard let context = pickingContext else { 81 log.error("Picking context lost") 82 return 83 } 84 85 pickingContext = nil 86 context.promise.resolve(DocumentPickerResponse(canceled: true)) 87 } 88 89 private func getFileSize(path: URL) -> Int? { 90 guard let resource = try? path.resourceValues(forKeys: [.fileSizeKey, .isDirectoryKey]) else { 91 return 0 92 } 93 94 if let isDirectory = resource.isDirectory { 95 if !isDirectory { 96 return resource.fileSize 97 } 98 } 99 100 guard let contents = try? FileManager.default.contentsOfDirectory(atPath: path.absoluteString) else { 101 return 0 102 } 103 104 let folderSize = contents.reduce(0) { currentSize, file in 105 let fileSize = getFileSize(path: path.appendingPathComponent(file)) ?? 0 106 return currentSize + fileSize 107 } 108 109 return folderSize 110 } 111 112 private func readDocumentDetails(documentUrl: URL, copy: Bool) throws -> DocumentInfo { 113 var newUrl = documentUrl 114 115 guard let fileSystem = self.appContext?.fileSystem else { 116 throw Exceptions.FileSystemModuleNotFound() 117 } 118 119 guard let fileSize = try? getFileSize(path: documentUrl) else { 120 throw InvalidFileException() 121 } 122 123 if copy { 124 let cacheDirURL = URL(fileURLWithPath: fileSystem.cachesDirectory) 125 let directory = cacheDirURL.appendingPathComponent("DocumentPicker", isDirectory: true).path 126 let fileExtension = "." + documentUrl.pathExtension 127 let path = fileSystem.generatePath(inDirectory: directory, withExtension: fileExtension) 128 newUrl = URL(fileURLWithPath: path) 129 130 try FileManager.default.copyItem(at: documentUrl, to: newUrl) 131 } 132 133 let mimeType = self.getMimeType(from: documentUrl.pathExtension) 134 135 return DocumentInfo( 136 uri: newUrl.absoluteString, 137 name: documentUrl.lastPathComponent, 138 size: fileSize, 139 mimeType: mimeType 140 ) 141 } 142 143 private func getMimeType(from pathExtension: String) -> String? { 144 if #available(iOS 14, *) { 145 return UTType(filenameExtension: pathExtension)?.preferredMIMEType 146 } else { 147 if let uti = UTTypeCreatePreferredIdentifierForTag( 148 kUTTagClassFilenameExtension, 149 pathExtension as NSString, nil 150 )?.takeRetainedValue() { 151 if let mimetype = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)?.takeRetainedValue() { 152 return mimetype as String 153 } 154 } 155 return nil 156 } 157 } 158 159 private func createDocumentPicker(with options: DocumentPickerOptions) -> UIDocumentPickerViewController { 160 if #available(iOS 14.0, *) { 161 let utTypes = options.type.compactMap { $0.toUTType() } 162 return UIDocumentPickerViewController( 163 forOpeningContentTypes: utTypes, 164 asCopy: true 165 ) 166 } else { 167 let utiTypes = options.type.map { $0.toUTI() } 168 return UIDocumentPickerViewController( 169 documentTypes: utiTypes, 170 in: .import 171 ) 172 } 173 } 174 } 175