tutanota/app-ios/calendar/Sources/Files/IosFileFacade.swift

221 lines
10 KiB
Swift
Raw Normal View History

import Foundation
import MobileCoreServices
import TutanotaSharedFramework
import UniformTypeIdentifiers
class IosFileFacade: FileFacade {
func openMacImportFileChooser() async throws -> [String] { fatalError("not implemented for this platform") }
let chooser: TUTFileChooser
let viewer: FileViewer
let schemeHandler: ApiSchemeHandler
let urlSession: URLSession
init(chooser: TUTFileChooser, viewer: FileViewer, schemeHandler: ApiSchemeHandler, urlSession: URLSession) {
self.chooser = chooser
self.viewer = viewer
self.schemeHandler = schemeHandler
self.urlSession = urlSession
}
func openFolderChooser() async throws -> String? { fatalError("not implemented for this platform") }
private func writeFile(_ file: String, _ content: DataWrapper) async throws {
let fileURL = URL(fileURLWithPath: file)
try content.data.write(to: fileURL, options: .atomic)
}
private func readFile(_ path: String) throws -> DataWrapper {
let data = try Data(contentsOf: URL(fileURLWithPath: path))
return data.wrap()
}
func open(_ location: String, _ mimeType: String) async throws { await self.viewer.openFile(path: location) }
func openFileChooser(_ boundingRect: IpcClientRect, _ filter: [String]?, _ isFileOnly: Bool? = false) async throws -> [String] {
let anchor = CGRect(x: boundingRect.x, y: boundingRect.y, width: boundingRect.width, height: boundingRect.height)
let files = try await self.chooser.open(withAnchorRect: anchor, isFileOnly: isFileOnly!)
var returnfiles = [String]()
for file in files {
let fileUrl = URL(fileURLWithPath: file)
let isDirectory: Bool = (try? fileUrl.resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true
// This should only be for files, but sometimes a directory masquerading as a file can slip through (such as a .band file).
// In those cases we just zip and add it.
if isDirectory {
let destinationPath = try zipDirectory(fileUrl: fileUrl)
returnfiles.append(destinationPath)
} else {
returnfiles.append(file)
}
}
return returnfiles
}
func deleteFile(_ file: String) async throws {
do { try FileManager.default.removeItem(atPath: file) } catch {
if let err = error as? NSError, err.code == NSFileNoSuchFileError { return printLog("Tried to delete file \(file) that does not exist.") }
throw TUTErrorFactory.wrapNativeError(withDomain: FILES_ERROR_DOMAIN, message: "Failed to delete file \(file)", error: error)
}
}
func getName(_ file: String) async throws -> String {
let fileName = (file as NSString).lastPathComponent
if FileUtils.fileExists(atPath: file) {
return fileName
} else {
throw TUTErrorFactory.createError(withDomain: FILES_ERROR_DOMAIN, message: "File does not exists")
}
}
func getMimeType(_ file: String) async throws -> String { getFileMIMETypeWithDefault(path: file) }
func getSize(_ file: String) async throws -> Int {
let attrs = try FileManager.default.attributesOfItem(atPath: file)
let size = attrs[.size] as! UInt64
// Technically we shouldn't do this but we are always running on 64bit devices and
// max Int64 number (even signed) is pretty huge so this is safe.
// If we somehow overflow we will actually crash.
return Int(size)
}
func putFileIntoDownloadsFolder(_ localFileUri: String, _ fileNameToSave: String) async throws -> String { fatalError("not implemented on this platform") }
func upload(_ sourceFileUrl: String, _ remoteUrl: String, _ method: String, _ headers: [String: String]) async throws -> UploadTaskResponse {
var request = URLRequest(url: URL(string: remoteUrl)!)
request.httpMethod = method
request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
request.allHTTPHeaderFields = headers
let (data, response) = try await self.urlSession.upload(for: self.schemeHandler.rewriteRequest(request), fromFile: URL(fileURLWithPath: sourceFileUrl))
let httpResponse = response as! HTTPURLResponse
return UploadTaskResponse(httpResponse: httpResponse, responseBody: data)
}
func download(_ sourceUrl: String, _ filename: String, _ headers: [String: String]) async throws -> DownloadTaskResponse {
let urlStruct = URL(string: sourceUrl)!
var request = URLRequest(url: urlStruct)
request.httpMethod = "GET"
request.allHTTPHeaderFields = headers
let (data, response) = try await self.urlSession.data(for: self.schemeHandler.rewriteRequest(request))
let httpResponse = response as! HTTPURLResponse
let encryptedFileUri: String?
if httpResponse.statusCode == 200 { encryptedFileUri = try self.writeEncryptedFile(fileName: filename, data: data) } else { encryptedFileUri = nil }
return DownloadTaskResponse(httpResponse: httpResponse, encryptedFileUri: encryptedFileUri)
}
private func writeEncryptedFile(fileName: String, data: Data) throws -> String {
let encryptedPath = try FileUtils.getEncryptedFolder()
let filePath = (encryptedPath as NSString).appendingPathComponent(fileName)
try data.write(to: URL(fileURLWithPath: filePath), options: .atomicWrite)
return filePath
}
func hashFile(_ fileUri: String) async throws -> String { try await BlobUtil().hashFile(fileUri: fileUri) }
func zipDirectory(fileUrl: URL) throws -> String {
var returnPath: String = ""
var err: NSError?
var ourError: Error?
NSFileCoordinator()
.coordinate(readingItemAt: fileUrl, options: [.forUploading], error: &err) { (zipUrl) in
do {
let decryptedFolder = try FileUtils.getDecryptedFolder()
let destinationPath = (decryptedFolder as NSString).appendingPathComponent((zipUrl.path as NSString).lastPathComponent)
try FileManager.default.copyItem(at: zipUrl, to: URL(fileURLWithPath: destinationPath))
returnPath = destinationPath
} catch {
ourError = error
return
}
}
if let e = err ?? ourError { throw TutanotaError(message: "could not read directory at \(fileUrl)", underlyingError: e) }
return returnPath
}
func clearFileData() async throws {
_ = await (
try self.clearDirectory(folderPath: FileUtils.getEncryptedFolder()), try self.clearDirectory(folderPath: FileUtils.getDecryptedFolder()),
try self.clearDirectory(folderPath: NSTemporaryDirectory())
)
}
func joinFiles(_ filename: String, _ files: [String]) async throws -> String { try await BlobUtil().joinFiles(fileName: filename, filePathsToJoin: files) }
func splitFile(_ fileUri: String, _ maxChunkSizeBytes: Int) async throws -> [String] {
try await BlobUtil().splitFile(fileUri: fileUri, maxBlobSize: maxChunkSizeBytes)
}
func writeTempDataFile(_ file: DataFile) async throws -> String {
let decryptedFolder = try FileUtils.getDecryptedFolder()
let filePath = (decryptedFolder as NSString).appendingPathComponent(file.name)
try await self.writeFile(filePath, file.data)
return filePath
}
func readDataFile(_ filePath: String) async throws -> DataFile? {
let data = try readFile(filePath)
return DataFile(name: try await getName(filePath), mimeType: try await getMimeType(filePath), size: try await getSize(filePath), data: data)
}
func writeToAppDir(_ content: TutanotaSharedFramework.DataWrapper, _ name: String) async throws {
let supportDir = try FileUtils.getApplicationSupportFolder()
let filePath = supportDir.appendingPathComponent(name)
try await self.writeFile(filePath.path, content)
}
func readFromAppDir(_ name: String) throws -> TutanotaSharedFramework.DataWrapper {
let supportDir = try FileUtils.getApplicationSupportFolder()
let filePath = supportDir.appendingPathComponent(name)
return try self.readFile(filePath.path)
}
private func clearDirectory(folderPath: String) async throws {
let fileManager = FileManager.default
let folderUrl = URL(fileURLWithPath: folderPath)
let files = try fileManager.contentsOfDirectory(at: folderUrl, includingPropertiesForKeys: nil, options: [])
for file in files where !file.hasDirectoryPath { try fileManager.removeItem(at: file) }
}
}
extension UploadTaskResponse {
init(httpResponse: HTTPURLResponse, responseBody: Data) {
self.init(
statusCode: httpResponse.statusCode,
errorId: httpResponse.valueForHeaderField("Error-Id"),
precondition: httpResponse.valueForHeaderField("Precondition"),
suspensionTime: httpResponse.valueForHeaderField("Retry-After") ?? httpResponse.valueForHeaderField("Suspension-Time"),
responseBody: responseBody.wrap()
)
}
}
extension DownloadTaskResponse {
init(httpResponse: HTTPURLResponse, encryptedFileUri: String?) {
self.init(
statusCode: httpResponse.statusCode,
errorId: httpResponse.valueForHeaderField("Error-Id"),
precondition: httpResponse.valueForHeaderField("Precondition"),
suspensionTime: httpResponse.valueForHeaderField("Retry-After") ?? httpResponse.valueForHeaderField("Suspension-Time"),
encryptedFileUri: encryptedFileUri
)
}
}
func getFileMIMETypeWithDefault(path: String) -> String { getFileMIMEType(path: path) ?? "application/octet-stream" }
func getFileMIMEType(path: String) -> String? {
// UTType is only available since iOS 15.
// We take retainedValue because both functions create new object and we
// are responsible for deallocating them.
// see https://developer.apple.com/documentation/swift/imported_c_and_objective-c_apis/working_with_core_foundation_types
// see https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFMemoryMgmt/Concepts/Ownership.html#//apple_ref/doc/uid/20001148
let UTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (path as NSString).pathExtension as CFString, nil)!.takeRetainedValue()
let MIMEUTI = UTTypeCopyPreferredTagWithClass(UTI, kUTTagClassMIMEType)?.takeRetainedValue()
return MIMEUTI as String?
}
/// Reading header fields from HTTPURLResponse.allHeaderFields is case-sensitive, it is a bug: https://bugs.swift.org/browse/SR-2429
/// From iOS13 we have a method to read headers case-insensitively: HTTPURLResponse.value(forHTTPHeaderField:)
/// For older iOS we use this NSDictionary cast workaround as suggested by a commenter in the bug report.
extension HTTPURLResponse { public func valueForHeaderField(_ headerField: String) -> String? { value(forHTTPHeaderField: headerField) } }