From c9b4365e9f4e5a9bb74a812c15ead7ac1ddb4b7d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 01:08:56 +0000 Subject: [PATCH] Materialize cloud-provider files (Google Drive, iCloud, Dropbox) on local import Files picked from cloud file providers often arrive as unmaterialized placeholders, so the direct copyItem into the audio cache fails. Retry the copy through NSFileCoordinator: the coordinated read tells the provider to download the real bytes (blocking off-main while it fetches), after which the normal import path proceeds. Makes the upload button work with Google Drive and other Files-app providers, not just on-device storage. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_013TJoWkqg8bzkGdzxWhWjWP --- .../Ingest/PreparationQueue+LocalImport.swift | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/Packages/ContinuityKit/Sources/Ingest/PreparationQueue+LocalImport.swift b/Packages/ContinuityKit/Sources/Ingest/PreparationQueue+LocalImport.swift index 091de59..c6d98a0 100644 --- a/Packages/ContinuityKit/Sources/Ingest/PreparationQueue+LocalImport.swift +++ b/Packages/ContinuityKit/Sources/Ingest/PreparationQueue+LocalImport.swift @@ -71,11 +71,46 @@ extension PreparationQueue { let ext = url.pathExtension.isEmpty ? "m4a" : url.pathExtension.lowercased() let destination = AudioCache.fileURL(videoID: trackID.uuidString, container: ext) do { - try FileManager.default.copyItem(at: url, to: destination) + try await Self.copyMaterialized(from: url, to: destination) } catch { Logger.ingest.error("local import copy failed for \(url.lastPathComponent, privacy: .public): \(String(describing: error), privacy: .public)") return false } + return await finishImport(of: destination, originalURL: url, trackID: trackID, in: context) + } + + /// Copies a picked file into the cache, downloading it first when it lives in a cloud + /// file provider (Google Drive, iCloud Drive, Dropbox…). Cloud picks often arrive as + /// unmaterialized placeholders — a plain `copyItem` fails on those — so on failure we + /// retry through `NSFileCoordinator`, whose coordinated read tells the provider to fetch + /// the real bytes. The coordinated read blocks while the provider downloads, so it runs + /// off the main actor. + private static func copyMaterialized(from url: URL, to destination: URL) async throws { + do { + try FileManager.default.copyItem(at: url, to: destination) + } catch { + try await Task.detached(priority: .userInitiated) { + var coordinatorError: NSError? + var copyError: Error? + NSFileCoordinator().coordinate( + readingItemAt: url, options: [], error: &coordinatorError + ) { materialized in + do { + try? FileManager.default.removeItem(at: destination) // partial first try + try FileManager.default.copyItem(at: materialized, to: destination) + } catch { + copyError = error + } + } + if let coordinatorError { throw coordinatorError } + if let copyError { throw copyError } + }.value + } + } + + /// Metadata extraction + model creation for a file already copied into the cache. + private func finishImport(of destination: URL, originalURL url: URL, trackID: UUID, + in context: ModelContext) async -> Bool { // Metadata load runs off-main (AVURLAsset touches the file). let fallbackTitle = url.deletingPathExtension().lastPathComponent