iOS: native upload and download services (background URLSession)
Swift implementation of the upload_service/download_service channels: background PUT/GET against the account's WebDAV root, one summary local notification per batch, Downloads visible in the Files app. Pure helpers covered by RunnerTests. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
d008185f98
commit
469e57cca7
@@ -0,0 +1,114 @@
|
||||
import Flutter
|
||||
import Foundation
|
||||
|
||||
/// Pushes upload-batch completions to Dart's `UploadService.completions`.
|
||||
final class UploadStatusStreamHandler: NSObject, FlutterStreamHandler {
|
||||
var sink: FlutterEventSink?
|
||||
|
||||
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink)
|
||||
-> FlutterError?
|
||||
{
|
||||
sink = events
|
||||
return nil
|
||||
}
|
||||
|
||||
func onCancel(withArguments arguments: Any?) -> FlutterError? {
|
||||
sink = nil
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// The iOS side of the `dev.ayushya.noo/*` channels `lib/services/` talks to
|
||||
/// (Android's lives in `MainActivity.kt`). Only the upload and download
|
||||
/// services are implemented so far; the rest still throw
|
||||
/// `MissingPluginException`, which the Dart side treats as "unavailable"
|
||||
/// (`lib/services/native_channel.dart`).
|
||||
enum NativeServices {
|
||||
private static let uploadStatus = UploadStatusStreamHandler()
|
||||
|
||||
static func register(messenger: FlutterBinaryMessenger) {
|
||||
FlutterMethodChannel(name: "dev.ayushya.noo/upload_service", binaryMessenger: messenger)
|
||||
.setMethodCallHandler { call, result in
|
||||
guard call.method == "startUpload" else { return result(FlutterMethodNotImplemented) }
|
||||
handle(call, result) { args in
|
||||
let files = try parseJSONList(args["files"])
|
||||
try TransferManager.shared.startUpload(
|
||||
serverUrl: try string(args, "serverUrl"),
|
||||
username: try string(args, "username"),
|
||||
authHeader: try string(args, "authHeader"),
|
||||
files: files.compactMap { entry in
|
||||
guard let uri = entry["uri"] as? String, let name = entry["name"] as? String else {
|
||||
return nil
|
||||
}
|
||||
return UploadFile(uri: uri, name: name)
|
||||
},
|
||||
remoteFolder: (args["remoteFolder"] as? String) ?? "/"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FlutterMethodChannel(name: "dev.ayushya.noo/download_service", binaryMessenger: messenger)
|
||||
.setMethodCallHandler { call, result in
|
||||
guard call.method == "startDownload" else { return result(FlutterMethodNotImplemented) }
|
||||
handle(call, result) { args in
|
||||
let items = try parseJSONList(args["files"])
|
||||
try TransferManager.shared.startDownload(
|
||||
serverUrl: try string(args, "serverUrl"),
|
||||
username: try string(args, "username"),
|
||||
authHeader: try string(args, "authHeader"),
|
||||
items: items.compactMap { entry in
|
||||
guard let path = entry["path"] as? String, let name = entry["name"] as? String else {
|
||||
return nil
|
||||
}
|
||||
return (path: path, name: name)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FlutterEventChannel(name: "dev.ayushya.noo/upload_service/status", binaryMessenger: messenger)
|
||||
.setStreamHandler(uploadStatus)
|
||||
TransferManager.shared.onUploadBatchFinished = { folder, succeeded, failed in
|
||||
uploadStatus.sink?(["remoteFolder": folder, "succeeded": succeeded, "failed": failed])
|
||||
}
|
||||
TransferManager.shared.reconnect()
|
||||
}
|
||||
|
||||
// MARK: - Argument helpers
|
||||
|
||||
/// Runs [work] with the call's argument map, turning a thrown error into
|
||||
/// the `FlutterError` Dart's `catch` shows the user.
|
||||
private static func handle(
|
||||
_ call: FlutterMethodCall,
|
||||
_ result: @escaping FlutterResult,
|
||||
work: ([String: Any]) throws -> Void
|
||||
) {
|
||||
guard let args = call.arguments as? [String: Any] else {
|
||||
return result(FlutterError(code: "bad_args", message: "Missing arguments.", details: nil))
|
||||
}
|
||||
do {
|
||||
try work(args)
|
||||
result(nil)
|
||||
} catch {
|
||||
result(FlutterError(code: "transfer_failed", message: error.localizedDescription, details: nil))
|
||||
}
|
||||
}
|
||||
|
||||
private static func string(_ args: [String: Any], _ key: String) throws -> String {
|
||||
guard let value = args[key] as? String, !value.isEmpty else {
|
||||
throw TransferError(message: "Missing \(key).")
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
/// The Dart services pass their file list as one JSON string.
|
||||
private static func parseJSONList(_ value: Any?) throws -> [[String: Any]] {
|
||||
guard let json = value as? String,
|
||||
let data = json.data(using: .utf8),
|
||||
let list = try JSONSerialization.jsonObject(with: data) as? [[String: Any]]
|
||||
else {
|
||||
throw TransferError(message: "Missing file list.")
|
||||
}
|
||||
return list
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user