@@ -64,16 +64,17 @@ public class FormModel: ObservableObject, CustomDebugStringConvertible {
6464 var submitAction : ( ( ) -> ( ) ) ?
6565
6666 /// The form data for this form.
67- @Published @ _spi ( LiveForm ) public private( set) var data = [ String : any FormValue ] ( )
67+ @Published internal private( set) var data = [ String : any FormValue ] ( )
6868 var formFieldWillChange = PassthroughSubject < String , Never > ( )
6969
7070 /// A publisher that emits a value before sending the form submission event.
7171 var formWillSubmit = PassthroughSubject < ( ) , Never > ( )
7272
73- @ _spi ( LiveForm ) public internal ( set ) var fileUploads : [ FileUpload ] = [ ]
73+ var fileUploads : [ FileUpload ] = [ ]
7474 public struct FileUpload : Identifiable {
7575 public let id : String
7676 public let data : Data
77+ public let ref : Int
7778 let upload : ( ) async throws -> ( )
7879 }
7980
@@ -267,13 +268,13 @@ public class FormModel: ObservableObject, CustomDebugStringConvertible {
267268 )
268269 }
269270
270- public func queueFileUpload(
271+ public func queueFileUpload< R : RootRegistry > (
271272 name: String ,
272273 id: String ,
273274 contents: Data ,
274275 fileType: UTType ,
275276 fileName: String ,
276- coordinator: LiveViewCoordinator < some RootRegistry >
277+ coordinator: LiveViewCoordinator < R >
277278 ) async throws {
278279 guard let liveChannel = coordinator. liveChannel
279280 else { return }
@@ -285,6 +286,19 @@ public class FormModel: ObservableObject, CustomDebugStringConvertible {
285286 " " ,
286287 id
287288 )
289+
290+ let ref = coordinator. nextUploadRef ( )
291+
292+ let fileMetadata = Json . object ( object: [
293+ " path " : . str( string: name) ,
294+ " ref " : . str( string: " \( ref) " ) ,
295+ " last_modified " : . numb( number: . posInt( pos: UInt64 ( Date ( ) . timeIntervalSince1970 * 1000 ) ) ) , // in milliseconds
296+ " name " : . str( string: fileName) ,
297+ " relative_path " : . str( string: " " ) ,
298+ " type " : . str( string: fileType. preferredMIMEType!) ,
299+ " size " : . numb( number: . posInt( pos: UInt64 ( contents. count) ) )
300+ ] )
301+
288302 if let changeEventName {
289303 let replyPayload = try await coordinator. liveChannel!. channel ( ) . call (
290304 event: . user( user: " event " ) ,
@@ -294,28 +308,103 @@ public class FormModel: ObservableObject, CustomDebugStringConvertible {
294308 " value " : . str( string: " _target= \( name) " ) ,
295309 " uploads " : . object( object: [
296310 id: . array( array: [
297- . object( object: [
298- " path " : . str( string: fileName) ,
299- " ref " : . str( string: String ( coordinator. nextUploadRef ( ) ) ) ,
300- " last_modified " : . numb( number: . posInt( pos: UInt64 ( Date ( ) . timeIntervalSince1970 * 1000 ) ) ) , // in milliseconds
301- " name " : . str( string: fileName) ,
302- " relative_path " : . str( string: " " ) ,
303- " type " : . str( string: fileType. preferredMIMEType!) ,
304- " size " : . numb( number: . posInt( pos: UInt64 ( contents. count) ) )
305- ] )
311+ fileMetadata
306312 ] )
307313 ] )
308314 ] ) ) ,
309315 timeout: 10_000
310316 )
311317 try await coordinator. handleEventReplyPayload ( replyPayload)
312318 }
313- self . fileUploads. append ( . init (
319+ self . fileUploads. append ( FileUpload (
314320 id: id,
315321 data: contents,
316- upload: { try await liveChannel. uploadFile ( file) }
322+ ref: ref,
323+ upload: {
324+ do {
325+ let entries = Json . array ( array: [
326+ fileMetadata
327+ ] )
328+
329+ let payload = LiveViewNativeCore . Payload. jsonPayload ( json: . object( object: [
330+ " ref " : . str( string: id) ,
331+ " entries " : entries,
332+ ] ) )
333+
334+ print ( " sending preflight request \( ref) " )
335+
336+ let response = try await coordinator. liveChannel!. channel ( ) . call (
337+ event: . user( user: " allow_upload " ) ,
338+ payload: payload,
339+ timeout: 10_000
340+ )
341+
342+ try await coordinator. handleEventReplyPayload ( response)
343+
344+ print ( " got preflight response \( response) " )
345+
346+ // LiveUploader.initAdapterUpload
347+ // UploadEntry.uploader
348+ // utils.channelUploader
349+ // EntryUploader
350+ let reply = switch response {
351+ case let . jsonPayload( json: json) :
352+ json
353+ default :
354+ fatalError ( )
355+ }
356+ print ( reply)
357+
358+ let allowUploadReply = try JsonDecoder ( ) . decode ( AllowUploadReply . self, from: reply)
359+
360+ let entry : Json = switch reply {
361+ case let . object( object: object) :
362+ switch object [ " entries " ] {
363+ case let . object( object: object) :
364+ object [ " \( ref) " ] !
365+ default :
366+ fatalError ( )
367+ }
368+ default :
369+ fatalError ( )
370+ }
371+
372+
373+ let uploadEntry = UploadEntry < R > ( data: contents, ref: allowUploadReply. ref, entryRef: ref, meta: entry, config: allowUploadReply. config, coordinator: coordinator)
374+ switch entry {
375+ case let . object( object: meta) :
376+ switch meta [ " uploader " ] ! {
377+ case let . str( string: uploader) :
378+ try await coordinator. session. configuration. uploaders [ uploader] !. upload ( uploadEntry, for: coordinator)
379+ default :
380+ fatalError ( )
381+ }
382+ case let . str( string: uploadToken) :
383+ try await UploadEntry < R > . ChannelUploader ( ) . upload ( uploadEntry, for: coordinator)
384+ default :
385+ fatalError ( )
386+ }
387+
388+ print ( " done " )
389+ } catch {
390+ fatalError ( error. localizedDescription)
391+ }
392+ }
317393 ) )
318394 }
395+
396+ public struct UploadConfig : Codable {
397+ public let chunk_size : Int
398+ public let max_entries : Int
399+ public let chunk_timeout : Int
400+ public let max_file_size : Int
401+ }
402+
403+ fileprivate struct AllowUploadReply : Codable {
404+ let ref : String
405+ let config : UploadConfig
406+ // let entries: [String:String]
407+ }
319408}
320409
321410private extension URLComponents {
@@ -330,3 +419,82 @@ private extension URLComponents {
330419 return components. query!
331420 }
332421}
422+
423+ public final class UploadEntry < R: RootRegistry > {
424+ public let data : Data
425+ public let ref : String
426+ public let entryRef : Int
427+ public let meta : Json
428+ public let config : FormModel . UploadConfig
429+ private weak var coordinator : LiveViewCoordinator < R > ?
430+
431+ init ( data: Data , ref: String , entryRef: Int , meta: Json , config: FormModel . UploadConfig , coordinator: LiveViewCoordinator < R > ) {
432+ self . data = data
433+ self . ref = ref
434+ self . entryRef = entryRef
435+ self . meta = meta
436+ self . config = config
437+ self . coordinator = coordinator
438+ }
439+
440+ @MainActor
441+ public func progress( _ progress: Int ) async throws {
442+ let progressReply = try await coordinator!. liveChannel!. channel ( ) . call (
443+ event: . user( user: " progress " ) ,
444+ payload: . jsonPayload( json: . object( object: [
445+ " event " : . null,
446+ " ref " : . str( string: ref) ,
447+ " entry_ref " : . str( string: " \( entryRef) " ) ,
448+ " progress " : . numb( number: . posInt( pos: UInt64 ( progress) ) ) ,
449+ ] ) ) ,
450+ timeout: 10_000
451+ )
452+ print ( progressReply)
453+ _ = try await coordinator!. handleEventReplyPayload ( progressReply)
454+ }
455+
456+ @MainActor
457+ public func error( _ error: some Error ) async throws {
458+
459+ }
460+
461+ @MainActor
462+ public func pause( ) async throws {
463+
464+ }
465+
466+ public struct ChannelUploader : Uploader {
467+ public init ( ) { }
468+
469+ public func upload< Root: RootRegistry > (
470+ _ entry: UploadEntry < Root > ,
471+ for coordinator: LiveViewCoordinator < Root >
472+ ) async throws {
473+ let uploadChannel = try await coordinator. session. liveSocket!. socket ( ) . channel ( topic: . fromString( topic: " lvu: \( entry. entryRef) " ) , payload: . jsonPayload( json: . object( object: [
474+ " token " : entry. meta
475+ ] ) ) )
476+ _ = try await uploadChannel. join ( timeout: 10_000 )
477+
478+ let stream = InputStream ( data: entry. data)
479+ var buf = [ UInt8] ( repeating: 0 , count: entry. config. chunk_size)
480+ stream. open ( )
481+ var amountRead = 0
482+ while case let amount = stream. read ( & buf, maxLength: entry. config. chunk_size) , amount > 0 {
483+ let resp = try await uploadChannel. call ( event: . user( user: " chunk " ) , payload: . binary( bytes: Data ( buf [ ..< amount] ) ) , timeout: 10_000 )
484+ print ( " uploaded chunk: \( resp) " )
485+ amountRead += amount
486+
487+ try await entry. progress ( Int ( ( Double ( amountRead) / Double( entry. data. count) ) * 100 ) )
488+ }
489+ stream. close ( )
490+
491+ print ( " finished uploading chunks " )
492+ try await entry. progress ( 100 )
493+ }
494+ }
495+ }
496+
497+ public protocol Uploader {
498+ @MainActor
499+ func upload< R: RootRegistry > ( _ entry: UploadEntry < R > , for coordinator: LiveViewCoordinator < R > ) async throws
500+ }
0 commit comments