diff --git a/NearbyShare/Connection/InboundNearbyConnection.swift b/NearbyShare/Connection/InboundNearbyConnection.swift index 7ca21c2..005e514 100644 --- a/NearbyShare/Connection/InboundNearbyConnection.swift +++ b/NearbyShare/Connection/InboundNearbyConnection.swift @@ -51,8 +51,7 @@ class InboundNearbyConnection: NearbyConnection { DispatchQueue.main.async { self.delegate?.connectionWasTerminated(connection: self, error: self.lastError) - SaveFilesManager.shared.movePendingFilesToTarget() - SaveFilesManager.shared.stopAccessingSecurityScopedResource() + SaveFilesManager.shared.movePendingFilesToTarget() } } @@ -170,6 +169,7 @@ class InboundNearbyConnection: NearbyConnection { fileInfo.progress?.unpublish() #endif SaveFilesManager.shared.registerFileFinishedDownloading(fileInfo.destinationURL) + EXIFUtils.applyEXIFTimestamps(at: fileInfo.destinationURL) filesToBeReceived.removeValue(forKey: id) @@ -223,6 +223,7 @@ class InboundNearbyConnection: NearbyConnection { #endif filesToBeReceived.removeValue(forKey: id) SaveFilesManager.shared.registerFileFinishedDownloading(fileInfo.destinationURL) + EXIFUtils.applyEXIFTimestamps(at: fileInfo.destinationURL) if filesToBeReceived.isEmpty { log("[InboundNearbyConnection \(self.id)] Received file payload. Disconnecting...") @@ -584,24 +585,14 @@ class InboundNearbyConnection: NearbyConnection { return } - let storeInTemp = isFileTransferRestricted() - - if storeInTemp { - log("[InboundNearbyConnection \(self.id)] File transfer restrictions detected, storing files in temporary directory.") + if isFileTransferRestricted() { + log("[InboundNearbyConnection \(self.id)] File transfer restrictions detected.") delegate?.showPlusScreen() - - #if os(iOS) - // Continue transfer for macOS only - // On iOS, the transfer should be cancelled by user rejectTransfer() return - #endif } do { - if storeInTemp { - try FileManager.default.createDirectory(at: SaveFilesManager.shared.tempDirectory, withIntermediateDirectories: true) - } if filesToBeReceived.count >= 1 { // Show progress bar for file transfers immediately @@ -609,7 +600,7 @@ class InboundNearbyConnection: NearbyConnection { } for (id, file) in filesToBeReceived { - let targetURL = storeInTemp ? SaveFilesManager.shared.tempDirectory.appendingPathComponent(file.destinationURL.lastPathComponent) : file.destinationURL + let targetURL = file.destinationURL FileManager.default.createFile(atPath: targetURL.path, contents: nil) let handle = try FileHandle(forWritingTo: targetURL) diff --git a/NearbyShare/Connection/NearbyConnectionManager.swift b/NearbyShare/Connection/NearbyConnectionManager.swift index dbf0e34..4e3067b 100644 --- a/NearbyShare/Connection/NearbyConnectionManager.swift +++ b/NearbyShare/Connection/NearbyConnectionManager.swift @@ -492,7 +492,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb outgoingTransfers[deviceID] = transfer conn.start() } catch { - log("[NearbyConnectionManager] Error zipping URLs: \(error)") + log("[NearbyConnectionManager] Error storing URLs: \(error)") outboundAppDelegates.forEach { delegate in delegate.connectionFailed(error: error) } diff --git a/NearbyShare/Util/EXIFUtils.swift b/NearbyShare/Util/EXIFUtils.swift new file mode 100644 index 0000000..e789ed1 --- /dev/null +++ b/NearbyShare/Util/EXIFUtils.swift @@ -0,0 +1,60 @@ +// +// EXIFUtils.swift +// QuickDrop +// +// Created by Leon Böttger on 10.11.25. +// + +import Foundation +import ImageIO + +struct EXIFUtils { + + /// Applies EXIF timestamps to a given file or directory. + static 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 static 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) + } + + + /// Returns the EXIF original date of an image file, adjusted to the current time zone. + private static 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) + } +} diff --git a/NearbyShare/Util/SaveFilesManager.swift b/NearbyShare/Util/SaveFilesManager.swift index 4996581..af91fa3 100644 --- a/NearbyShare/Util/SaveFilesManager.swift +++ b/NearbyShare/Util/SaveFilesManager.swift @@ -6,7 +6,6 @@ // import Foundation -import ImageIO #if os(macOS) import AppKit #else @@ -39,84 +38,21 @@ public class SaveFilesManager { log("[SaveFilesManager] 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 fullVersion() { - 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("[SaveFilesManager] Moving pending files to target directory") - loggedExecution = true - } - - let fileName = file.lastPathComponent - let destinationURL = target.appendingPathComponent(fileName) - - if !filesFinishedDownloading.contains(destinationURL) { - log("[SaveFilesManager] File \(file) not finished downloading, skipping") - continue - } - - log("[SaveFilesManager] 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 - #if os(macOS) - progress.publish() - #endif - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - progress.completedUnitCount = 10 - #if os(macOS) - progress.unpublish() - #endif - } - - try fileManager.removeItem(at: file) - } - catch { - log("[SaveFilesManager] Error moving file: \(error)") - } - } - log("[SaveFilesManager] Moved all pending files to target directory") - } - catch { - // Pending directory doesn't exist or is empty - } - } - if !filesFinishedDownloadingSinceLastRun.isEmpty && !isFileTransferRestricted() { + if !filesFinishedDownloadingSinceLastRun.isEmpty { #if os(macOS) if Settings.shared.openFinderAfterReceiving { @@ -207,53 +143,4 @@ public class SaveFilesManager { } #endif } - - - /// 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) - } } diff --git a/QuickDrop.xcodeproj/project.pbxproj b/QuickDrop.xcodeproj/project.pbxproj index c22040e..9cd8b3b 100644 --- a/QuickDrop.xcodeproj/project.pbxproj +++ b/QuickDrop.xcodeproj/project.pbxproj @@ -46,6 +46,10 @@ EB2F9BFE2DA05F48004010A2 /* BezelNotification in Frameworks */ = {isa = PBXBuildFile; productRef = EB2F9BFD2DA05F48004010A2 /* BezelNotification */; }; EB3643602D73A2A2009CDFF8 /* AudioManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB36435F2D73A29F009CDFF8 /* AudioManager.swift */; }; EB4F9D1D2EC113EA00A74A0F /* DistributionDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB704A812E4A7BAE008E608F /* DistributionDetector.swift */; }; + EB4F9D1F2EC26C8100A74A0F /* EXIFUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB4F9D1E2EC26C7C00A74A0F /* EXIFUtils.swift */; }; + EB4F9D202EC26C8100A74A0F /* EXIFUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB4F9D1E2EC26C7C00A74A0F /* EXIFUtils.swift */; }; + EB4F9D212EC26C8100A74A0F /* EXIFUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB4F9D1E2EC26C7C00A74A0F /* EXIFUtils.swift */; }; + EB4F9D222EC26C8100A74A0F /* EXIFUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB4F9D1E2EC26C7C00A74A0F /* EXIFUtils.swift */; }; EB5C36932E34EBBD0052EFD7 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = EB5C36922E34EBBD0052EFD7 /* Localizable.xcstrings */; }; EB5C36942E34EBBD0052EFD7 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = EB5C36922E34EBBD0052EFD7 /* Localizable.xcstrings */; }; EB5C36E42E34EC0E0052EFD7 /* License.txt in Resources */ = {isa = PBXBuildFile; fileRef = EB5C36E32E34EC0E0052EFD7 /* License.txt */; }; @@ -328,6 +332,7 @@ EB2B41E32DC63AC80070FC39 /* SleepManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SleepManager.swift; sourceTree = ""; }; EB2F9BFA2DA04B67004010A2 /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = ""; }; EB36435F2D73A29F009CDFF8 /* AudioManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioManager.swift; sourceTree = ""; }; + EB4F9D1E2EC26C7C00A74A0F /* EXIFUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EXIFUtils.swift; sourceTree = ""; }; EB5C36922E34EBBD0052EFD7 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; EB5C36952E34EBBD0052EFD7 /* mul */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = mul; path = mul.lproj/ShareViewController.xcstrings; sourceTree = ""; }; EB5C36E32E34EC0E0052EFD7 /* License.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = License.txt; sourceTree = ""; }; @@ -535,6 +540,7 @@ EBCC27F82D80C9F70079C478 /* Zip.swift */, EB16B7762E59BA9F007D9792 /* AppGroup.swift */, EB886E512E4E19390052B7D9 /* HKDF.swift */, + EB4F9D1E2EC26C7C00A74A0F /* EXIFUtils.swift */, EB7CD1442D6FB0C100CB0CB5 /* Logger.swift */, EB5C36E62E3544FB0052EFD7 /* SaveFilesManager.swift */, EB2F9BFA2DA04B67004010A2 /* Helpers.swift */, @@ -933,6 +939,7 @@ EB5C37C72E377A780052EFD7 /* wire_format.pb.swift in Sources */, EB5C37C82E377A780052EFD7 /* ukey.pb.swift in Sources */, EB5C37C92E377A780052EFD7 /* device_to_device_messages.pb.swift in Sources */, + EB4F9D222EC26C8100A74A0F /* EXIFUtils.swift in Sources */, EB950B742E520957002D68FA /* Attachments.swift in Sources */, EB1325D32E701D8E00FD749C /* Settings.swift in Sources */, EB16B7782E59BAA1007D9792 /* AppGroup.swift in Sources */, @@ -995,6 +1002,7 @@ EB96ED8C2D7F7E7B004B3FF1 /* SmallSheetView.swift in Sources */, EB886E682E4F2F800052B7D9 /* License.swift in Sources */, EB7F54142D78E9E700989564 /* LicenseWindow.swift in Sources */, + EB4F9D202EC26C8100A74A0F /* EXIFUtils.swift in Sources */, 69DA9A1229E0BF5100A442DA /* AppDelegate.swift in Sources */, EB6F62D72D91F4AF00534EC2 /* SettingsView.swift in Sources */, EB7F54122D78E96000989564 /* TutorialView.swift in Sources */, @@ -1041,6 +1049,7 @@ EB5C37FD2E377CE70052EFD7 /* Logger.swift in Sources */, EB97A00F2E511124008B8C5C /* ReceiveModel.swift in Sources */, EB5C37FE2E377CE70052EFD7 /* offline_wire_formats.pb.swift in Sources */, + EB4F9D1F2EC26C8100A74A0F /* EXIFUtils.swift in Sources */, EB886E6A2E4F37B90052B7D9 /* ErrorAlertHandler.swift in Sources */, EB886E5E2E4F13EB0052B7D9 /* Zip.swift in Sources */, EB5C383D2E38C2D90052EFD7 /* AudioManager.swift in Sources */, @@ -1074,6 +1083,7 @@ EB950B4B2E520436002D68FA /* offline_wire_formats.pb.swift in Sources */, EB950B4C2E520436002D68FA /* InboundNearbyConnection.swift in Sources */, EB16B6132E58B4B9007D9792 /* IdentityManager.swift in Sources */, + EB4F9D212EC26C8100A74A0F /* EXIFUtils.swift in Sources */, EB950B4D2E520436002D68FA /* NearbyConnection.swift in Sources */, EB950B4F2E520436002D68FA /* Logger.swift in Sources */, EB950B502E520436002D68FA /* wire_format.pb.swift in Sources */,