@@ -32,8 +32,11 @@ public class DesktopDropPlugin: NSObject, FlutterPlugin {
3232 let d = DropTarget ( frame: vc. view. bounds, channel: channel)
3333 d. autoresizingMask = [ . width, . height]
3434
35- d. registerForDraggedTypes ( NSFilePromiseReceiver . readableDraggedTypes. map { NSPasteboard . PasteboardType ( $0) } )
36- d. registerForDraggedTypes ( [ NSPasteboard . PasteboardType. fileURL] )
35+ // Register for all relevant types (promises, URLs, and legacy filename arrays)
36+ var types = NSFilePromiseReceiver . readableDraggedTypes. map { NSPasteboard . PasteboardType ( $0) }
37+ types. append ( . fileURL) // public.file-url
38+ types. append ( NSPasteboard . PasteboardType ( " NSFilenamesPboardType " ) ) // legacy multi-file array
39+ d. registerForDraggedTypes ( types)
3740
3841 vc. view. addSubview ( d)
3942
@@ -49,7 +52,7 @@ public class DesktopDropPlugin: NSObject, FlutterPlugin {
4952 let bookmarkByte = map [ " apple-bookmark " ] as! FlutterStandardTypedData
5053 let bookmark = bookmarkByte. data
5154
52- let url = try ? URL ( resolvingBookmarkData: bookmark, bookmarkDataIsStale: & isStale)
55+ let url = try ? URL ( resolvingBookmarkData: bookmark, options : [ . withSecurityScope ] , relativeTo : nil , bookmarkDataIsStale: & isStale)
5356 let suc = url? . startAccessingSecurityScopedResource ( )
5457 result ( suc)
5558 return
@@ -60,7 +63,7 @@ public class DesktopDropPlugin: NSObject, FlutterPlugin {
6063 var isStale : Bool = false
6164 let bookmarkByte = map [ " apple-bookmark " ] as! FlutterStandardTypedData
6265 let bookmark = bookmarkByte. data
63- let url = try ? URL ( resolvingBookmarkData: bookmark, bookmarkDataIsStale: & isStale)
66+ let url = try ? URL ( resolvingBookmarkData: bookmark, options : [ . withSecurityScope ] , relativeTo : nil , bookmarkDataIsStale: & isStale)
6467 url? . stopAccessingSecurityScopedResource ( )
6568 result ( true )
6669 return
@@ -76,6 +79,7 @@ public class DesktopDropPlugin: NSObject, FlutterPlugin {
7679
7780class DropTarget : NSView {
7881 private let channel : FlutterMethodChannel
82+ private let itemsLock = NSLock ( )
7983
8084 init ( frame frameRect: NSRect , channel: FlutterMethodChannel ) {
8185 self . channel = channel
@@ -100,12 +104,17 @@ class DropTarget: NSView {
100104 channel. invokeMethod ( " exited " , arguments: nil )
101105 }
102106
103- /// Directory URL used for accepting file promises.
104- private lazy var destinationURL : URL = {
105- let destinationURL = URL ( fileURLWithPath: NSTemporaryDirectory ( ) ) . appendingPathComponent ( " Drops " )
106- try ? FileManager . default. createDirectory ( at: destinationURL, withIntermediateDirectories: true , attributes: nil )
107- return destinationURL
108- } ( )
107+ /// Create a per-drop destination for promised files (avoids name collisions).
108+ private func uniqueDropDestination( ) -> URL {
109+ let base = FileManager . default. temporaryDirectory. appendingPathComponent ( " Drops " , isDirectory: true )
110+ let formatter = DateFormatter ( )
111+ formatter. locale = Locale ( identifier: " en_US_POSIX " )
112+ formatter. dateFormat = " yyyyMMdd_HHmmss_SSS'Z' "
113+ let stamp = formatter. string ( from: Date ( ) )
114+ let dest = base. appendingPathComponent ( stamp, isDirectory: true )
115+ try ? FileManager . default. createDirectory ( at: dest, withIntermediateDirectories: true , attributes: nil )
116+ return dest
117+ }
109118
110119 /// Queue used for reading and writing file promises.
111120 private lazy var workQueue : OperationQueue = {
@@ -114,40 +123,70 @@ class DropTarget: NSView {
114123 return providerQueue
115124 } ( )
116125
117- override func performDragOperation( _ sender: NSDraggingInfo ) -> Bool {
118- var items : [ [ String : Any ? ] ] = [ ] ;
119-
120- let searchOptions : [ NSPasteboard . ReadingOptionKey : Any ] = [
121- . urlReadingFileURLsOnly: true ,
122- ]
123-
126+ override func performDragOperation( _ sender: NSDraggingInfo ) -> Bool {
127+ let pb = sender. draggingPasteboard
128+ let dest = uniqueDropDestination ( )
129+ var items : [ [ String : Any ] ] = [ ]
130+ var seen = Set < String > ( )
124131 let group = DispatchGroup ( )
125132
126- // retrieve NSFilePromise.
127- sender. enumerateDraggingItems ( options: [ ] , for: nil , classes: [ NSFilePromiseReceiver . self, NSURL . self] , searchOptions: searchOptions) { draggingItem, _, _ in
128- switch draggingItem. item {
129- case let filePromiseReceiver as NSFilePromiseReceiver :
130- group. enter ( )
131- filePromiseReceiver. receivePromisedFiles ( atDestination: self . destinationURL, options: [ : ] , operationQueue: self . workQueue) { fileURL, error in
132- if let error = error {
133- debugPrint ( " error: \( error) " )
134- } else {
135- let data = try ? fileURL. bookmarkData ( )
136- items. append ( [
137- " path " : fileURL. path,
138- " apple-bookmark " : data,
139- ] )
133+ func push( url: URL , fromPromise: Bool ) {
134+ let path = url. path
135+ itemsLock. lock ( ) ; defer { itemsLock. unlock ( ) }
136+
137+ // de-dupe safely under lock
138+ if !seen. insert ( path) . inserted { return }
139+
140+ let values = try ? url. resourceValues ( forKeys: [ . isDirectoryKey] )
141+ let isDirectory : Bool = values? . isDirectory ?? false
142+
143+ // Only create a security-scoped bookmark for items outside our container.
144+ let bundleID = Bundle . main. bundleIdentifier ?? " "
145+ let containerRoot = FileManager . default. homeDirectoryForCurrentUser
146+ . appendingPathComponent ( " Library/Containers/ \( bundleID) " , isDirectory: true )
147+ . path
148+ let tmpPath = FileManager . default. temporaryDirectory. path
149+ let isInsideContainer = path. hasPrefix ( containerRoot) || path. hasPrefix ( tmpPath)
150+
151+ let bmData : Any
152+ if isInsideContainer {
153+ bmData = NSNull ( )
154+ } else {
155+ let bm = try ? url. bookmarkData ( options: [ . withSecurityScope] , includingResourceValuesForKeys: nil , relativeTo: nil )
156+ bmData = bm ?? NSNull ( )
157+ }
158+ items. append ( [
159+ " path " : path,
160+ " apple-bookmark " : bmData,
161+ " isDirectory " : isDirectory,
162+ " fromPromise " : fromPromise,
163+ ] )
164+ }
165+
166+ // Prefer real file URLs if they exist; only fall back to promises
167+ let urls = ( pb. readObjects ( forClasses: [ NSURL . self] , options: [ . urlReadingFileURLsOnly: true ] ) as? [ URL ] ) ?? [ ]
168+ let legacyList = ( pb. propertyList ( forType: NSPasteboard . PasteboardType ( " NSFilenamesPboardType " ) ) as? [ String ] ) ?? [ ]
169+
170+ if !urls. isEmpty || !legacyList. isEmpty {
171+ // 1) Modern file URLs
172+ urls. forEach { push ( url: $0, fromPromise: false ) }
173+ // 2) Legacy filename array used by some apps
174+ legacyList. forEach { push ( url: URL ( fileURLWithPath: $0) , fromPromise: false ) }
175+ } else {
176+ // 3) Handle file promises (e.g., VS Code, browsers, Mail)
177+ if let receivers = pb. readObjects ( forClasses: [ NSFilePromiseReceiver . self] , options: nil ) as? [ NSFilePromiseReceiver ] ,
178+ !receivers. isEmpty {
179+ for r in receivers {
180+ group. enter ( )
181+ r. receivePromisedFiles ( atDestination: dest, options: [ : ] , operationQueue: self . workQueue) { url, error in
182+ defer { group. leave ( ) }
183+ if let error = error {
184+ debugPrint ( " NSFilePromiseReceiver error: \( error) " )
185+ return
186+ }
187+ push ( url: url, fromPromise: true )
140188 }
141- group. leave ( )
142189 }
143- case let fileURL as URL :
144- let data = try ? fileURL. bookmarkData ( )
145-
146- items. append ( [
147- " path " : fileURL. path,
148- " apple-bookmark " : data,
149- ] )
150- default : break
151190 }
152191 }
153192
0 commit comments