// // Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // import MultipeerConnectivity import SignalServiceKit class DeviceTransferOperation: OWSOperation { public struct CancelError: Error {} let file: DeviceTransferProtoFile let promise: Promise private let future: Future class func scheduleTransfer(file: DeviceTransferProtoFile, priority: Operation.QueuePriority = .normal) -> Promise { let operation = DeviceTransferOperation(file: file) operationQueue.addOperation(operation) return operation.promise } class func cancelAllOperations() { operationQueue.cancelAllOperations() } private static let operationQueue: OperationQueue = { let queue = OperationQueue() queue.name = logTag() queue.maxConcurrentOperationCount = 10 return queue }() private init(file: DeviceTransferProtoFile) { self.file = file (self.promise, self.future) = Promise.pending() super.init() } // MARK: - Run override func reportSuccess() { super.reportSuccess() future.resolve() } override func didReportError(_ error: Error) { super.didReportError(error) future.reject(error) } override func reportCancelled() { super.reportCancelled() future.reject(CancelError()) } override public func run() { Logger.info("Transferring file: \(file.identifier), estimatedSize: \(file.estimatedSize)") // Use the main thread for all MCSession related operations. // There shouldn't be anything else going on in the app, anyway. DispatchQueue.main.async { self.prepareForSending() } } private var progress: Progress? private func prepareForSending() { guard case .outgoing(let newDevicePeerId, _, _, let transferredFiles, let progress) = deviceTransferService.transferState else { return reportError(OWSAssertionError("Tried to transfer file while in unexpected state: \(deviceTransferService.transferState)")) } guard !transferredFiles.contains(file.identifier) else { Logger.info("File was already transferred, skipping") return reportSuccess() } var url = URL(fileURLWithPath: file.relativePath, relativeTo: DeviceTransferService.appSharedDataDirectory) if !OWSFileSystem.fileOrFolderExists(url: url) { guard ![ DeviceTransferService.databaseWALIdentifier, DeviceTransferService.databaseIdentifier ].contains(file.identifier) else { return reportError(OWSAssertionError("Mandatory database file is missing for transfer")) } Logger.warn("Missing file for transfer, it probably disappeared or was otherwise deleted. Sending missing file placeholder.") url = URL( fileURLWithPath: UUID().uuidString, relativeTo: URL(fileURLWithPath: OWSTemporaryDirectory(), isDirectory: true) ) guard FileManager.default.createFile( atPath: url.path, contents: DeviceTransferService.missingFileData, attributes: nil ) else { return reportError(OWSAssertionError("Failed to create temp file for missing file \(url)")) } } guard let sha256Digest = try? Cryptography.computeSHA256DigestOfFile(at: url) else { return reportError(OWSAssertionError("Failed to calculate sha256 for file")) } guard let session = deviceTransferService.session else { return reportError(OWSAssertionError("Tried to transfer file with no active session")) } guard let fileProgress = session.sendResource( at: url, withName: file.identifier + " " + sha256Digest.hexadecimalString, toPeer: newDevicePeerId, withCompletionHandler: { [weak self] error in guard let self = self else { return } if let error = error { self.reportError(OWSAssertionError("Transferring file \(self.file.identifier) failed \(error)")) } else { Logger.info("Transferring file \(self.file.identifier) complete") self.deviceTransferService.transferState = self.deviceTransferService.transferState.appendingFileId(self.file.identifier) self.reportSuccess() } self.progress?.removeObserver(self, forKeyPath: "fractionCompleted") } ) else { return reportError(OWSAssertionError("Transfer of file failed \(file.identifier)")) } progress.addChild(fileProgress, withPendingUnitCount: Int64(file.estimatedSize)) self.progress = fileProgress fileProgress.addObserver(self, forKeyPath: "fractionCompleted", options: .initial, context: nil) } private var lastWholeNumberProgress = 0 override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { guard keyPath == "fractionCompleted", let progress = object as? Progress else { return super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) } let currentWholeNumberProgress = Int(progress.fractionCompleted * 100) let percentChange = currentWholeNumberProgress - lastWholeNumberProgress defer { lastWholeNumberProgress = currentWholeNumberProgress } // Determine how frequently to log progress updates. If in verbose mode, we log // every 1%. Otherwise, every 10%. guard percentChange >= (DebugFlags.deviceTransferVerboseProgressLogging ? 1 : 10) else { return } Logger.info("Transferring file \(self.file.identifier) \(currentWholeNumberProgress)%") } }