mirror of
https://github.com/leonboe1/QuickDrop.git
synced 2026-11-12 19:41:11 +00:00
Refactoring
This commit is contained in:
1 parent
98d1596672
commit
13716f41e2
7 files changed
+341
-293
No files matched your search
@@ -1,47 +0,0 @@
|
||||
//
|
||||
// Data+Extensions.swift
|
||||
// QuickDrop
|
||||
//
|
||||
// Created by Grishka on 08.04.2023.
|
||||
//
|
||||
|
||||
import CoreFoundation
|
||||
import Foundation
|
||||
|
||||
extension Data {
|
||||
func urlSafeBase64EncodedString() -> String {
|
||||
return String(base64EncodedString().replacingOccurrences(of: "=", with: "").map {
|
||||
if $0 == "/" {
|
||||
return "_"
|
||||
} else if $0 == "+" {
|
||||
return "-"
|
||||
} else {
|
||||
return $0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static func randomData(length: Int) -> Data {
|
||||
var data = Data(count: length)
|
||||
data.withUnsafeMutableBytes {
|
||||
guard SecRandomCopyBytes(kSecRandomDefault, length, $0.baseAddress!) == 0 else { fatalError() }
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
static func dataFromUrlSafeBase64(_ str: String) -> Data? {
|
||||
var regularB64 = String(str.map {
|
||||
if $0 == "_" {
|
||||
return "/"
|
||||
} else if $0 == "-" {
|
||||
return "+"
|
||||
} else {
|
||||
return $0
|
||||
}
|
||||
})
|
||||
while (regularB64.count % 4) != 0 {
|
||||
regularB64 = regularB64 + "="
|
||||
}
|
||||
return Data(base64Encoded: regularB64, options: .ignoreUnknownCharacters)
|
||||
}
|
||||
}
|
||||
@@ -7,18 +7,87 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Data {
|
||||
func urlSafeBase64EncodedString() -> String {
|
||||
return String(base64EncodedString().replacingOccurrences(of: "=", with: "").map {
|
||||
if $0 == "/" {
|
||||
return "_"
|
||||
} else if $0 == "+" {
|
||||
return "-"
|
||||
} else {
|
||||
return $0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
static func randomData(length: Int) -> Data {
|
||||
var data = Data(count: length)
|
||||
data.withUnsafeMutableBytes {
|
||||
guard SecRandomCopyBytes(kSecRandomDefault, length, $0.baseAddress!) == 0 else { fatalError() }
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
|
||||
static func dataFromUrlSafeBase64(_ str: String) -> Data? {
|
||||
var regularB64 = String(str.map {
|
||||
if $0 == "_" {
|
||||
return "/"
|
||||
} else if $0 == "-" {
|
||||
return "+"
|
||||
} else {
|
||||
return $0
|
||||
}
|
||||
})
|
||||
while (regularB64.count % 4) != 0 {
|
||||
regularB64 = regularB64 + "="
|
||||
}
|
||||
return Data(base64Encoded: regularB64, options: .ignoreUnknownCharacters)
|
||||
}
|
||||
|
||||
|
||||
var hex: String {
|
||||
return map { String(format: "%02hhx", $0) }.joined()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension String {
|
||||
var dataFromHex: Data {
|
||||
var data = Data(capacity: count / 2)
|
||||
var index = startIndex
|
||||
while index < endIndex {
|
||||
let nextIndex = self.index(index, offsetBy: 2)
|
||||
if nextIndex <= endIndex,
|
||||
let byte = UInt8(self[index ..< nextIndex], radix: 16)
|
||||
{
|
||||
data.append(byte)
|
||||
} else {
|
||||
break // or handle invalid hex gracefully
|
||||
}
|
||||
index = nextIndex
|
||||
}
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func isPlusVersion() -> Bool {
|
||||
return UserDefaults.standard.bool(forKey: UserDefaultsKeys.plusVersion.rawValue)
|
||||
}
|
||||
|
||||
|
||||
public func isFileTransferRestricted() -> Bool {
|
||||
(!isPlusVersion() && transmissionCount() > 1)
|
||||
}
|
||||
|
||||
|
||||
public func transmissionCount() -> Int {
|
||||
return UserDefaults.standard.integer(forKey: UserDefaultsKeys.transmissionCount.rawValue)
|
||||
}
|
||||
|
||||
|
||||
public enum UserDefaultsKeys: String, CaseIterable {
|
||||
case isEligibleForIap = "isEligibleForIap"
|
||||
case appLaunchedBefore = "ShowedWelcomeScreen"
|
||||
|
||||
@@ -11,11 +11,11 @@ import CryptoKit
|
||||
import Foundation
|
||||
import Network
|
||||
import System
|
||||
import ImageIO
|
||||
import BigInt
|
||||
import SwiftECC
|
||||
|
||||
class InboundNearbyConnection: NearbyConnection {
|
||||
|
||||
private var currentState: State = .initial
|
||||
private var wasRejected = false
|
||||
public var delegate: InboundNearbyConnectionDelegate?
|
||||
@@ -27,13 +27,15 @@ class InboundNearbyConnection: NearbyConnection {
|
||||
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()
|
||||
override func disconnect() {
|
||||
super.disconnect()
|
||||
currentState = .disconnected
|
||||
|
||||
if !self.wasRejected {
|
||||
@@ -45,6 +47,7 @@ class InboundNearbyConnection: NearbyConnection {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override func processReceivedFrame(frameData: Data) {
|
||||
|
||||
@@ -89,6 +92,7 @@ class InboundNearbyConnection: NearbyConnection {
|
||||
protocolError()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override func processTransferSetupFrame(_ frame: Sharing_Nearby_Frame) throws {
|
||||
if frame.hasV1 && frame.v1.hasType, case .cancel = frame.v1.type {
|
||||
@@ -109,9 +113,11 @@ class InboundNearbyConnection: NearbyConnection {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override func isServer() -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
override func processFileChunk(frame: Location_Nearby_Connections_PayloadTransferFrame) throws {
|
||||
|
||||
@@ -151,6 +157,7 @@ class InboundNearbyConnection: NearbyConnection {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override func processBytesPayload(payload: Data, id: Int64) throws -> Bool {
|
||||
if id == textPayloadID {
|
||||
if let urlStr = String(data: payload, encoding: .utf8) {
|
||||
@@ -188,6 +195,7 @@ class InboundNearbyConnection: NearbyConnection {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
private func processConnectionRequestFrame(_ frame: Location_Nearby_Connections_OfflineFrame) throws {
|
||||
|
||||
@@ -210,6 +218,7 @@ class InboundNearbyConnection: NearbyConnection {
|
||||
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") }
|
||||
@@ -275,6 +284,7 @@ class InboundNearbyConnection: NearbyConnection {
|
||||
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 }
|
||||
@@ -291,6 +301,7 @@ class InboundNearbyConnection: NearbyConnection {
|
||||
|
||||
currentState = .receivedUkeyClientFinish
|
||||
}
|
||||
|
||||
|
||||
private func processConnectionResponseFrame(_ frame: Location_Nearby_Connections_OfflineFrame) throws {
|
||||
guard frame.hasV1, frame.v1.hasType else { throw NearbyError.requiredFieldMissing("offlineFrame.v1.type") }
|
||||
@@ -323,6 +334,7 @@ class InboundNearbyConnection: NearbyConnection {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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()
|
||||
@@ -335,10 +347,12 @@ class InboundNearbyConnection: NearbyConnection {
|
||||
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
|
||||
@@ -358,6 +372,7 @@ class InboundNearbyConnection: NearbyConnection {
|
||||
}
|
||||
return dest
|
||||
}
|
||||
|
||||
|
||||
private func processIntroductionFrame(_ frame: Sharing_Nearby_Frame) throws {
|
||||
guard frame.hasV1, frame.v1.hasIntroduction else { throw NearbyError.requiredFieldMissing("shareNearbyFrame.v1.introduction") }
|
||||
@@ -403,6 +418,7 @@ class InboundNearbyConnection: NearbyConnection {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func rejectDueToUnsupportedFileType(_ frame: Sharing_Nearby_Frame) {
|
||||
|
||||
log("Rejecting transfer due to unsupported file type. Frame is \(frame.debugDescription)")
|
||||
@@ -410,6 +426,7 @@ class InboundNearbyConnection: NearbyConnection {
|
||||
NearbyConnectionManager.shared.mainAppDelegate?.showUnsupportedFileAlert(for: remoteDeviceInfo)
|
||||
rejectTransfer(with: .unsupportedAttachmentType)
|
||||
}
|
||||
|
||||
|
||||
func submitUserConsent(accepted: Bool, storeInTemp: Bool = false) {
|
||||
DispatchQueue.global(qos: .utility).async {
|
||||
@@ -421,6 +438,7 @@ class InboundNearbyConnection: NearbyConnection {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func acceptTransfer(storeInTemp: Bool) {
|
||||
if currentState == .disconnected {
|
||||
log("Detected timeout, not accepting transfer")
|
||||
@@ -461,6 +479,7 @@ class InboundNearbyConnection: NearbyConnection {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func rejectTransfer(with reason: Sharing_Nearby_ConnectionResponseFrame.Status = .reject) {
|
||||
|
||||
self.wasRejected = true
|
||||
@@ -481,236 +500,8 @@ class InboundNearbyConnection: NearbyConnection {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protocol InboundNearbyConnectionDelegate {
|
||||
func obtainUserConsent(for transfer: TransferMetadata, from device: RemoteDeviceInfo, connection: InboundNearbyConnection)
|
||||
func connectionWasTerminated(connection: InboundNearbyConnection, error: Error?)
|
||||
}
|
||||
|
||||
public class SaveFilesManager {
|
||||
|
||||
private init() {
|
||||
// remove old temp directory
|
||||
|
||||
let tempPath = FileManager.default.temporaryDirectory
|
||||
let fileManager = FileManager.default
|
||||
|
||||
do {
|
||||
let contents = try fileManager.contentsOfDirectory(at: tempPath, includingPropertiesForKeys: nil)
|
||||
|
||||
var didSomething = false
|
||||
|
||||
for item in contents {
|
||||
didSomething = true
|
||||
try fileManager.removeItem(at: item)
|
||||
}
|
||||
|
||||
if didSomething {
|
||||
log("Temporary directory cleared.")
|
||||
}
|
||||
} catch {
|
||||
log("Failed to list contents of temp directory: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
public static let shared = SaveFilesManager()
|
||||
|
||||
let tempDirectory: URL = FileManager.default.temporaryDirectory.appendingPathComponent("Pending")
|
||||
private var securityScopeUrl: URL?
|
||||
|
||||
private var filesFinishedDownloading = [URL]()
|
||||
private var filesFinishedDownloadingSinceLastRun = [URL]()
|
||||
|
||||
public func registerFileFinishedDownloading(_ fileURL: URL) {
|
||||
filesFinishedDownloading.append(fileURL)
|
||||
filesFinishedDownloadingSinceLastRun.append(fileURL)
|
||||
|
||||
// read out EXIF timestamp and manually set file creation date to it
|
||||
applyEXIFTimestamps(at: fileURL)
|
||||
}
|
||||
|
||||
public func movePendingFilesToTarget() {
|
||||
|
||||
if isPlusVersion() {
|
||||
do {
|
||||
let fileManager = FileManager.default
|
||||
let files = try fileManager.contentsOfDirectory(at: tempDirectory, includingPropertiesForKeys: nil)
|
||||
|
||||
let target = getSaveDirectory()
|
||||
var loggedExecution = false
|
||||
|
||||
for file in files {
|
||||
|
||||
if !loggedExecution {
|
||||
log("Moving pending files to target directory")
|
||||
loggedExecution = true
|
||||
}
|
||||
|
||||
let fileName = file.lastPathComponent
|
||||
let destinationURL = target.appendingPathComponent(fileName)
|
||||
|
||||
if !filesFinishedDownloading.contains(destinationURL) {
|
||||
log("File \(file) not finished downloading, skipping")
|
||||
continue
|
||||
}
|
||||
|
||||
log("Moving file: \(file.lastPathComponent) to \(destinationURL.lastPathComponent)")
|
||||
|
||||
do {
|
||||
try fileManager.copyItem(at: file, to: destinationURL)
|
||||
|
||||
let progress = Progress()
|
||||
progress.fileURL = destinationURL
|
||||
progress.totalUnitCount = 10
|
||||
progress.kind = .file
|
||||
progress.isPausable = false
|
||||
progress.publish()
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
progress.completedUnitCount = 10
|
||||
progress.unpublish()
|
||||
}
|
||||
|
||||
try fileManager.removeItem(at: file)
|
||||
}
|
||||
catch {
|
||||
log("Error moving file: \(error)")
|
||||
}
|
||||
}
|
||||
log("Moved all pending files to target directory")
|
||||
}
|
||||
catch {
|
||||
// Pending directory doesn't exist or is empty
|
||||
}
|
||||
}
|
||||
|
||||
if !filesFinishedDownloadingSinceLastRun.isEmpty && !isFileTransferRestricted() {
|
||||
|
||||
if UserDefaults.standard.bool(forKey: UserDefaultsKeys.openFinderAfterReceiving.rawValue) {
|
||||
log("Opening \(filesFinishedDownloadingSinceLastRun.count) file(s) in Finder.")
|
||||
NSWorkspace.shared.activateFileViewerSelecting(filesFinishedDownloadingSinceLastRun)
|
||||
}
|
||||
|
||||
// Clear the list of finished files
|
||||
filesFinishedDownloadingSinceLastRun.removeAll()
|
||||
}
|
||||
|
||||
stopAccessingSecurityScopedResource()
|
||||
}
|
||||
|
||||
public func stopAccessingSecurityScopedResource() {
|
||||
guard let url = securityScopeUrl else {
|
||||
return
|
||||
}
|
||||
|
||||
log("Stopping access to security scoped resource: \(url)")
|
||||
url.stopAccessingSecurityScopedResource()
|
||||
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() {
|
||||
log("Successfully accessed security scoped resource: \(url)")
|
||||
|
||||
securityScopeUrl = url
|
||||
return url
|
||||
}
|
||||
} else {
|
||||
log("Bookmark is stale, using default downloads folder.")
|
||||
}
|
||||
|
||||
} catch {
|
||||
log("Failed to resolve bookmark: \(error), using default downloads folder.")
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
return try FileManager.default.url(for: .downloadsDirectory, in: .userDomainMask, appropriateFor: nil, create: true).resolvingSymlinksInPath()
|
||||
} catch {
|
||||
fatalError("Failed to get downloads directory: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the EXIF original date of an image file, adjusted to the current time zone.
|
||||
private func exifOriginalDate(from url: URL) -> Date? {
|
||||
guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil),
|
||||
let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [CFString: Any],
|
||||
let exif = properties[kCGImagePropertyExifDictionary] as? [CFString: Any],
|
||||
let dateString = exif[kCGImagePropertyExifDateTimeOriginal] as? String
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy:MM:dd HH:mm:ss"
|
||||
formatter.timeZone = TimeZone.current // Use the current device time zone
|
||||
return formatter.date(from: dateString)
|
||||
}
|
||||
|
||||
|
||||
/// Applies EXIF timestamps to a given file or directory.
|
||||
private func applyEXIFTimestamps(at url: URL) {
|
||||
let fm = FileManager.default
|
||||
var isDir: ObjCBool = false
|
||||
guard fm.fileExists(atPath: url.path, isDirectory: &isDir) else { return }
|
||||
|
||||
if isDir.boolValue {
|
||||
// It's a directory — enumerate files
|
||||
if let enumerator = fm.enumerator(at: url, includingPropertiesForKeys: [.isRegularFileKey]) {
|
||||
for case let fileURL as URL in enumerator {
|
||||
guard (try? fileURL.resourceValues(forKeys: [.isRegularFileKey]))?.isRegularFile == true else { continue }
|
||||
applyEXIFTimestamp(to: fileURL)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// It's a single file
|
||||
applyEXIFTimestamp(to: url)
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies EXIF timestamp (if available) to a single file
|
||||
private func applyEXIFTimestamp(to fileURL: URL) {
|
||||
guard let exifDate = exifOriginalDate(from: fileURL) else {
|
||||
return
|
||||
}
|
||||
|
||||
// Set the "creation date"
|
||||
try? FileManager.default.setAttributes([.creationDate: exifDate], ofItemAtPath: fileURL.path)
|
||||
}
|
||||
}
|
||||
|
||||
extension Data {
|
||||
var hex: String {
|
||||
return map { String(format: "%02hhx", $0) }.joined()
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
var dataFromHex: Data {
|
||||
var data = Data(capacity: count / 2)
|
||||
var index = startIndex
|
||||
while index < endIndex {
|
||||
let nextIndex = self.index(index, offsetBy: 2)
|
||||
if nextIndex <= endIndex,
|
||||
let byte = UInt8(self[index ..< nextIndex], radix: 16)
|
||||
{
|
||||
data.append(byte)
|
||||
} else {
|
||||
break // or handle invalid hex gracefully
|
||||
}
|
||||
index = nextIndex
|
||||
}
|
||||
return data
|
||||
}
|
||||
}
|
||||
@@ -112,12 +112,6 @@ class NearbyConnection {
|
||||
func connectionReady() {}
|
||||
|
||||
|
||||
func handleConnectionClosure() {
|
||||
log("Connection closed")
|
||||
deletePartiallyReceivedFiles()
|
||||
}
|
||||
|
||||
|
||||
func protocolError() {
|
||||
log("Protocol error: \(String(describing: lastError))")
|
||||
disconnect()
|
||||
@@ -506,7 +500,7 @@ class NearbyConnection {
|
||||
inactivityTimer = nil
|
||||
connectionClosed = true
|
||||
|
||||
self.handleConnectionClosure()
|
||||
deletePartiallyReceivedFiles()
|
||||
|
||||
connection.send(content: nil, isComplete: true, completion: .contentProcessed { _ in
|
||||
})
|
||||
|
||||
@@ -11,7 +11,6 @@ import Foundation
|
||||
import Network
|
||||
import System
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
import BigInt
|
||||
import SwiftECC
|
||||
|
||||
@@ -46,6 +45,7 @@ class OutboundNearbyConnection: NearbyConnection {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
deinit {
|
||||
if let transfer = currentTransfer, let handle = transfer.handle {
|
||||
try? handle.close()
|
||||
@@ -57,8 +57,9 @@ class OutboundNearbyConnection: NearbyConnection {
|
||||
}
|
||||
}
|
||||
|
||||
override func handleConnectionClosure() {
|
||||
super.handleConnectionClosure()
|
||||
|
||||
override func disconnect() {
|
||||
super.disconnect()
|
||||
|
||||
if let error = lastError {
|
||||
DispatchQueue.main.async {
|
||||
@@ -67,6 +68,7 @@ class OutboundNearbyConnection: NearbyConnection {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func cancel() {
|
||||
cancelled = true
|
||||
if encryptionDone {
|
||||
@@ -78,6 +80,7 @@ class OutboundNearbyConnection: NearbyConnection {
|
||||
}
|
||||
try? sendDisconnectionAndDisconnect()
|
||||
}
|
||||
|
||||
|
||||
override func connectionReady() {
|
||||
super.connectionReady()
|
||||
@@ -89,11 +92,13 @@ class OutboundNearbyConnection: NearbyConnection {
|
||||
protocolError()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override func isServer() -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
override func processReceivedFrame(frameData: Data) {
|
||||
do {
|
||||
log("received \(frameData), state is \(currentState)")
|
||||
@@ -123,6 +128,7 @@ class OutboundNearbyConnection: NearbyConnection {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override func processTransferSetupFrame(_ frame: Sharing_Nearby_Frame) throws {
|
||||
if frame.hasV1 && frame.v1.hasType, case .cancel = frame.v1.type {
|
||||
log("Transfer canceled")
|
||||
@@ -144,12 +150,14 @@ class OutboundNearbyConnection: NearbyConnection {
|
||||
assertionFailure("Unexpected state \(currentState)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override func protocolError() {
|
||||
super.protocolError()
|
||||
delegate?.outboundConnection(connection: self, failedWithError: lastError!)
|
||||
}
|
||||
|
||||
|
||||
private func sendConnectionRequest() throws {
|
||||
var frame = Location_Nearby_Connections_OfflineFrame()
|
||||
frame.version = .v1
|
||||
@@ -164,6 +172,7 @@ class OutboundNearbyConnection: NearbyConnection {
|
||||
try sendFrameAsync(frame.serializedData())
|
||||
}
|
||||
|
||||
|
||||
private func sendUkey2ClientInit() throws {
|
||||
let domain = Domain.instance(curve: .EC256r1)
|
||||
let (pubKey, privKey) = domain.makeKeyPair()
|
||||
@@ -201,6 +210,7 @@ class OutboundNearbyConnection: NearbyConnection {
|
||||
sendFrameAsync(ukeyClientInitMsgData!)
|
||||
currentState = .sentUkeyClientInit
|
||||
}
|
||||
|
||||
|
||||
private func processUkey2ServerInit(frame: Securegcm_Ukey2Message, raw: Data) throws {
|
||||
ukeyServerInitMsgData = raw
|
||||
@@ -242,6 +252,7 @@ class OutboundNearbyConnection: NearbyConnection {
|
||||
delegate?.outboundConnectionWasEstablished(connection: self)
|
||||
}
|
||||
|
||||
|
||||
private func processConnectionResponse(frame: Location_Nearby_Connections_OfflineFrame) throws {
|
||||
log("connection response: \(frame)")
|
||||
guard frame.version == .v1 else { throw NearbyError.protocolError("Unexpected offline frame version \(frame.version)") }
|
||||
@@ -260,6 +271,7 @@ class OutboundNearbyConnection: NearbyConnection {
|
||||
currentState = .sentPairedKeyEncryption
|
||||
}
|
||||
|
||||
|
||||
private func processPairedKeyEncryption(frame: Sharing_Nearby_Frame) throws {
|
||||
guard frame.hasV1, frame.v1.hasPairedKeyEncryption else { throw NearbyError.requiredFieldMissing("sharingNearbyFrame.v1.pairedKeyEncryption") }
|
||||
var pairedResult = Sharing_Nearby_Frame()
|
||||
@@ -272,6 +284,7 @@ class OutboundNearbyConnection: NearbyConnection {
|
||||
currentState = .sentPairedKeyResult
|
||||
}
|
||||
|
||||
|
||||
private func processPairedKeyResult(frame: Sharing_Nearby_Frame) throws {
|
||||
guard frame.hasV1, frame.v1.hasPairedKeyResult else { throw NearbyError.requiredFieldMissing("sharingNearbyFrame.v1.pairedKeyResult") }
|
||||
|
||||
@@ -331,6 +344,7 @@ class OutboundNearbyConnection: NearbyConnection {
|
||||
|
||||
currentState = .sentIntroduction
|
||||
}
|
||||
|
||||
|
||||
private func processConsent(frame: Sharing_Nearby_Frame) throws {
|
||||
guard frame.version == .v1, frame.v1.type == .response else { throw NearbyError.requiredFieldMissing("sharingNearbyFrame.v1.type==response") }
|
||||
@@ -362,19 +376,23 @@ class OutboundNearbyConnection: NearbyConnection {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func hasURL() -> Bool {
|
||||
urlsToSend.count == 1 && !urlsToSend[0].isFileURL
|
||||
}
|
||||
|
||||
|
||||
private func sendURL() throws {
|
||||
try sendText(text: urlsToSend[0].absoluteString)
|
||||
}
|
||||
|
||||
|
||||
private func sendText(text: String) throws {
|
||||
try sendBytesPayload(data: Data(text.utf8), id: textPayloadID)
|
||||
delegate?.outboundConnectionTransferFinished(connection: self)
|
||||
try sendDisconnectionAndDisconnect()
|
||||
}
|
||||
|
||||
|
||||
private func sendNextFileChunk() throws {
|
||||
if cancelled {
|
||||
@@ -448,11 +466,13 @@ class OutboundNearbyConnection: NearbyConnection {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static func sanitizeFileName(name: String) -> String {
|
||||
return name.replacingOccurrences(of: "[\\/\\\\?%\\*:\\|\"<>=]", with: "_", options: .regularExpression)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private struct OutgoingFileTransfer {
|
||||
let url: URL
|
||||
let payloadID: Int64
|
||||
@@ -461,6 +481,7 @@ private struct OutgoingFileTransfer {
|
||||
var currentOffset: Int64
|
||||
}
|
||||
|
||||
|
||||
protocol OutboundNearbyConnectionDelegate {
|
||||
func outboundConnectionWasEstablished(connection: OutboundNearbyConnection)
|
||||
func outboundConnection(connection: OutboundNearbyConnection, transferProgress: Double)
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
//
|
||||
// SaveFilesManager.swift
|
||||
// QuickDrop
|
||||
//
|
||||
// Created by Leon Böttger on 26.07.25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AppKit
|
||||
import ImageIO
|
||||
|
||||
public class SaveFilesManager {
|
||||
|
||||
private init() {
|
||||
// remove old temp directory
|
||||
|
||||
let tempPath = FileManager.default.temporaryDirectory
|
||||
let fileManager = FileManager.default
|
||||
|
||||
do {
|
||||
let contents = try fileManager.contentsOfDirectory(at: tempPath, includingPropertiesForKeys: nil)
|
||||
|
||||
var didSomething = false
|
||||
|
||||
for item in contents {
|
||||
didSomething = true
|
||||
try fileManager.removeItem(at: item)
|
||||
}
|
||||
|
||||
if didSomething {
|
||||
log("Temporary directory cleared.")
|
||||
}
|
||||
} catch {
|
||||
log("Failed to list contents of temp directory: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
public static let shared = SaveFilesManager()
|
||||
|
||||
let tempDirectory: URL = FileManager.default.temporaryDirectory.appendingPathComponent("Pending")
|
||||
private var securityScopeUrl: URL?
|
||||
|
||||
private var filesFinishedDownloading = [URL]()
|
||||
private var filesFinishedDownloadingSinceLastRun = [URL]()
|
||||
|
||||
|
||||
public func registerFileFinishedDownloading(_ fileURL: URL) {
|
||||
filesFinishedDownloading.append(fileURL)
|
||||
filesFinishedDownloadingSinceLastRun.append(fileURL)
|
||||
|
||||
// read out EXIF timestamp and manually set file creation date to it
|
||||
applyEXIFTimestamps(at: fileURL)
|
||||
}
|
||||
|
||||
|
||||
public func movePendingFilesToTarget() {
|
||||
|
||||
if isPlusVersion() {
|
||||
do {
|
||||
let fileManager = FileManager.default
|
||||
let files = try fileManager.contentsOfDirectory(at: tempDirectory, includingPropertiesForKeys: nil)
|
||||
|
||||
let target = getSaveDirectory()
|
||||
var loggedExecution = false
|
||||
|
||||
for file in files {
|
||||
|
||||
if !loggedExecution {
|
||||
log("Moving pending files to target directory")
|
||||
loggedExecution = true
|
||||
}
|
||||
|
||||
let fileName = file.lastPathComponent
|
||||
let destinationURL = target.appendingPathComponent(fileName)
|
||||
|
||||
if !filesFinishedDownloading.contains(destinationURL) {
|
||||
log("File \(file) not finished downloading, skipping")
|
||||
continue
|
||||
}
|
||||
|
||||
log("Moving file: \(file.lastPathComponent) to \(destinationURL.lastPathComponent)")
|
||||
|
||||
do {
|
||||
try fileManager.copyItem(at: file, to: destinationURL)
|
||||
|
||||
let progress = Progress()
|
||||
progress.fileURL = destinationURL
|
||||
progress.totalUnitCount = 10
|
||||
progress.kind = .file
|
||||
progress.isPausable = false
|
||||
progress.publish()
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
progress.completedUnitCount = 10
|
||||
progress.unpublish()
|
||||
}
|
||||
|
||||
try fileManager.removeItem(at: file)
|
||||
}
|
||||
catch {
|
||||
log("Error moving file: \(error)")
|
||||
}
|
||||
}
|
||||
log("Moved all pending files to target directory")
|
||||
}
|
||||
catch {
|
||||
// Pending directory doesn't exist or is empty
|
||||
}
|
||||
}
|
||||
|
||||
if !filesFinishedDownloadingSinceLastRun.isEmpty && !isFileTransferRestricted() {
|
||||
|
||||
if UserDefaults.standard.bool(forKey: UserDefaultsKeys.openFinderAfterReceiving.rawValue) {
|
||||
log("Opening \(filesFinishedDownloadingSinceLastRun.count) file(s) in Finder.")
|
||||
NSWorkspace.shared.activateFileViewerSelecting(filesFinishedDownloadingSinceLastRun)
|
||||
}
|
||||
|
||||
// Clear the list of finished files
|
||||
filesFinishedDownloadingSinceLastRun.removeAll()
|
||||
}
|
||||
|
||||
stopAccessingSecurityScopedResource()
|
||||
}
|
||||
|
||||
|
||||
public func stopAccessingSecurityScopedResource() {
|
||||
guard let url = securityScopeUrl else {
|
||||
return
|
||||
}
|
||||
|
||||
log("Stopping access to security scoped resource: \(url)")
|
||||
url.stopAccessingSecurityScopedResource()
|
||||
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() {
|
||||
log("Successfully accessed security scoped resource: \(url)")
|
||||
|
||||
securityScopeUrl = url
|
||||
return url
|
||||
}
|
||||
} else {
|
||||
log("Bookmark is stale, using default downloads folder.")
|
||||
}
|
||||
|
||||
} catch {
|
||||
log("Failed to resolve bookmark: \(error), using default downloads folder.")
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
return try FileManager.default.url(for: .downloadsDirectory, in: .userDomainMask, appropriateFor: nil, create: true).resolvingSymlinksInPath()
|
||||
} catch {
|
||||
fatalError("Failed to get downloads directory: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Returns the EXIF original date of an image file, adjusted to the current time zone.
|
||||
private func exifOriginalDate(from url: URL) -> Date? {
|
||||
guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil),
|
||||
let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [CFString: Any],
|
||||
let exif = properties[kCGImagePropertyExifDictionary] as? [CFString: Any],
|
||||
let dateString = exif[kCGImagePropertyExifDateTimeOriginal] as? String
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy:MM:dd HH:mm:ss"
|
||||
formatter.timeZone = TimeZone.current // Use the current device time zone
|
||||
return formatter.date(from: dateString)
|
||||
}
|
||||
|
||||
|
||||
/// Applies EXIF timestamps to a given file or directory.
|
||||
private func applyEXIFTimestamps(at url: URL) {
|
||||
let fm = FileManager.default
|
||||
var isDir: ObjCBool = false
|
||||
guard fm.fileExists(atPath: url.path, isDirectory: &isDir) else { return }
|
||||
|
||||
if isDir.boolValue {
|
||||
// It's a directory — enumerate files
|
||||
if let enumerator = fm.enumerator(at: url, includingPropertiesForKeys: [.isRegularFileKey]) {
|
||||
for case let fileURL as URL in enumerator {
|
||||
guard (try? fileURL.resourceValues(forKeys: [.isRegularFileKey]))?.isRegularFile == true else { continue }
|
||||
applyEXIFTimestamp(to: fileURL)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// It's a single file
|
||||
applyEXIFTimestamp(to: url)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Applies EXIF timestamp (if available) to a single file
|
||||
private func applyEXIFTimestamp(to fileURL: URL) {
|
||||
guard let exifDate = exifOriginalDate(from: fileURL) else {
|
||||
return
|
||||
}
|
||||
|
||||
// Set the "creation date"
|
||||
try? FileManager.default.setAttributes([.creationDate: exifDate], ofItemAtPath: fileURL.path)
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,6 @@
|
||||
69DCF4902AB70E9700CBE2CC /* InboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */; };
|
||||
69DCF4912AB70E9700CBE2CC /* NearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698DFAE529E2F91A0064F247 /* NearbyConnection.swift */; };
|
||||
69DCF4922AB70E9700CBE2CC /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
|
||||
69DCF4932AB70E9700CBE2CC /* Data+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2029E0CC4E00A442DA /* Data+Extensions.swift */; };
|
||||
69DCF4942AB8BF7B00CBE2CC /* libNearbyShare.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 69DCF4812AB70D0600CBE2CC /* libNearbyShare.dylib */; };
|
||||
69DCF4952AB8BF7B00CBE2CC /* libNearbyShare.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 69DCF4812AB70D0600CBE2CC /* libNearbyShare.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
||||
69DCF49A2AB8C58500CBE2CC /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = 69DCF4992AB8C58500CBE2CC /* SwiftProtobuf */; };
|
||||
@@ -40,6 +39,7 @@
|
||||
EB5C36942E34EBBD0052EFD7 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = EB5C36922E34EBBD0052EFD7 /* Localizable.xcstrings */; };
|
||||
EB5C36E42E34EC0E0052EFD7 /* License.txt in Resources */ = {isa = PBXBuildFile; fileRef = EB5C36E32E34EC0E0052EFD7 /* License.txt */; };
|
||||
EB5C36E52E34EC0E0052EFD7 /* License.txt in Resources */ = {isa = PBXBuildFile; fileRef = EB5C36E32E34EC0E0052EFD7 /* License.txt */; };
|
||||
EB5C36E72E3544FF0052EFD7 /* SaveFilesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C36E62E3544FB0052EFD7 /* SaveFilesManager.swift */; };
|
||||
EB6E743E2DD5C77700331466 /* ConnectionFailureTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB6E743D2DD5C76F00331466 /* ConnectionFailureTracker.swift */; };
|
||||
EB6F62D72D91F4AF00534EC2 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB6F62D62D91F4AC00534EC2 /* SettingsView.swift */; };
|
||||
EB7CD1452D6FB0C300CB0CB5 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB7CD1442D6FB0C100CB0CB5 /* Logger.swift */; };
|
||||
@@ -164,7 +164,6 @@
|
||||
69DA9A1329E0BF5200A442DA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
69DA9A1829E0BF5200A442DA /* QuickDrop.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = QuickDrop.entitlements; sourceTree = "<group>"; };
|
||||
69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearbyConnectionManager.swift; sourceTree = "<group>"; };
|
||||
69DA9A2029E0CC4E00A442DA /* Data+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Extensions.swift"; sourceTree = "<group>"; };
|
||||
69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InboundNearbyConnection.swift; sourceTree = "<group>"; };
|
||||
69DA9A2829E18CB500A442DA /* ukey.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ukey.pb.swift; sourceTree = "<group>"; };
|
||||
69DA9A2929E18CB500A442DA /* wire_format.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = wire_format.pb.swift; sourceTree = "<group>"; };
|
||||
@@ -185,6 +184,7 @@
|
||||
EB5C36922E34EBBD0052EFD7 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
|
||||
EB5C36952E34EBBD0052EFD7 /* mul */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = mul; path = mul.lproj/ShareViewController.xcstrings; sourceTree = "<group>"; };
|
||||
EB5C36E32E34EC0E0052EFD7 /* License.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = License.txt; sourceTree = "<group>"; };
|
||||
EB5C36E62E3544FB0052EFD7 /* SaveFilesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveFilesManager.swift; sourceTree = "<group>"; };
|
||||
EB6E743D2DD5C76F00331466 /* ConnectionFailureTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionFailureTracker.swift; sourceTree = "<group>"; };
|
||||
EB6F62D62D91F4AC00534EC2 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||
EB7CD1442D6FB0C100CB0CB5 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
|
||||
@@ -323,10 +323,10 @@
|
||||
69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */,
|
||||
69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */,
|
||||
691F53BD2ABF03820089FD92 /* OutboundNearbyConnection.swift */,
|
||||
EB5C36E62E3544FB0052EFD7 /* SaveFilesManager.swift */,
|
||||
EB2F9BFA2DA04B67004010A2 /* Helpers.swift */,
|
||||
698DFAE529E2F91A0064F247 /* NearbyConnection.swift */,
|
||||
EB6E743D2DD5C76F00331466 /* ConnectionFailureTracker.swift */,
|
||||
69DA9A2029E0CC4E00A442DA /* Data+Extensions.swift */,
|
||||
);
|
||||
path = NearbyShare;
|
||||
sourceTree = "<group>";
|
||||
@@ -640,8 +640,8 @@
|
||||
69DCF4902AB70E9700CBE2CC /* InboundNearbyConnection.swift in Sources */,
|
||||
69DCF48B2AB70E8C00CBE2CC /* ukey.pb.swift in Sources */,
|
||||
EB2B41E42DC63ACC0070FC39 /* SleepManager.swift in Sources */,
|
||||
EB5C36E72E3544FF0052EFD7 /* SaveFilesManager.swift in Sources */,
|
||||
69DCF48A2AB70E8C00CBE2CC /* wire_format.pb.swift in Sources */,
|
||||
69DCF4932AB70E9700CBE2CC /* Data+Extensions.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user