@@ -376,6 +376,92 @@ public class StorageFileApi: StorageApi {
376
376
) throws -> URL {
377
377
try getPublicURL ( path: path, download: download ? " " : nil , options: options)
378
378
}
379
+
380
+ /// Creates a signed upload URL.
381
+ /// - Parameter path: The file path, including the current file name. For example
382
+ /// `folder/image.png`.
383
+ /// - Returns: A URL that can be used to upload files to the bucket without further
384
+ /// authentication.
385
+ ///
386
+ /// - Note: Signed upload URLs can be used to upload files to the bucket without further
387
+ /// authentication. They are valid for 2 hours.
388
+ public func createSignedUploadURL( path: String ) async throws -> SignedUploadURL {
389
+ struct Response : Decodable {
390
+ let url : URL
391
+ }
392
+
393
+ let response = try await execute (
394
+ Request ( path: " /object/upload/sign/ \( bucketId) / \( path) " , method: . post)
395
+ )
396
+ . decoded ( as: Response . self, decoder: configuration. decoder)
397
+
398
+ let signedURL = try makeSignedURL ( response. url, download: nil )
399
+
400
+ guard let components = URLComponents ( url: signedURL, resolvingAgainstBaseURL: false ) else {
401
+ throw URLError ( . badURL)
402
+ }
403
+
404
+ guard let token = components. queryItems? . first ( where: { $0. name == " token " } ) ? . value else {
405
+ throw StorageError ( statusCode: nil , message: " No token returned by API " , error: nil )
406
+ }
407
+
408
+ guard let url = components. url else {
409
+ throw URLError ( . badURL)
410
+ }
411
+
412
+ return SignedUploadURL (
413
+ signedURL: url,
414
+ path: path,
415
+ token: token
416
+ )
417
+ }
418
+
419
+ /// Upload a file with a token generated from ``StorageFileApi/createSignedUploadURL(path:)``.
420
+ /// - Parameters:
421
+ /// - path: The file path, including the file name. Should be of the format
422
+ /// `folder/subfolder/filename.png`. The bucket must already exist before attempting to upload.
423
+ /// - token: The token generated from ``StorageFileApi/createSignedUploadURL(path:)``.
424
+ /// - file: The Data to be stored in the bucket.
425
+ /// - options: HTTP headers, for example `cacheControl`.
426
+ /// - Returns: A key pointing to stored location.
427
+ @discardableResult
428
+ public func uploadToSignedURL(
429
+ path: String ,
430
+ token: String ,
431
+ file: Data ,
432
+ options: FileOptions = FileOptions ( )
433
+ ) async throws -> String {
434
+ let contentType = options. contentType
435
+ var headers = [
436
+ " x-upsert " : " \( options. upsert) " ,
437
+ ]
438
+ headers [ " duplex " ] = options. duplex
439
+
440
+ let fileName = fileName ( fromPath: path)
441
+
442
+ let form = FormData ( )
443
+ form. append ( file: File (
444
+ name: fileName,
445
+ data: file,
446
+ fileName: fileName,
447
+ contentType: contentType
448
+ ) )
449
+
450
+ return try await execute (
451
+ Request (
452
+ path: " /object/upload/sign/ \( bucketId) / \( path) " ,
453
+ method: . put,
454
+ query: [
455
+ URLQueryItem ( name: " token " , value: token) ,
456
+ ] ,
457
+ formData: form,
458
+ options: options,
459
+ headers: headers
460
+ )
461
+ )
462
+ . decoded ( as: UploadResponse . self, decoder: configuration. decoder)
463
+ . Key
464
+ }
379
465
}
380
466
381
467
private func fileName( fromPath path: String ) -> String {
0 commit comments