Refactoring

This commit is contained in:
Leon Böttger committed 2025-07-26 19:19:34 +02:00
1 parent 98d1596672
commit 13716f41e2
7 files changed
+341 -293

No files matched your search

-47
View File
@@ -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)
}
}
+69
View File
@@ -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"
+23 -232
View File
@@ -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
}
}
+1 -7
View File
@@ -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
})
+24 -3
View File
@@ -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)
+220
View File
@@ -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)
}
}
+4 -4
View File
@@ -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;
};