1 import ExpoModulesCore 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 let pathExtension = documentUrl.pathExtension 114 var newUrl = documentUrl 115 116 guard let fileSystem = self.appContext?.fileSystem else { 117 throw Exceptions.FileSystemModuleNotFound() 118 } 119 120 guard let fileSize = try? getFileSize(path: documentUrl) else { 121 throw InvalidFileException() 122 } 123 124 if copy { 125 let directory = fileSystem.cachesDirectory.appending("DocumentPicker") 126 let path = fileSystem.generatePath(inDirectory: directory, withExtension: pathExtension) 127 newUrl = URL(fileURLWithPath: path) 128 try FileManager.default.copyItem(at: documentUrl, to: newUrl) 129 } 130 131 let mimeType = self.getMimeType(from: pathExtension) 132 133 return DocumentInfo( 134 uri: newUrl.absoluteString, 135 name: documentUrl.lastPathComponent, 136 size: fileSize, 137 mimeType: mimeType 138 ) 139 } 140 141 private func getMimeType(from pathExtension: String) -> String? { 142 if #available(iOS 14, *) { 143 return UTType(filenameExtension: pathExtension)?.preferredMIMEType 144 } else { 145 if let uti = UTTypeCreatePreferredIdentifierForTag( 146 kUTTagClassFilenameExtension, 147 pathExtension as NSString, nil 148 )?.takeRetainedValue() { 149 if let mimetype = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)?.takeRetainedValue() { 150 return mimetype as String 151 } 152 } 153 return nil 154 } 155 } 156 157 private func createDocumentPicker(with options: DocumentPickerOptions) -> UIDocumentPickerViewController { 158 if #available(iOS 14.0, *) { 159 let utTypes = options.type.compactMap { $0.toUTType() } 160 return UIDocumentPickerViewController( 161 forOpeningContentTypes: utTypes, 162 asCopy: true 163 ) 164 } else { 165 let utiTypes = options.type.map { $0.toUTI() } 166 return UIDocumentPickerViewController( 167 documentTypes: utiTypes, 168 in: .import 169 ) 170 } 171 } 172 } 173