Temporarily put files in temp directory

This commit is contained in:
Leon Böttger committed 2025-04-03 20:56:41 +02:00
1 parent 867bf3fff5
commit ba1f49a711
21 files changed
+537 -447

No files matched your search

+12 -9
View File
@@ -115,7 +115,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
@objc func openWelcomeScreen() {
// Create the welcome screen SwiftUI view
let welcomeView = WelcomeScreen {
self.openPlusScreen(continueTransfer: {})
self.openPlusScreen()
}
// Create an NSWindow to host the SwiftUI view
@@ -142,11 +142,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
}
@objc func openPlusScreen(continueTransfer: @escaping () -> Void) {
@objc func openPlusScreen() {
// Create the welcome screen SwiftUI view
let plusView = GetPlusView(closeView: {
log("Closing plus screen")
self.plusWindow?.close()
continueTransfer()
})
// Create an NSWindow to host the SwiftUI view
@@ -210,8 +211,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
}
public func continueTransmission(accept: Bool, transferID: String) {
NearbyConnectionManager.shared.submitUserConsent(transferID: transferID, accept: accept)
public func continueTransmission(accept: Bool, transferID: String, storeInTemp: Bool = false) {
NearbyConnectionManager.shared.submitUserConsent(transferID: transferID, accept: accept, storeInTemp: storeInTemp)
if !accept {
activeIncomingTransfers.removeValue(forKey: transferID)
@@ -284,10 +285,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
private func pressAcceptButton(transferID: String) {
if !isPlusVersion() && transmissionCount() > 0 {
self.openPlusScreen {
self.continueTransmission(accept: self.isPlusVersion(), transferID: transferID)
}
if false || (!isPlusVersion() && transmissionCount() > 0) {
self.continueTransmission(accept: true, transferID: transferID, storeInTemp: true)
log("Showing plus screen...")
self.openPlusScreen()
}
else {
continueTransmission(accept: true, transferID: transferID)
+1 -1
View File
@@ -91,7 +91,7 @@ If you still encounter issues, the issues could be caused due to your WiFi route
"plusview_title" = "Thanks for using QuickDrop!";
"plusview_description" = "Unlock QuickDrop+ to continue exchanging files without limits. QuickDrop+ is a one-time purchase – no subscription required. All current and future features are included.";
"plusview_success_title" = "Thanks for your purchase!";
"plusview_success_description" = "You can now continue receiving files without restrictions.";
"plusview_success_description" = "You can now continue receiving files without restrictions. Any files you tried to send were already saved on your device.";
"plusview_success_button" = "Got it!";
"plusview_unlock_label" = "Support QuickDrop";
+9 -1
View File
@@ -7,6 +7,7 @@
import SwiftUI
import StoreKit
import NearbyShare
let plusViewWidth: CGFloat = 440
let plusViewHeight: CGFloat = 200
@@ -28,7 +29,14 @@ struct GetPlusView: View {
@State var buying = false
@State var errorText = ""
@State var boughtSuccessAlert = false
@State var boughtSuccessAlert = false {
didSet {
if boughtSuccessAlert {
log("User bought plus version. Moving pending files...")
SaveFilesManager.shared.movePendingFilesToTarget()
}
}
}
var body: some View {
+1 -1
View File
@@ -80,7 +80,7 @@
"plusview_title" = "شكرًا لاستخدامك QuickDrop!";
"plusview_description" = "قم بفتح QuickDrop+ لمواصلة تبادل الملفات بلا حدود. QuickDrop+ هو شراء لمرة واحدة – لا حاجة للاشتراك. تشمل جميع الميزات الحالية والمستقبلية.";
"plusview_success_title" = "شكرًا لشرائك!";
"plusview_success_description" = "يمكنك الآن متابعة استقبال الملفات بدون قيود.";
"plusview_success_description" = "يمكنك الآن متابعة استلام الملفات بدون قيود. أي ملفات حاولت إرسالها سابقًا تم حفظها بالفعل على جهازك.";
"plusview_success_button" = "حسنًا!";
"plusview_unlock_label" = "ادعم QuickDrop";
+1 -1
View File
@@ -88,7 +88,7 @@ Wenn du weiterhin Probleme hast, könnten diese durch deinen WLAN-Router verursa
"plusview_title" = "Danke, dass du QuickDrop benutzt!";
"plusview_description" = "Schalte QuickDrop+ frei, um weiterhin Dateien unbegrenzt auszutauschen. QuickDrop+ ist ein Einmalkauf - ein Abonnement ist nicht erforderlich. Alle aktuellen und zukünftigen Funktionen sind enthalten.";
"plusview_success_title" = "Danke für deinen Kauf!";
"plusview_success_description" = "Du kannst nun weiterhin Dateien ohne Einschränkungen empfangen.";
"plusview_success_description" = "Du kannst nun weiterhin Dateien ohne Einschränkungen empfangen. Alle Dateien, welche gerade gesendet wurden, wurden bereits gespeichert.";
"plusview_success_button" = "Alles klar!";
"plusview_unlock_label" = "Unterstütze QuickDrop";
+1 -1
View File
@@ -86,7 +86,7 @@ Si aún encuentras problemas, estos podrían ser causados por tu router WiFi. Al
"plusview_title" = "¡Gracias por usar QuickDrop!";
"plusview_description" = "Desbloquea QuickDrop+ para seguir intercambiando archivos sin límites. QuickDrop+ es una compra única – no se requiere suscripción. Todas las funciones actuales y futuras están incluidas.";
"plusview_success_title" = "¡Gracias por tu compra!";
"plusview_success_description" = "Ahora puedes seguir recibiendo archivos sin restricciones.";
"plusview_success_description" = "Ahora puedes seguir recibiendo archivos sin restricciones. Todos los archivos que intentaste enviar antes ya se han guardado en tu dispositivo.";
"plusview_success_button" = "¡Entendido!";
"plusview_unlock_label" = "Apoyar QuickDrop";
+1 -1
View File
@@ -85,7 +85,7 @@ Si vous rencontrez toujours des problèmes, ceux-ci peuvent être causés par vo
"plusview_title" = "Merci d'utiliser QuickDrop !";
"plusview_description" = "Débloquez QuickDrop+ pour continuer à échanger des fichiers sans limites. QuickDrop+ est un achat unique – aucun abonnement requis. Toutes les fonctionnalités actuelles et futures sont incluses.";
"plusview_success_title" = "Merci pour votre achat !";
"plusview_success_description" = "Vous pouvez maintenant continuer à recevoir des fichiers sans restrictions.";
"plusview_success_description" = "Vous pouvez maintenant continuer à recevoir des fichiers sans restrictions. Tous les fichiers que vous avez essayé d'envoyer précédemment ont déjà été enregistrés sur votre appareil.";
"plusview_success_button" = "D'accord !";
"plusview_unlock_label" = "Soutenir QuickDrop";
+1 -1
View File
@@ -86,7 +86,7 @@ Ha továbbra is problémákat tapasztalsz, azok a WiFi-routeredből adódhatnak.
"plusview_title" = "Köszönjük, hogy használod a QuickDropot!";
"plusview_description" = "Oldd fel a QuickDrop+-t, hogy korlátlanul folytathasd a fájlcserét. A QuickDrop+ egy egyszeri vásárlás – előfizetés nem szükséges. Az összes jelenlegi és jövőbeli funkciót tartalmazza.";
"plusview_success_title" = "Köszönjük a vásárlást!";
"plusview_success_description" = "Mostantól korlátozások nélkül fogadhatsz fájlokat.";
"plusview_success_description" = "Mostantól korlátozások nélkül fogadhat fájlokat. Az összes fájl, amelyet korábban próbált elküldeni, már el lett mentve az eszközére.";
"plusview_success_button" = "Értettem!";
"plusview_unlock_label" = "Támogasd a QuickDropot";
+1 -1
View File
@@ -85,7 +85,7 @@ Se il problema persiste, potrebbe essere causato dal tuo router WiFi. Alcuni rou
"plusview_title" = "Grazie per aver usato QuickDrop!";
"plusview_description" = "Sblocca QuickDrop+ per continuare a scambiare file senza limiti. QuickDrop+ è un acquisto unico – nessun abbonamento richiesto. Tutte le funzionalità attuali e future sono incluse.";
"plusview_success_title" = "Grazie per il tuo acquisto!";
"plusview_success_description" = "Ora puoi continuare a ricevere file senza restrizioni.";
"plusview_success_description" = "Ora puoi continuare a ricevere file senza restrizioni. Tutti i file che hai provato a inviare in precedenza sono già stati salvati sul tuo dispositivo.";
"plusview_success_button" = "Capito!";
"plusview_unlock_label" = "Supporta QuickDrop";
+1 -1
View File
@@ -85,7 +85,7 @@ AndroidデバイスからMacにファイルを送信するには、スマート
"plusview_title" = "QuickDropをご利用いただきありがとうございます!";
"plusview_description" = "QuickDrop+ をアンロックして、無制限にファイルを交換できます。QuickDrop+ は買い切り型のアプリです。サブスクリプション不要で、現在および将来のすべての機能が含まれます。";
"plusview_success_title" = "ご購入ありがとうございます!";
"plusview_success_description" = "これで、制限なくファイルを受信できます。";
"plusview_success_description" = "これで、制限なくファイルを受信し続けることができます。以前送信しようとしたファイルはすでにデバイスに保存されています。";
"plusview_success_button" = "了解!";
"plusview_unlock_label" = "QuickDropをサポート";
+1 -1
View File
@@ -85,7 +85,7 @@ Android 기기에서 Mac으로 파일을 보내려면 휴대폰에서 공유 아
"plusview_title" = "QuickDrop을 사용해 주셔서 감사합니다!";
"plusview_description" = "QuickDrop+를 잠금 해제하여 파일을 제한 없이 교환하세요. QuickDrop+는 일회성 구매이며, 구독이 필요하지 않습니다. 모든 현재 및 미래 기능이 포함됩니다.";
"plusview_success_title" = "구매해 주셔서 감사합니다!";
"plusview_success_description" = "이제 제한 없이 파일을 받을 수 있습니다.";
"plusview_success_description" = "이제 파일을 제한 없이 계속 받을 수 있습니다. 이전에 전송을 시도한 모든 파일이 이미 기기에 저장되었습니다.";
"plusview_success_button" = "알겠습니다!";
"plusview_unlock_label" = "QuickDrop 지원하기";
+1 -1
View File
@@ -86,7 +86,7 @@ Als je nog steeds problemen ondervindt, kunnen deze veroorzaakt worden door je W
"plusview_title" = "Bedankt voor het gebruiken van QuickDrop!";
"plusview_description" = "Ontgrendel QuickDrop+ om bestanden zonder limieten te blijven uitwisselen. QuickDrop+ is een eenmalige aankoop – geen abonnement vereist. Alle huidige en toekomstige functies zijn inbegrepen.";
"plusview_success_title" = "Bedankt voor je aankoop!";
"plusview_success_description" = "Je kunt nu bestanden blijven ontvangen zonder beperkingen.";
"plusview_success_description" = "Je kunt nu bestanden blijven ontvangen zonder beperkingen. Alle bestanden die je eerder probeerde te verzenden, zijn al opgeslagen op je apparaat.";
"plusview_success_button" = "Begrepen!";
"plusview_unlock_label" = "Ondersteun QuickDrop";
+1 -1
View File
@@ -86,7 +86,7 @@ Se você ainda encontrar problemas, eles podem ser causados pelo seu roteador Wi
"plusview_title" = "Obrigado por usar o QuickDrop!";
"plusview_description" = "Desbloqueie o QuickDrop+ para continuar trocando arquivos sem limites. QuickDrop+ é uma compra única – sem necessidade de assinatura. Todos os recursos atuais e futuros estão incluídos.";
"plusview_success_title" = "Obrigado pela sua compra!";
"plusview_success_description" = "Agora você pode continuar recebendo arquivos sem restrições.";
"plusview_success_description" = "Agora você pode continuar recebendo arquivos sem restrições. Todos os arquivos que você tentou enviar antes já foram salvos no seu dispositivo.";
"plusview_success_button" = "Entendi!";
"plusview_unlock_label" = "Apoie o QuickDrop";
+1 -1
View File
@@ -85,7 +85,7 @@ Se ainda tiver problemas, estes podem ser causados pelo seu router WiFi. Alguns
"plusview_title" = "Obrigado por usar o QuickDrop!";
"plusview_description" = "Desbloqueia o QuickDrop+ para continuar a trocar ficheiros sem limites. QuickDrop+ é uma compra única – não é necessária subscrição. Todas as funcionalidades atuais e futuras estão incluídas.";
"plusview_success_title" = "Obrigado pela sua compra!";
"plusview_success_description" = "Agora você pode continuar enviando arquivos sem restrições.";
"plusview_success_description" = "Agora pode continuar a receber ficheiros sem restrições. Todos os ficheiros que tentou enviar antes já foram guardados no seu dispositivo.";
"plusview_success_button" = "Entendido!";
"plusview_unlock_label" = "Apoiar QuickDrop";
+1 -1
View File
@@ -85,7 +85,7 @@ Dacă întâmpini în continuare probleme, acestea ar putea fi cauzate de router
"plusview_title" = "Mulțumim că folosești QuickDrop!";
"plusview_description" = "Deblochează QuickDrop+ pentru a continua schimbul de fișiere fără limite. QuickDrop+ este o achiziție unică – nu este necesar abonament. Toate funcțiile curente și viitoare sunt incluse.";
"plusview_success_title" = "Mulțumim pentru achiziție!";
"plusview_success_description" = "Acum poți continua să primești fișiere fără restricții.";
"plusview_success_description" = "Acum puteți continua să primiți fișiere fără restricții. Toate fișierele pe care ați încercat să le trimiteți anterior au fost deja salvate pe dispozitivul dvs.";
"plusview_success_button" = "Înțeles!";
"plusview_unlock_label" = "Susține QuickDrop";
+1 -1
View File
@@ -87,7 +87,7 @@
"plusview_title" = "Спасибо за использование QuickDrop!";
"plusview_description" = "Разблокируйте QuickDrop+, чтобы продолжить обмен файлами без ограничений. QuickDrop+ – это разовая покупка, подписка не требуется. Все текущие и будущие функции включены.";
"plusview_success_title" = "Спасибо за покупку!";
"plusview_success_description" = "Теперь вы можете продолжать получать файлы без ограничений.";
"plusview_success_description" = "Теперь вы можете продолжать получать файлы без ограничений. Все файлы, которые вы пытались отправить ранее, уже сохранены на вашем устройстве.";
"plusview_success_button" = "Понял!";
"plusview_unlock_label" = "Поддержать QuickDrop";
+1 -1
View File
@@ -82,7 +82,7 @@
"plusview_title" = "ขอบคุณที่ใช้ QuickDrop!";
"plusview_description" = "ปลดล็อก QuickDrop+ เพื่อแลกเปลี่ยนไฟล์ได้อย่างไม่จำกัด QuickDrop+ เป็นการซื้อครั้งเดียว – ไม่ต้องสมัครสมาชิก ฟีเจอร์ทั้งหมดทั้งปัจจุบันและอนาคตรวมอยู่ในนี้แล้ว";
"plusview_success_title" = "ขอบคุณสำหรับการซื้อ!";
"plusview_success_description" = "คุณสามารถรับไฟล์ได้ต่อไปโดยไม่มีข้อจำกัดแล้ว";
"plusview_success_description" = "คุณสามารถรับไฟล์ต่อไปได้โดยไม่มีข้อจำกัด ไฟล์ทั้งหมดที่คุณพยายามส่งก่อนหน้านี้ถูกบันทึกไว้ในอุปกรณ์ของคุณแล้ว";
"plusview_success_button" = "รับทราบ!";
"plusview_unlock_label" = "สนับสนุน QuickDrop";
+1 -1
View File
@@ -85,7 +85,7 @@
"plusview_title" = "Дякуємо за використання QuickDrop!";
"plusview_description" = "Розблокуйте QuickDrop+, щоб продовжити обмін файлами без обмежень. QuickDrop+ – це одноразова покупка, підписка не потрібна. Всі поточні та майбутні функції включені.";
"plusview_success_title" = "Дякуємо за покупку!";
"plusview_success_description" = "Тепер ви можете продовжувати отримувати файли без обмежень.";
"plusview_success_description" = "Тепер ви можете продовжувати отримувати файли без обмежень. Усі файли, які ви намагалися надіслати раніше, вже збережено на вашому пристрої.";
"plusview_success_button" = "Зрозуміло!";
"plusview_unlock_label" = "Підтримати QuickDrop";
+1 -1
View File
@@ -85,7 +85,7 @@
"plusview_title" = "感谢使用 QuickDrop!";
"plusview_description" = "解锁 QuickDrop+ 以无限制地交换文件。QuickDrop+ 是一次性购买 – 无需订阅。所有当前和未来的功能均包含在内。";
"plusview_success_title" = "感谢您的购买!";
"plusview_success_description" = "您现在可以无限制地接收文件了。";
"plusview_success_description" = "您现在可以继续无限制地接收文件。您之前尝试发送的所有文件已保存到您的设备上。";
"plusview_success_button" = "知道了!";
"plusview_unlock_label" = "支持 QuickDrop";
+497 -418
View File
@@ -16,360 +16,509 @@ import SwiftECC
import BigInt
class InboundNearbyConnection: NearbyConnection{
private var currentState:State = .initial
public var delegate:InboundNearbyConnectionDelegate?
private var cipherCommitment:Data?
private var textPayloadID:Int64=0
private var securityScopeUrl: URL?
enum State{
case initial, receivedConnectionRequest, sentUkeyServerInit, receivedUkeyClientFinish, sentConnectionResponse, sentPairedKeyResult, receivedPairedKeyResult, waitingForUserConsent, receivingFiles, disconnected
}
override init(connection: NWConnection, id:String) {
super.init(connection: connection, id: id)
}
override func handleConnectionClosure() {
super.handleConnectionClosure()
currentState = .disconnected
do{
try deletePartiallyReceivedFiles()
}catch{
log("Error deleting partially received files: \(error)")
}
DispatchQueue.main.async {
self.delegate?.connectionWasTerminated(connection: self, error: self.lastError)
if let url = self.securityScopeUrl {
log("Stopped accessing security scoped resource.")
url.stopAccessingSecurityScopedResource()
self.securityScopeUrl = nil
}
}
}
override internal func processReceivedFrame(frameData:Data){
do{
switch currentState {
case .initial:
let frame=try Location_Nearby_Connections_OfflineFrame(serializedData: frameData)
try processConnectionRequestFrame(frame)
case .receivedConnectionRequest:
let msg=try Securegcm_Ukey2Message(serializedData: frameData)
ukeyClientInitMsgData=frameData
try processUkey2ClientInit(msg)
case .sentUkeyServerInit:
let msg=try Securegcm_Ukey2Message(serializedData: frameData)
try processUkey2ClientFinish(msg, raw: frameData)
case .receivedUkeyClientFinish:
let frame=try Location_Nearby_Connections_OfflineFrame(serializedData: frameData)
try processConnectionResponseFrame(frame)
default:
let smsg=try Securemessage_SecureMessage(serializedData: frameData)
try decryptAndProcessReceivedSecureMessage(smsg)
}
}catch{
lastError=error
log("Deserialization error: \(error) in state \(currentState)")
#if !DEBUG
protocolError()
#endif
}
}
override internal func processTransferSetupFrame(_ frame:Sharing_Nearby_Frame) throws{
if frame.hasV1 && frame.v1.hasType, case .cancel = frame.v1.type {
log("Transfer canceled")
try sendDisconnectionAndDisconnect()
return
}
switch currentState{
case .sentConnectionResponse:
try processPairedKeyEncryptionFrame(frame)
case .sentPairedKeyResult:
try processPairedKeyResultFrame(frame)
case .receivedPairedKeyResult:
try processIntroductionFrame(frame)
default:
log("Unexpected connection state in processTransferSetupFrame: \(currentState)")
log(frame.debugDescription)
}
}
override func isServer() -> Bool {
return true
}
override func processFileChunk(frame: Location_Nearby_Connections_PayloadTransferFrame) throws{
let id=frame.payloadHeader.id
guard let fileInfo=transferredFiles[id] else { throw NearbyError.protocolError("File payload ID \(id) is not known") }
let currentOffset=fileInfo.bytesTransferred
guard frame.payloadChunk.offset==currentOffset else { throw NearbyError.protocolError("Invalid offset into file \(frame.payloadChunk.offset), expected \(currentOffset)") }
guard currentOffset+Int64(frame.payloadChunk.body.count)<=fileInfo.meta.size else { throw NearbyError.protocolError("Transferred file size exceeds previously specified value") }
if frame.payloadChunk.body.count>0{
fileInfo.fileHandle?.write(frame.payloadChunk.body)
transferredFiles[id]!.bytesTransferred+=Int64(frame.payloadChunk.body.count)
fileInfo.progress?.completedUnitCount=transferredFiles[id]!.bytesTransferred
}else if (frame.payloadChunk.flags & 1)==1{
try fileInfo.fileHandle?.close()
transferredFiles[id]!.fileHandle=nil
fileInfo.progress?.unpublish()
transferredFiles.removeValue(forKey: id)
if transferredFiles.isEmpty{
try sendDisconnectionAndDisconnect()
}
}
}
override func processBytesPayload(payload: Data, id: Int64) throws -> Bool {
if id==textPayloadID{
if let urlStr=String(data: payload, encoding: .utf8), let url=URL(string: urlStr){
NSWorkspace.shared.open(url)
}
try sendDisconnectionAndDisconnect()
return true
}else if let fileInfo=transferredFiles[id]{
fileInfo.fileHandle?.write(payload)
transferredFiles[id]!.bytesTransferred+=Int64(payload.count)
fileInfo.progress?.completedUnitCount=transferredFiles[id]!.bytesTransferred
try fileInfo.fileHandle?.close()
transferredFiles[id]!.fileHandle=nil
fileInfo.progress?.unpublish()
transferredFiles.removeValue(forKey: id)
try sendDisconnectionAndDisconnect()
return true
}
return false
}
private func processConnectionRequestFrame(_ frame:Location_Nearby_Connections_OfflineFrame) throws{
guard frame.hasV1 && frame.v1.hasConnectionRequest && frame.v1.connectionRequest.hasEndpointInfo else { throw NearbyError.requiredFieldMissing("connectionRequest.endpointInfo") }
guard case .connectionRequest = frame.v1.type else { throw NearbyError.protocolError("Unexpected frame type \(frame.v1.type)") }
let endpointInfo=frame.v1.connectionRequest.endpointInfo
guard endpointInfo.count>17 else { throw NearbyError.protocolError("Endpoint info too short") }
let deviceNameLength=Int(endpointInfo[17])
guard endpointInfo.count>=deviceNameLength+18 else { throw NearbyError.protocolError("Endpoint info too short to contain the device name") }
guard let deviceName=String(data: endpointInfo[18..<(18+deviceNameLength)], encoding: .utf8) else { throw NearbyError.protocolError("Device name is not valid UTF-8") }
let rawDeviceType:Int=Int(endpointInfo[0] & 7) >> 1
remoteDeviceInfo=RemoteDeviceInfo(name: deviceName, type: RemoteDeviceInfo.DeviceType.fromRawValue(value: rawDeviceType))
currentState = .receivedConnectionRequest
}
private func processUkey2ClientInit(_ msg:Securegcm_Ukey2Message) throws{
guard msg.hasMessageType, msg.hasMessageData else { throw NearbyError.requiredFieldMissing("clientInit ukey2message.type|data") }
guard case .clientInit = msg.messageType else{
sendUkey2Alert(type: .badMessageType)
throw NearbyError.ukey2
}
let clientInit:Securegcm_Ukey2ClientInit
do{
clientInit=try Securegcm_Ukey2ClientInit(serializedData: msg.messageData)
}catch{
sendUkey2Alert(type: .badMessageData)
throw NearbyError.ukey2
}
guard clientInit.version==1 else{
sendUkey2Alert(type: .badVersion)
throw NearbyError.ukey2
}
guard clientInit.random.count==32 else{
sendUkey2Alert(type: .badRandom)
throw NearbyError.ukey2
}
var found=false
for commitment in clientInit.cipherCommitments{
if case .p256Sha512 = commitment.handshakeCipher{
found=true
cipherCommitment=commitment.commitment
break
}
}
guard found else{
sendUkey2Alert(type: .badHandshakeCipher)
throw NearbyError.ukey2
}
guard clientInit.nextProtocol=="AES_256_CBC-HMAC_SHA256" else{
sendUkey2Alert(type: .badNextProtocol)
throw NearbyError.ukey2
}
let domain=Domain.instance(curve: .EC256r1)
let (pubKey, privKey)=domain.makeKeyPair()
publicKey=pubKey
privateKey=privKey
var serverInit=Securegcm_Ukey2ServerInit()
serverInit.version=1
serverInit.random=Data.randomData(length: 32)
serverInit.handshakeCipher = .p256Sha512
var pkey=Securemessage_GenericPublicKey()
pkey.type = .ecP256
pkey.ecP256PublicKey=Securemessage_EcP256PublicKey()
pkey.ecP256PublicKey.x=Data(pubKey.w.x.asSignedBytes())
pkey.ecP256PublicKey.y=Data(pubKey.w.y.asSignedBytes())
serverInit.publicKey=try pkey.serializedData()
var serverInitMsg=Securegcm_Ukey2Message()
serverInitMsg.messageType = .serverInit
serverInitMsg.messageData=try serverInit.serializedData()
let serverInitData=try serverInitMsg.serializedData()
ukeyServerInitMsgData=serverInitData
sendFrameAsync(serverInitData)
currentState = .sentUkeyServerInit
}
private func processUkey2ClientFinish(_ msg:Securegcm_Ukey2Message, raw:Data) throws{
guard msg.hasMessageType, msg.hasMessageData else { throw NearbyError.requiredFieldMissing("clientFinish ukey2message.type|data") }
guard case .clientFinish = msg.messageType else { throw NearbyError.ukey2 }
var sha=SHA512()
sha.update(data: raw)
guard cipherCommitment==Data(sha.finalize()) else { throw NearbyError.ukey2 }
let clientFinish=try Securegcm_Ukey2ClientFinished(serializedData: msg.messageData)
guard clientFinish.hasPublicKey else {throw NearbyError.requiredFieldMissing("ukey2clientFinish.publicKey") }
let clientKey=try Securemessage_GenericPublicKey(serializedData: clientFinish.publicKey)
try finalizeKeyExchange(peerKey: clientKey)
currentState = .receivedUkeyClientFinish
}
private func processConnectionResponseFrame(_ frame:Location_Nearby_Connections_OfflineFrame) throws{
guard frame.hasV1, frame.v1.hasType else { throw NearbyError.requiredFieldMissing("offlineFrame.v1.type") }
if case .connectionResponse = frame.v1.type {
var resp=Location_Nearby_Connections_OfflineFrame()
resp.version = .v1
resp.v1=Location_Nearby_Connections_V1Frame()
resp.v1.type = .connectionResponse
resp.v1.connectionResponse=Location_Nearby_Connections_ConnectionResponseFrame()
resp.v1.connectionResponse.response = .accept
resp.v1.connectionResponse.status=0
resp.v1.connectionResponse.osInfo=Location_Nearby_Connections_OsInfo()
resp.v1.connectionResponse.osInfo.type = .apple
sendFrameAsync(try resp.serializedData())
encryptionDone=true
var pairedEncryption=Sharing_Nearby_Frame()
pairedEncryption.version = .v1
pairedEncryption.v1=Sharing_Nearby_V1Frame()
pairedEncryption.v1.type = .pairedKeyEncryption
pairedEncryption.v1.pairedKeyEncryption=Sharing_Nearby_PairedKeyEncryptionFrame()
// Presumably used for all the phone number stuff that no one needs anyway
pairedEncryption.v1.pairedKeyEncryption.secretIDHash=Data.randomData(length: 6)
pairedEncryption.v1.pairedKeyEncryption.signedData=Data.randomData(length: 72)
try sendTransferSetupFrame(pairedEncryption)
currentState = .sentConnectionResponse
} else {
log("Unhandled offline frame plaintext: \(frame)")
}
}
private func processPairedKeyEncryptionFrame(_ frame:Sharing_Nearby_Frame) throws{
guard frame.hasV1, frame.v1.hasPairedKeyEncryption else { throw NearbyError.requiredFieldMissing("shareNearbyFrame.v1.pairedKeyEncryption") }
var pairedResult=Sharing_Nearby_Frame()
pairedResult.version = .v1
pairedResult.v1=Sharing_Nearby_V1Frame()
pairedResult.v1.type = .pairedKeyResult
pairedResult.v1.pairedKeyResult=Sharing_Nearby_PairedKeyResultFrame()
pairedResult.v1.pairedKeyResult.status = .unable
try sendTransferSetupFrame(pairedResult)
currentState = .sentPairedKeyResult
}
private func processPairedKeyResultFrame(_ frame:Sharing_Nearby_Frame) throws{
guard frame.hasV1, frame.v1.hasPairedKeyResult else { throw NearbyError.requiredFieldMissing("shareNearbyFrame.v1.pairedKeyResult") }
currentState = .receivedPairedKeyResult
}
private func makeFileDestinationURL(_ initialDest:URL) -> URL {
var dest=initialDest
if FileManager.default.fileExists(atPath: dest.path){
var counter=1
var path:String
let ext=dest.pathExtension
let baseUrl=dest.deletingPathExtension()
repeat{
path="\(baseUrl.path) (\(counter))"
if !ext.isEmpty{
path+=".\(ext)"
}
counter+=1
}while FileManager.default.fileExists(atPath: path)
dest=URL(fileURLWithPath: path)
}
return dest
}
private func processIntroductionFrame(_ frame:Sharing_Nearby_Frame) throws{
guard frame.hasV1, frame.v1.hasIntroduction else { throw NearbyError.requiredFieldMissing("shareNearbyFrame.v1.introduction") }
currentState = .waitingForUserConsent
if frame.v1.introduction.fileMetadata.count>0 && frame.v1.introduction.textMetadata.isEmpty{
let saveDirectory = getSaveDirectory()
for file in frame.v1.introduction.fileMetadata {
let dest = makeFileDestinationURL(saveDirectory.appendingPathComponent(file.name))
let info = InternalFileInfo(meta: FileMetadata(name: file.name, size: file.size, mimeType: file.mimeType),
payloadID: file.payloadID,
destinationURL: dest)
transferredFiles[file.payloadID] = info
}
let metadata=TransferMetadata(files: transferredFiles.map({$0.value.meta}), id: id, pinCode: pinCode)
DispatchQueue.main.async {
self.delegate?.obtainUserConsent(for: metadata, from: self.remoteDeviceInfo!, connection: self)
}
}else if frame.v1.introduction.textMetadata.count==1{
let meta=frame.v1.introduction.textMetadata[0]
if case .url=meta.type{
let metadata=TransferMetadata(files: [], id: id, pinCode: pinCode, textDescription: meta.textTitle)
textPayloadID=meta.payloadID
DispatchQueue.main.async {
self.delegate?.obtainUserConsent(for: metadata, from: self.remoteDeviceInfo!, connection: self)
}
}else if case .text=meta.type{
let saveDirectory = getSaveDirectory()
let dateFormatter=DateFormatter()
dateFormatter.dateFormat="yyyy-MM-dd HH.mm.ss"
let dest=makeFileDestinationURL(saveDirectory.appendingPathComponent("\(dateFormatter.string(from: Date())).txt"))
let info=InternalFileInfo(meta: FileMetadata(name: dest.lastPathComponent, size: meta.size, mimeType: "text/plain"),
payloadID: meta.payloadID,
destinationURL: dest)
transferredFiles[meta.payloadID]=info
DispatchQueue.main.async {
self.delegate?.obtainUserConsent(for: TransferMetadata(files: [info.meta], id: self.id, pinCode: self.pinCode), from: self.remoteDeviceInfo!, connection: self)
}
}else{
rejectTransfer(with: .unsupportedAttachmentType)
}
}else{
rejectTransfer(with: .unsupportedAttachmentType)
}
}
private func getSaveDirectory() -> URL {
private var currentState:State = .initial
public var delegate:InboundNearbyConnectionDelegate?
private var cipherCommitment:Data?
private var textPayloadID:Int64=0
enum State{
case initial, receivedConnectionRequest, sentUkeyServerInit, receivedUkeyClientFinish, sentConnectionResponse, sentPairedKeyResult, receivedPairedKeyResult, waitingForUserConsent, receivingFiles, disconnected
}
override init(connection: NWConnection, id:String) {
super.init(connection: connection, id: id)
}
override func handleConnectionClosure() {
super.handleConnectionClosure()
currentState = .disconnected
do {
try deletePartiallyReceivedFiles()
} catch{
log("Error deleting partially received files: \(error)")
}
DispatchQueue.main.async {
self.delegate?.connectionWasTerminated(connection: self, error: self.lastError)
SaveFilesManager.shared.stopAccessingSecurityScopedResource()
}
}
override internal func processReceivedFrame(frameData:Data){
do{
switch currentState {
case .initial:
let frame=try Location_Nearby_Connections_OfflineFrame(serializedData: frameData)
try processConnectionRequestFrame(frame)
case .receivedConnectionRequest:
let msg=try Securegcm_Ukey2Message(serializedData: frameData)
ukeyClientInitMsgData=frameData
try processUkey2ClientInit(msg)
case .sentUkeyServerInit:
let msg=try Securegcm_Ukey2Message(serializedData: frameData)
try processUkey2ClientFinish(msg, raw: frameData)
case .receivedUkeyClientFinish:
let frame=try Location_Nearby_Connections_OfflineFrame(serializedData: frameData)
try processConnectionResponseFrame(frame)
default:
let smsg=try Securemessage_SecureMessage(serializedData: frameData)
try decryptAndProcessReceivedSecureMessage(smsg)
}
}catch{
lastError=error
log("Deserialization error: \(error) in state \(currentState)")
#if !DEBUG
protocolError()
#endif
}
}
override internal func processTransferSetupFrame(_ frame:Sharing_Nearby_Frame) throws{
if frame.hasV1 && frame.v1.hasType, case .cancel = frame.v1.type {
log("Transfer canceled")
try sendDisconnectionAndDisconnect()
return
}
switch currentState{
case .sentConnectionResponse:
try processPairedKeyEncryptionFrame(frame)
case .sentPairedKeyResult:
try processPairedKeyResultFrame(frame)
case .receivedPairedKeyResult:
try processIntroductionFrame(frame)
default:
log("Unexpected connection state in processTransferSetupFrame: \(currentState)")
log(frame.debugDescription)
}
}
override func isServer() -> Bool {
return true
}
override func processFileChunk(frame: Location_Nearby_Connections_PayloadTransferFrame) throws{
let id=frame.payloadHeader.id
guard let fileInfo=transferredFiles[id] else { throw NearbyError.protocolError("File payload ID \(id) is not known") }
let currentOffset=fileInfo.bytesTransferred
guard frame.payloadChunk.offset==currentOffset else { throw NearbyError.protocolError("Invalid offset into file \(frame.payloadChunk.offset), expected \(currentOffset)") }
guard currentOffset+Int64(frame.payloadChunk.body.count)<=fileInfo.meta.size else { throw NearbyError.protocolError("Transferred file size exceeds previously specified value") }
if frame.payloadChunk.body.count>0{
fileInfo.fileHandle?.write(frame.payloadChunk.body)
transferredFiles[id]!.bytesTransferred+=Int64(frame.payloadChunk.body.count)
fileInfo.progress?.completedUnitCount=transferredFiles[id]!.bytesTransferred
}else if (frame.payloadChunk.flags & 1)==1{
try fileInfo.fileHandle?.close()
transferredFiles[id]!.fileHandle=nil
fileInfo.progress?.unpublish()
transferredFiles.removeValue(forKey: id)
if transferredFiles.isEmpty{
try sendDisconnectionAndDisconnect()
}
}
}
override func processBytesPayload(payload: Data, id: Int64) throws -> Bool {
if id==textPayloadID{
if let urlStr=String(data: payload, encoding: .utf8), let url=URL(string: urlStr){
NSWorkspace.shared.open(url)
}
try sendDisconnectionAndDisconnect()
return true
}else if let fileInfo=transferredFiles[id]{
fileInfo.fileHandle?.write(payload)
transferredFiles[id]!.bytesTransferred+=Int64(payload.count)
fileInfo.progress?.completedUnitCount=transferredFiles[id]!.bytesTransferred
try fileInfo.fileHandle?.close()
transferredFiles[id]!.fileHandle=nil
fileInfo.progress?.unpublish()
transferredFiles.removeValue(forKey: id)
try sendDisconnectionAndDisconnect()
return true
}
return false
}
private func processConnectionRequestFrame(_ frame:Location_Nearby_Connections_OfflineFrame) throws{
guard frame.hasV1 && frame.v1.hasConnectionRequest && frame.v1.connectionRequest.hasEndpointInfo else { throw NearbyError.requiredFieldMissing("connectionRequest.endpointInfo") }
guard case .connectionRequest = frame.v1.type else { throw NearbyError.protocolError("Unexpected frame type \(frame.v1.type)") }
let endpointInfo=frame.v1.connectionRequest.endpointInfo
guard endpointInfo.count>17 else { throw NearbyError.protocolError("Endpoint info too short") }
let deviceNameLength=Int(endpointInfo[17])
guard endpointInfo.count>=deviceNameLength+18 else { throw NearbyError.protocolError("Endpoint info too short to contain the device name") }
guard let deviceName=String(data: endpointInfo[18..<(18+deviceNameLength)], encoding: .utf8) else { throw NearbyError.protocolError("Device name is not valid UTF-8") }
let rawDeviceType:Int=Int(endpointInfo[0] & 7) >> 1
remoteDeviceInfo=RemoteDeviceInfo(name: deviceName, type: RemoteDeviceInfo.DeviceType.fromRawValue(value: rawDeviceType))
currentState = .receivedConnectionRequest
}
private func processUkey2ClientInit(_ msg:Securegcm_Ukey2Message) throws{
guard msg.hasMessageType, msg.hasMessageData else { throw NearbyError.requiredFieldMissing("clientInit ukey2message.type|data") }
guard case .clientInit = msg.messageType else{
sendUkey2Alert(type: .badMessageType)
throw NearbyError.ukey2
}
let clientInit:Securegcm_Ukey2ClientInit
do{
clientInit=try Securegcm_Ukey2ClientInit(serializedData: msg.messageData)
}catch{
sendUkey2Alert(type: .badMessageData)
throw NearbyError.ukey2
}
guard clientInit.version==1 else{
sendUkey2Alert(type: .badVersion)
throw NearbyError.ukey2
}
guard clientInit.random.count==32 else{
sendUkey2Alert(type: .badRandom)
throw NearbyError.ukey2
}
var found=false
for commitment in clientInit.cipherCommitments{
if case .p256Sha512 = commitment.handshakeCipher{
found=true
cipherCommitment=commitment.commitment
break
}
}
guard found else{
sendUkey2Alert(type: .badHandshakeCipher)
throw NearbyError.ukey2
}
guard clientInit.nextProtocol=="AES_256_CBC-HMAC_SHA256" else{
sendUkey2Alert(type: .badNextProtocol)
throw NearbyError.ukey2
}
let domain=Domain.instance(curve: .EC256r1)
let (pubKey, privKey)=domain.makeKeyPair()
publicKey=pubKey
privateKey=privKey
var serverInit=Securegcm_Ukey2ServerInit()
serverInit.version=1
serverInit.random=Data.randomData(length: 32)
serverInit.handshakeCipher = .p256Sha512
var pkey=Securemessage_GenericPublicKey()
pkey.type = .ecP256
pkey.ecP256PublicKey=Securemessage_EcP256PublicKey()
pkey.ecP256PublicKey.x=Data(pubKey.w.x.asSignedBytes())
pkey.ecP256PublicKey.y=Data(pubKey.w.y.asSignedBytes())
serverInit.publicKey=try pkey.serializedData()
var serverInitMsg=Securegcm_Ukey2Message()
serverInitMsg.messageType = .serverInit
serverInitMsg.messageData=try serverInit.serializedData()
let serverInitData=try serverInitMsg.serializedData()
ukeyServerInitMsgData=serverInitData
sendFrameAsync(serverInitData)
currentState = .sentUkeyServerInit
}
private func processUkey2ClientFinish(_ msg:Securegcm_Ukey2Message, raw:Data) throws{
guard msg.hasMessageType, msg.hasMessageData else { throw NearbyError.requiredFieldMissing("clientFinish ukey2message.type|data") }
guard case .clientFinish = msg.messageType else { throw NearbyError.ukey2 }
var sha=SHA512()
sha.update(data: raw)
guard cipherCommitment==Data(sha.finalize()) else { throw NearbyError.ukey2 }
let clientFinish=try Securegcm_Ukey2ClientFinished(serializedData: msg.messageData)
guard clientFinish.hasPublicKey else {throw NearbyError.requiredFieldMissing("ukey2clientFinish.publicKey") }
let clientKey=try Securemessage_GenericPublicKey(serializedData: clientFinish.publicKey)
try finalizeKeyExchange(peerKey: clientKey)
currentState = .receivedUkeyClientFinish
}
private func processConnectionResponseFrame(_ frame:Location_Nearby_Connections_OfflineFrame) throws{
guard frame.hasV1, frame.v1.hasType else { throw NearbyError.requiredFieldMissing("offlineFrame.v1.type") }
if case .connectionResponse = frame.v1.type {
var resp=Location_Nearby_Connections_OfflineFrame()
resp.version = .v1
resp.v1=Location_Nearby_Connections_V1Frame()
resp.v1.type = .connectionResponse
resp.v1.connectionResponse=Location_Nearby_Connections_ConnectionResponseFrame()
resp.v1.connectionResponse.response = .accept
resp.v1.connectionResponse.status=0
resp.v1.connectionResponse.osInfo=Location_Nearby_Connections_OsInfo()
resp.v1.connectionResponse.osInfo.type = .apple
sendFrameAsync(try resp.serializedData())
encryptionDone=true
var pairedEncryption=Sharing_Nearby_Frame()
pairedEncryption.version = .v1
pairedEncryption.v1=Sharing_Nearby_V1Frame()
pairedEncryption.v1.type = .pairedKeyEncryption
pairedEncryption.v1.pairedKeyEncryption=Sharing_Nearby_PairedKeyEncryptionFrame()
// Presumably used for all the phone number stuff that no one needs anyway
pairedEncryption.v1.pairedKeyEncryption.secretIDHash=Data.randomData(length: 6)
pairedEncryption.v1.pairedKeyEncryption.signedData=Data.randomData(length: 72)
try sendTransferSetupFrame(pairedEncryption)
currentState = .sentConnectionResponse
} else {
log("Unhandled offline frame plaintext: \(frame)")
}
}
private func processPairedKeyEncryptionFrame(_ frame:Sharing_Nearby_Frame) throws{
guard frame.hasV1, frame.v1.hasPairedKeyEncryption else { throw NearbyError.requiredFieldMissing("shareNearbyFrame.v1.pairedKeyEncryption") }
var pairedResult=Sharing_Nearby_Frame()
pairedResult.version = .v1
pairedResult.v1=Sharing_Nearby_V1Frame()
pairedResult.v1.type = .pairedKeyResult
pairedResult.v1.pairedKeyResult=Sharing_Nearby_PairedKeyResultFrame()
pairedResult.v1.pairedKeyResult.status = .unable
try sendTransferSetupFrame(pairedResult)
currentState = .sentPairedKeyResult
}
private func processPairedKeyResultFrame(_ frame:Sharing_Nearby_Frame) throws{
guard frame.hasV1, frame.v1.hasPairedKeyResult else { throw NearbyError.requiredFieldMissing("shareNearbyFrame.v1.pairedKeyResult") }
currentState = .receivedPairedKeyResult
}
private func makeFileDestinationURL(_ initialDest:URL) -> URL {
var dest=initialDest
if FileManager.default.fileExists(atPath: dest.path){
var counter=1
var path:String
let ext=dest.pathExtension
let baseUrl=dest.deletingPathExtension()
repeat{
path="\(baseUrl.path) (\(counter))"
if !ext.isEmpty{
path+=".\(ext)"
}
counter+=1
}while FileManager.default.fileExists(atPath: path)
dest=URL(fileURLWithPath: path)
}
return dest
}
private func processIntroductionFrame(_ frame:Sharing_Nearby_Frame) throws{
guard frame.hasV1, frame.v1.hasIntroduction else { throw NearbyError.requiredFieldMissing("shareNearbyFrame.v1.introduction") }
currentState = .waitingForUserConsent
if frame.v1.introduction.fileMetadata.count>0 && frame.v1.introduction.textMetadata.isEmpty{
let saveDirectory = SaveFilesManager.shared.getSaveDirectory()
for file in frame.v1.introduction.fileMetadata {
let dest = makeFileDestinationURL(saveDirectory.appendingPathComponent(file.name))
let info = InternalFileInfo(meta: FileMetadata(name: file.name, size: file.size, mimeType: file.mimeType),
payloadID: file.payloadID,
destinationURL: dest)
transferredFiles[file.payloadID] = info
}
let metadata=TransferMetadata(files: transferredFiles.map({$0.value.meta}), id: id, pinCode: pinCode)
DispatchQueue.main.async {
self.delegate?.obtainUserConsent(for: metadata, from: self.remoteDeviceInfo!, connection: self)
}
}else if frame.v1.introduction.textMetadata.count==1{
let meta=frame.v1.introduction.textMetadata[0]
if case .url=meta.type{
let metadata=TransferMetadata(files: [], id: id, pinCode: pinCode, textDescription: meta.textTitle)
textPayloadID=meta.payloadID
DispatchQueue.main.async {
self.delegate?.obtainUserConsent(for: metadata, from: self.remoteDeviceInfo!, connection: self)
}
}else if case .text=meta.type{
let saveDirectory = SaveFilesManager.shared.getSaveDirectory()
let dateFormatter=DateFormatter()
dateFormatter.dateFormat="yyyy-MM-dd HH.mm.ss"
let dest=makeFileDestinationURL(saveDirectory.appendingPathComponent("\(dateFormatter.string(from: Date())).txt"))
let info=InternalFileInfo(meta: FileMetadata(name: dest.lastPathComponent, size: meta.size, mimeType: "text/plain"),
payloadID: meta.payloadID,
destinationURL: dest)
transferredFiles[meta.payloadID]=info
DispatchQueue.main.async {
self.delegate?.obtainUserConsent(for: TransferMetadata(files: [info.meta], id: self.id, pinCode: self.pinCode), from: self.remoteDeviceInfo!, connection: self)
}
}else{
rejectTransfer(with: .unsupportedAttachmentType)
}
}else{
rejectTransfer(with: .unsupportedAttachmentType)
}
}
func submitUserConsent(accepted:Bool, storeInTemp: Bool = false){
DispatchQueue.global(qos: .utility).async {
if accepted{
self.acceptTransfer(storeInTemp: storeInTemp)
}else{
self.rejectTransfer()
}
}
}
private func acceptTransfer(storeInTemp: Bool) {
if currentState == .disconnected {
log("Detected timeout, not accepting transfer")
return
}
do {
if storeInTemp {
try FileManager.default.createDirectory(at: SaveFilesManager.shared.tempDirectory, withIntermediateDirectories: true)
}
for (id, file) in transferredFiles {
let targetURL = storeInTemp ? SaveFilesManager.shared.tempDirectory.appendingPathComponent(file.destinationURL.lastPathComponent) : file.destinationURL
FileManager.default.createFile(atPath: targetURL.path, contents: nil)
let handle = try FileHandle(forWritingTo: targetURL)
transferredFiles[id]!.fileHandle = handle
let progress = Progress()
progress.fileURL = targetURL
progress.totalUnitCount = file.meta.size
progress.kind = .file
progress.isPausable = false
progress.publish()
transferredFiles[id]!.progress = progress
transferredFiles[id]!.created = true
}
var frame = Sharing_Nearby_Frame()
frame.version = .v1
frame.v1.type = .response
frame.v1.connectionResponse.status = .accept
currentState = .receivingFiles
try sendTransferSetupFrame(frame)
} catch {
lastError = error
protocolError()
}
}
private func rejectTransfer(with reason:Sharing_Nearby_ConnectionResponseFrame.Status = .reject){
var frame=Sharing_Nearby_Frame()
frame.version = .v1
frame.v1.type = .response
frame.v1.connectionResponse.status = reason
do{
try sendTransferSetupFrame(frame)
try sendDisconnectionAndDisconnect()
}catch{
log("Error \(error)")
protocolError()
}
}
private func deletePartiallyReceivedFiles() throws{
for (_, file) in transferredFiles {
guard file.created else { continue }
try FileManager.default.removeItem(at: file.destinationURL)
}
}
}
protocol InboundNearbyConnectionDelegate{
func obtainUserConsent(for transfer:TransferMetadata, from device:RemoteDeviceInfo, connection:InboundNearbyConnection)
func connectionWasTerminated(connection:InboundNearbyConnection, error:Error?)
}
public class SaveFilesManager {
private init() {}
public static let shared = SaveFilesManager()
internal let tempDirectory: URL = FileManager.default.temporaryDirectory.appendingPathComponent("Pending")
private var securityScopeUrl: URL?
public func movePendingFilesToTarget() {
log("Moving pending files to target directory")
do {
let fileManager = FileManager.default
let files = try fileManager.contentsOfDirectory(at: tempDirectory, includingPropertiesForKeys: nil)
let target = getSaveDirectory()
for file in files {
let fileName = file.lastPathComponent
var destinationURL = target.appendingPathComponent(fileName)
var counter = 1
let fileExtension = destinationURL.pathExtension
let baseName = destinationURL.deletingPathExtension().lastPathComponent
while fileManager.fileExists(atPath: destinationURL.path) {
let newFileName = "\(baseName)-\(counter)"
let newFileNameWithExtension = fileExtension.isEmpty ? newFileName : "\(newFileName).\(fileExtension)"
destinationURL = target.appendingPathComponent(newFileNameWithExtension)
counter += 1
}
log("Moving file: \(file.lastPathComponent) to \(destinationURL.lastPathComponent)")
try fileManager.copyItem(at: file, to: destinationURL)
}
try fileManager.removeItem(at: tempDirectory)
stopAccessingSecurityScopedResource()
log("Moved all pending files to target directory")
} catch {
log("Error moving pending files: \(error)")
}
}
public func stopAccessingSecurityScopedResource() {
guard let url = securityScopeUrl else {
return
}
log("Stopping access to security scoped resource: \(url)")
url.stopAccessingSecurityScopedResource()
self.securityScopeUrl = nil
}
public func getSaveDirectory() -> URL {
if let securityScopeUrl = securityScopeUrl {
log("Using existing security scope URL: \(securityScopeUrl)")
return securityScopeUrl
}
if let bookmarkData = UserDefaults.standard.data(forKey: UserDefaultsKeys.saveFolderBookmark.rawValue) {
var isStale = false
do {
let url = try URL(resolvingBookmarkData: bookmarkData, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale)
if !isStale {
if url.startAccessingSecurityScopedResource() {
self.securityScopeUrl = url
return url
}
} else {
print("Bookmark is stale, using default downloads folder.")
if !isStale {
if url.startAccessingSecurityScopedResource() {
log("Successfully accessed security scoped resource: \(url)")
self.securityScopeUrl = url
return url
}
} else {
print("Bookmark is stale, using default downloads folder.")
}
} catch {
print("Failed to resolve bookmark: \(error), using default downloads folder.")
@@ -382,74 +531,4 @@ class InboundNearbyConnection: NearbyConnection{
fatalError("Failed to get downloads directory: \(error)")
}
}
func submitUserConsent(accepted:Bool){
DispatchQueue.global(qos: .utility).async {
if accepted{
self.acceptTransfer()
}else{
self.rejectTransfer()
}
}
}
private func acceptTransfer(){
if currentState == .disconnected {
log("Detected timeout, not accepting transfer")
return
}
do{
for (id, file) in transferredFiles{
FileManager.default.createFile(atPath: file.destinationURL.path, contents: nil)
let handle=try FileHandle(forWritingTo: file.destinationURL)
transferredFiles[id]!.fileHandle=handle
let progress=Progress()
progress.fileURL=file.destinationURL
progress.totalUnitCount=file.meta.size
progress.kind = .file
progress.isPausable=false
progress.publish()
transferredFiles[id]!.progress=progress
transferredFiles[id]!.created=true
}
var frame=Sharing_Nearby_Frame()
frame.version = .v1
frame.v1.type = .response
frame.v1.connectionResponse.status = .accept
currentState = .receivingFiles
try sendTransferSetupFrame(frame)
}catch{
lastError=error
protocolError()
}
}
private func rejectTransfer(with reason:Sharing_Nearby_ConnectionResponseFrame.Status = .reject){
var frame=Sharing_Nearby_Frame()
frame.version = .v1
frame.v1.type = .response
frame.v1.connectionResponse.status = reason
do{
try sendTransferSetupFrame(frame)
try sendDisconnectionAndDisconnect()
}catch{
log("Error \(error)")
protocolError()
}
}
private func deletePartiallyReceivedFiles() throws{
for (_, file) in transferredFiles{
guard file.created else { continue }
try FileManager.default.removeItem(at: file.destinationURL)
}
}
}
protocol InboundNearbyConnectionDelegate{
func obtainUserConsent(for transfer:TransferMetadata, from device:RemoteDeviceInfo, connection:InboundNearbyConnection)
func connectionWasTerminated(connection:InboundNearbyConnection, error:Error?)
}
+2 -2
View File
@@ -228,9 +228,9 @@ public class NearbyConnectionManager : NSObject, NetServiceDelegate, InboundNear
activeConnections.removeValue(forKey: connection.id)
}
public func submitUserConsent(transferID:String, accept:Bool){
public func submitUserConsent(transferID:String, accept:Bool, storeInTemp: Bool = false){
guard let conn=activeConnections[transferID] else {return}
conn.submitUserConsent(accepted: accept)
conn.submitUserConsent(accepted: accept, storeInTemp: storeInTemp)
}
public func startDeviceDiscovery(){