• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

supabase / supabase-swift / 12886660635

21 Jan 2025 12:11PM UTC coverage: 54.731% (+6.4%) from 48.322%
12886660635

Pull #645

github

web-flow
Merge d1784a12d into 37a32aef8
Pull Request #645: test: integration tests revamp

5 of 80 new or added lines in 9 files covered. (6.25%)

43 existing lines in 3 files now uncovered.

3679 of 6722 relevant lines covered (54.73%)

11.47 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

34.22
/Sources/Storage/StorageFileApi.swift
1
import Foundation
2
import HTTPTypes
3
import Helpers
4

5
#if canImport(FoundationNetworking)
6
  import FoundationNetworking
7
#endif
8

9
let defaultSearchOptions = SearchOptions(
10
  limit: 100,
11
  offset: 0,
12
  sortBy: SortBy(
13
    column: "name",
14
    order: "asc"
15
  )
16
)
17

18
private let defaultFileOptions = FileOptions(
19
  cacheControl: "3600",
20
  contentType: "text/plain;charset=UTF-8",
21
  upsert: false
22
)
23

24
enum FileUpload {
25
  case data(Data)
26
  case url(URL)
27

28
  func encode(to formData: MultipartFormData, withPath path: String, options: FileOptions) {
2✔
29
    formData.append(
2✔
30
      options.cacheControl.data(using: .utf8)!,
2✔
31
      withName: "cacheControl"
2✔
32
    )
2✔
33

2✔
34
    if let metadata = options.metadata {
2✔
35
      formData.append(encodeMetadata(metadata), withName: "metadata")
2✔
36
    }
2✔
37

2✔
38
    switch self {
2✔
39
    case let .data(data):
2✔
40
      formData.append(
1✔
41
        data,
1✔
42
        withName: "",
1✔
43
        fileName: path.fileName,
1✔
44
        mimeType: options.contentType ?? mimeType(forPathExtension: path.pathExtension)
1✔
45
      )
1✔
46

2✔
47
    case let .url(url):
2✔
48
      formData.append(url, withName: "")
1✔
49
    }
2✔
50
  }
2✔
51
}
52

53
#if DEBUG
54
  import ConcurrencyExtras
55
  let testingBoundary = LockIsolated<String?>(nil)
1✔
56
#endif
57

58
/// Supabase Storage File API
59
public class StorageFileApi: StorageApi, @unchecked Sendable {
60
  /// The bucket id to operate on.
61
  let bucketId: String
62

63
  init(bucketId: String, configuration: StorageClientConfiguration) {
7✔
64
    self.bucketId = bucketId
7✔
65
    super.init(configuration: configuration)
7✔
66
  }
7✔
67

68
  private struct MoveResponse: Decodable {
69
    let message: String
70
  }
71

72
  private struct SignedURLResponse: Decodable {
73
    let signedURL: String
74
  }
75

76
  private func _uploadOrUpdate(
77
    method: HTTPTypes.HTTPRequest.Method,
78
    path: String,
79
    file: FileUpload,
80
    options: FileOptions?
81
  ) async throws -> FileUploadResponse {
2✔
82
    let options = options ?? defaultFileOptions
2✔
83
    var headers = options.headers.map { HTTPFields($0) } ?? HTTPFields()
2✔
84

2✔
85
    if method == .post {
2✔
86
      headers[.xUpsert] = "\(options.upsert)"
2✔
87
    }
2✔
88

2✔
89
    headers[.duplex] = options.duplex
2✔
90

2✔
91
    #if DEBUG
92
      let formData = MultipartFormData(boundary: testingBoundary.value)
2✔
93
    #else
94
      let formData = MultipartFormData()
95
    #endif
96
    file.encode(to: formData, withPath: path, options: options)
2✔
97

2✔
98
    struct UploadResponse: Decodable {
2✔
99
      let Key: String
2✔
100
      let Id: String
2✔
101
    }
2✔
102

2✔
103
    let cleanPath = _removeEmptyFolders(path)
2✔
104
    let _path = _getFinalPath(cleanPath)
2✔
105

2✔
106
    let response = try await execute(
2✔
107
      HTTPRequest(
2✔
108
        url: configuration.url.appendingPathComponent("object/\(_path)"),
2✔
109
        method: method,
2✔
110
        query: [],
2✔
111
        formData: formData,
2✔
112
        options: options,
2✔
113
        headers: headers
2✔
114
      )
2✔
115
    )
2✔
116
    .decoded(as: UploadResponse.self, decoder: configuration.decoder)
2✔
117

2✔
118
    return FileUploadResponse(
2✔
119
      id: response.Id,
2✔
120
      path: path,
2✔
121
      fullPath: response.Key
2✔
122
    )
2✔
123
  }
2✔
124

125
  /// Uploads a file to an existing bucket.
126
  /// - Parameters:
127
  ///   - path: The relative file path. Should be of the format `folder/subfolder/filename.png`. The bucket must already exist before attempting to upload.
128
  ///   - data: The Data to be stored in the bucket.
129
  ///   - options: The options for the uploaded file.
130
  @discardableResult
131
  public func upload(
132
    _ path: String,
133
    data: Data,
134
    options: FileOptions = FileOptions()
135
  ) async throws -> FileUploadResponse {
1✔
136
    try await _uploadOrUpdate(
1✔
137
      method: .post,
1✔
138
      path: path,
1✔
139
      file: .data(data),
1✔
140
      options: options
1✔
141
    )
1✔
142
  }
1✔
143

144
  /// Uploads a file to an existing bucket.
145
  /// - Parameters:
146
  ///   - path: The relative file path. Should be of the format `folder/subfolder/filename.png`. The bucket must already exist before attempting to upload.
147
  ///   - fileURL: The file URL to be stored in the bucket.
148
  ///   - options: The options for the uploaded file.
149
  @discardableResult
150
  public func upload(
151
    _ path: String,
152
    fileURL: URL,
153
    options: FileOptions = FileOptions()
154
  ) async throws -> FileUploadResponse {
1✔
155
    try await _uploadOrUpdate(
1✔
156
      method: .post,
1✔
157
      path: path,
1✔
158
      file: .url(fileURL),
1✔
159
      options: options
1✔
160
    )
1✔
161
  }
1✔
162

163
  /// Replaces an existing file at the specified path with a new one.
164
  /// - Parameters:
165
  ///   - path: The relative file path. Should be of the format `folder/subfolder`. The bucket already exist before attempting to upload.
166
  ///   - data: The Data to be stored in the bucket.
167
  ///   - options: The options for the updated file.
168
  @discardableResult
169
  public func update(
170
    _ path: String,
171
    data: Data,
172
    options: FileOptions = FileOptions()
173
  ) async throws -> FileUploadResponse {
×
174
    try await _uploadOrUpdate(
×
175
      method: .put,
×
176
      path: path,
×
177
      file: .data(data),
×
178
      options: options
×
179
    )
×
180
  }
×
181

182
  /// Replaces an existing file at the specified path with a new one.
183
  /// - Parameters:
184
  ///   - path: The relative file path. Should be of the format `folder/subfolder`. The bucket already exist before attempting to upload.
185
  ///   - fileURL: The file URL to be stored in the bucket.
186
  ///   - options: The options for the updated file.
187
  @discardableResult
188
  public func update(
189
    _ path: String,
190
    fileURL: URL,
191
    options: FileOptions = FileOptions()
192
  ) async throws -> FileUploadResponse {
×
193
    try await _uploadOrUpdate(
×
194
      method: .put,
×
195
      path: path,
×
196
      file: .url(fileURL),
×
197
      options: options
×
198
    )
×
199
  }
×
200

201
  /// Moves an existing file to a new path.
202
  /// - Parameters:
203
  ///   - source: The original file path, including the current file name. For example `folder/image.png`.
204
  ///   - destination: The new file path, including the new file name. For example `folder/image-new.png`.
205
  ///   - options: The destination options.
206
  public func move(
207
    from source: String,
208
    to destination: String,
209
    options: DestinationOptions? = nil
210
  ) async throws {
×
211
    try await execute(
×
212
      HTTPRequest(
×
213
        url: configuration.url.appendingPathComponent("object/move"),
×
214
        method: .post,
×
215
        body: configuration.encoder.encode(
×
216
          [
×
217
            "bucketId": bucketId,
×
218
            "sourceKey": source,
×
219
            "destinationKey": destination,
×
220
            "destinationBucket": options?.destinationBucket,
×
221
          ]
×
222
        )
×
223
      )
×
224
    )
×
225
  }
×
226

227
  /// Copies an existing file to a new path.
228
  /// - Parameters:
229
  ///   - source: The original file path, including the current file name. For example `folder/image.png`.
230
  ///   - destination: The new file path, including the new file name. For example `folder/image-copy.png`.
231
  ///   - options: The destination options.
232
  @discardableResult
233
  public func copy(
234
    from source: String,
235
    to destination: String,
236
    options: DestinationOptions? = nil
237
  ) async throws -> String {
×
238
    struct UploadResponse: Decodable {
×
239
      let Key: String
×
240
    }
×
241

×
242
    return try await execute(
×
243
      HTTPRequest(
×
244
        url: configuration.url.appendingPathComponent("object/copy"),
×
245
        method: .post,
×
246
        body: configuration.encoder.encode(
×
247
          [
×
248
            "bucketId": bucketId,
×
249
            "sourceKey": source,
×
250
            "destinationKey": destination,
×
251
            "destinationBucket": options?.destinationBucket,
×
252
          ]
×
253
        )
×
254
      )
×
255
    )
×
256
    .decoded(as: UploadResponse.self, decoder: configuration.decoder)
×
257
    .Key
×
258
  }
×
259

260
  /// Creates a signed URL. Use a signed URL to share a file for a fixed amount of time.
261
  /// - Parameters:
262
  ///   - path: The file path, including the current file name. For example `folder/image.png`.
263
  ///   - expiresIn: The number of seconds until the signed URL expires. For example, `60` for a URL which is valid for one minute.
264
  ///   - download: Trigger a download with the specified file name.
265
  ///   - transform: Transform the asset before serving it to the client.
266
  public func createSignedURL(
267
    path: String,
268
    expiresIn: Int,
269
    download: String? = nil,
270
    transform: TransformOptions? = nil
271
  ) async throws -> URL {
×
272
    struct Body: Encodable {
×
273
      let expiresIn: Int
×
274
      let transform: TransformOptions?
×
275
    }
×
276

×
NEW
277
    let encoder = JSONEncoder.unconfiguredEncoder
×
278

×
279
    let response = try await execute(
×
280
      HTTPRequest(
×
281
        url: configuration.url.appendingPathComponent("object/sign/\(bucketId)/\(path)"),
×
282
        method: .post,
×
283
        body: encoder.encode(
×
284
          Body(expiresIn: expiresIn, transform: transform)
×
285
        )
×
286
      )
×
287
    )
×
288
    .decoded(as: SignedURLResponse.self, decoder: configuration.decoder)
×
289

×
290
    return try makeSignedURL(response.signedURL, download: download)
×
291
  }
×
292

293
  /// Creates a signed URL. Use a signed URL to share a file for a fixed amount of time.
294
  /// - Parameters:
295
  ///   - path: The file path, including the current file name. For example `folder/image.png`.
296
  ///   - expiresIn: The number of seconds until the signed URL expires. For example, `60` for a URL which is valid for one minute.
297
  ///   - download: Trigger a download with the default file name.
298
  ///   - transform: Transform the asset before serving it to the client.
299
  public func createSignedURL(
300
    path: String,
301
    expiresIn: Int,
302
    download: Bool,
303
    transform: TransformOptions? = nil
304
  ) async throws -> URL {
×
305
    try await createSignedURL(
×
306
      path: path,
×
307
      expiresIn: expiresIn,
×
308
      download: download ? "" : nil,
×
309
      transform: transform
×
310
    )
×
311
  }
×
312

313
  /// Creates multiple signed URLs. Use a signed URL to share a file for a fixed amount of time.
314
  /// - Parameters:
315
  ///   - paths: The file paths to be downloaded, including the current file names. For example `["folder/image.png", "folder2/image2.png"]`.
316
  ///   - expiresIn: The number of seconds until the signed URLs expire. For example, `60` for URLs which are valid for one minute.
317
  ///   - download: Trigger a download with the specified file name.
318
  public func createSignedURLs(
319
    paths: [String],
320
    expiresIn: Int,
321
    download: String? = nil
322
  ) async throws -> [URL] {
1✔
323
    struct Params: Encodable {
1✔
324
      let expiresIn: Int
1✔
325
      let paths: [String]
1✔
326
    }
1✔
327

1✔
328
    let encoder = JSONEncoder.unconfiguredEncoder
1✔
329

1✔
330
    let response = try await execute(
1✔
331
      HTTPRequest(
1✔
332
        url: configuration.url.appendingPathComponent("object/sign/\(bucketId)"),
1✔
333
        method: .post,
1✔
334
        body: encoder.encode(
1✔
335
          Params(expiresIn: expiresIn, paths: paths)
1✔
336
        )
1✔
337
      )
1✔
338
    )
1✔
339
    .decoded(as: [SignedURLResponse].self, decoder: configuration.decoder)
1✔
340

1✔
341
    return try response.map { try makeSignedURL($0.signedURL, download: download) }
2✔
342
  }
1✔
343

344
  /// Creates multiple signed URLs. Use a signed URL to share a file for a fixed amount of time.
345
  /// - Parameters:
346
  ///   - paths: The file paths to be downloaded, including the current file names. For example `["folder/image.png", "folder2/image2.png"]`.
347
  ///   - expiresIn: The number of seconds until the signed URLs expire. For example, `60` for URLs which are valid for one minute.
348
  ///   - download: Trigger a download with the default file name.
349
  public func createSignedURLs(
350
    paths: [String],
351
    expiresIn: Int,
352
    download: Bool
353
  ) async throws -> [URL] {
×
354
    try await createSignedURLs(paths: paths, expiresIn: expiresIn, download: download ? "" : nil)
×
355
  }
×
356

NEW
357
  private func makeSignedURL(_ signedURL: String, download: String?) throws -> URL {
×
NEW
358
    guard let signedURLComponents = URLComponents(string: signedURL),
×
NEW
359
      var baseComponents = URLComponents(
×
NEW
360
        url: configuration.url, resolvingAgainstBaseURL: false)
×
361
    else {
×
362
      throw URLError(.badURL)
×
UNCOV
363
    }
×
UNCOV
364

×
NEW
365
    baseComponents.path +=
×
NEW
366
      signedURLComponents.path.hasPrefix("/")
×
NEW
367
      ? signedURLComponents.path : "/\(signedURLComponents.path)"
×
NEW
368
    baseComponents.queryItems = signedURLComponents.queryItems
×
UNCOV
369

×
UNCOV
370
    if let download {
×
NEW
371
      baseComponents.queryItems = baseComponents.queryItems ?? []
×
NEW
372
      baseComponents.queryItems!.append(URLQueryItem(name: "download", value: download))
×
UNCOV
373
    }
×
UNCOV
374

×
NEW
375
    guard let signedURL = baseComponents.url else {
×
376
      throw URLError(.badURL)
×
UNCOV
377
    }
×
UNCOV
378

×
UNCOV
379
    return signedURL
×
UNCOV
380
  }
×
381

382
  /// Deletes files within the same bucket
383
  /// - Parameters:
384
  ///   - paths: An array of files to be deletes, including the path and file name. For example [`folder/image.png`].
385
  /// - Returns: A list of removed ``FileObject``.
386
  public func remove(paths: [String]) async throws -> [FileObject] {
×
387
    try await execute(
×
388
      HTTPRequest(
×
389
        url: configuration.url.appendingPathComponent("object/\(bucketId)"),
×
390
        method: .delete,
×
391
        body: configuration.encoder.encode(["prefixes": paths])
×
392
      )
×
393
    )
×
394
    .decoded(decoder: configuration.decoder)
×
395
  }
×
396

397
  /// Lists all the files within a bucket.
398
  /// - Parameters:
399
  ///   - path: The folder path.
400
  ///   - options: Search options, including `limit`, `offset`, and `sortBy`.
401
  public func list(
402
    path: String? = nil,
403
    options: SearchOptions? = nil
404
  ) async throws -> [FileObject] {
×
NEW
405
    let encoder = JSONEncoder.unconfiguredEncoder
×
406

×
407
    var options = options ?? defaultSearchOptions
×
408
    options.prefix = path ?? ""
×
409

×
410
    return try await execute(
×
411
      HTTPRequest(
×
412
        url: configuration.url.appendingPathComponent("object/list/\(bucketId)"),
×
413
        method: .post,
×
414
        body: encoder.encode(options)
×
415
      )
×
416
    )
×
417
    .decoded(decoder: configuration.decoder)
×
418
  }
×
419

420
  /// Downloads a file from a private bucket. For public buckets, make a request to the URL returned
421
  /// from ``StorageFileApi/getPublicURL(path:download:fileName:options:)`` instead.
422
  /// - Parameters:
423
  ///   - path: The file path to be downloaded, including the path and file name. For example `folder/image.png`.
424
  ///   - options: Transform the asset before serving it to the client.
425
  @discardableResult
426
  public func download(
427
    path: String,
428
    options: TransformOptions? = nil
429
  ) async throws -> Data {
×
430
    let queryItems = options?.queryItems ?? []
×
431
    let renderPath = options != nil ? "render/image/authenticated" : "object"
×
432
    let _path = _getFinalPath(path)
×
433

×
434
    return try await execute(
×
435
      HTTPRequest(
×
436
        url: configuration.url
×
437
          .appendingPathComponent("\(renderPath)/\(_path)"),
×
438
        method: .get,
×
439
        query: queryItems
×
440
      )
×
441
    )
×
442
    .data
×
443
  }
×
444

445
  /// Retrieves the details of an existing file.
446
  public func info(path: String) async throws -> FileObjectV2 {
×
447
    let _path = _getFinalPath(path)
×
448

×
449
    return try await execute(
×
450
      HTTPRequest(
×
451
        url: configuration.url.appendingPathComponent("object/info/\(_path)"),
×
452
        method: .get
×
453
      )
×
454
    )
×
455
    .decoded(decoder: configuration.decoder)
×
456
  }
×
457

458
  /// Checks the existence of file.
459
  public func exists(path: String) async throws -> Bool {
×
460
    do {
×
461
      try await execute(
×
462
        HTTPRequest(
×
463
          url: configuration.url.appendingPathComponent("object/\(bucketId)/\(path)"),
×
464
          method: .head
×
465
        )
×
466
      )
×
467
      return true
×
468
    } catch {
×
469
      var statusCode: Int?
×
470

×
471
      if let error = error as? StorageError {
×
472
        statusCode = error.statusCode.flatMap(Int.init)
×
473
      } else if let error = error as? HTTPError {
×
474
        statusCode = error.response.statusCode
×
475
      }
×
476

×
477
      if let statusCode, [400, 404].contains(statusCode) {
×
478
        return false
×
479
      }
×
480

×
481
      throw error
×
482
    }
×
483
  }
×
484

485
  /// A simple convenience function to get the URL for an asset in a public bucket. If you do not want to use this function, you can construct the public URL by concatenating the bucket URL with the path to the asset. This function does not verify if the bucket is public. If a public URL is created for a bucket which is not public, you will not be able to download the asset.
486
  /// - Parameters:
487
  ///  - path: The path and name of the file to generate the public URL for. For example `folder/image.png`.
488
  ///  - download: Trigger a download with the specified file name.
489
  ///  - options: Transform the asset before retrieving it on the client.
490
  ///
491
  ///  - Note: The bucket needs to be set to public, either via ``StorageBucketApi/updateBucket(_:options:)`` or by going to Storage on [supabase.com/dashboard](https://supabase.com/dashboard), clicking the overflow menu on a bucket and choosing "Make public".
492
  public func getPublicURL(
493
    path: String,
494
    download: String? = nil,
495
    options: TransformOptions? = nil
496
  ) throws -> URL {
4✔
497
    var queryItems: [URLQueryItem] = []
4✔
498

4✔
499
    guard var components = URLComponents(url: configuration.url, resolvingAgainstBaseURL: true)
4✔
500
    else {
4✔
501
      throw URLError(.badURL)
×
502
    }
4✔
503

4✔
504
    if let download {
4✔
505
      queryItems.append(URLQueryItem(name: "download", value: download))
3✔
506
    }
3✔
507

4✔
508
    if let optionsQueryItems = options?.queryItems {
4✔
509
      queryItems.append(contentsOf: optionsQueryItems)
1✔
510
    }
1✔
511

4✔
512
    let renderPath = options != nil ? "render/image" : "object"
4✔
513

4✔
514
    components.path += "/\(renderPath)/public/\(bucketId)/\(path)"
4✔
515
    components.queryItems = !queryItems.isEmpty ? queryItems : nil
4✔
516

4✔
517
    guard let generatedUrl = components.url else {
4✔
518
      throw URLError(.badURL)
×
519
    }
4✔
520

4✔
521
    return generatedUrl
4✔
522
  }
4✔
523

524
  /// A simple convenience function to get the URL for an asset in a public bucket. If you do not want to use this function, you can construct the public URL by concatenating the bucket URL with the path to the asset. This function does not verify if the bucket is public. If a public URL is created for a bucket which is not public, you will not be able to download the asset.
525
  /// - Parameters:
526
  ///  - path: The path and name of the file to generate the public URL for. For example `folder/image.png`.
527
  ///  - download: Trigger a download with the default file name.
528
  ///  - options: Transform the asset before retrieving it on the client.
529
  ///
530
  ///  - Note: The bucket needs to be set to public, either via ``StorageBucketApi/updateBucket(_:options:)`` or by going to Storage on [supabase.com/dashboard](https://supabase.com/dashboard), clicking the overflow menu on a bucket and choosing "Make public".
531
  public func getPublicURL(
532
    path: String,
533
    download: Bool,
534
    options: TransformOptions? = nil
535
  ) throws -> URL {
1✔
536
    try getPublicURL(path: path, download: download ? "" : nil, options: options)
1✔
537
  }
1✔
538

539
  /// Creates a signed upload URL. Signed upload URLs can be used to upload files to the bucket without further authentication. They are valid for 2 hours.
540
  /// - Parameter path: The file path, including the current file name. For example `folder/image.png`.
541
  /// - Returns: A URL that can be used to upload files to the bucket without further
542
  /// authentication.
543
  public func createSignedUploadURL(
544
    path: String,
545
    options: CreateSignedUploadURLOptions? = nil
546
  ) async throws -> SignedUploadURL {
×
547
    struct Response: Decodable {
×
NEW
548
      let url: String
×
549
    }
×
550

×
551
    var headers = HTTPFields()
×
552
    if let upsert = options?.upsert, upsert {
×
553
      headers[.xUpsert] = "true"
×
554
    }
×
555

×
556
    let response = try await execute(
×
557
      HTTPRequest(
×
558
        url: configuration.url.appendingPathComponent("object/upload/sign/\(bucketId)/\(path)"),
×
559
        method: .post,
×
560
        headers: headers
×
561
      )
×
562
    )
×
563
    .decoded(as: Response.self, decoder: configuration.decoder)
×
564

×
565
    let signedURL = try makeSignedURL(response.url, download: nil)
×
566

×
567
    guard let components = URLComponents(url: signedURL, resolvingAgainstBaseURL: false) else {
×
568
      throw URLError(.badURL)
×
569
    }
×
570

×
571
    guard let token = components.queryItems?.first(where: { $0.name == "token" })?.value else {
×
572
      throw StorageError(statusCode: nil, message: "No token returned by API", error: nil)
×
573
    }
×
574

×
575
    guard let url = components.url else {
×
576
      throw URLError(.badURL)
×
577
    }
×
578

×
579
    return SignedUploadURL(
×
580
      signedURL: url,
×
581
      path: path,
×
582
      token: token
×
583
    )
×
584
  }
×
585

586
  /// Upload a file with a token generated from ``StorageFileApi/createSignedUploadURL(path:)``.
587
  /// - Parameters:
588
  ///   - path: The file path, including the file name. Should be of the format `folder/subfolder/filename.png`. The bucket must already exist before attempting to upload.
589
  ///   - token: The token generated from ``StorageFileApi/createSignedUploadURL(path:)``.
590
  ///   - data: The Data to be stored in the bucket.
591
  ///   - options: HTTP headers, for example `cacheControl`.
592
  /// - Returns: A key pointing to stored location.
593
  @discardableResult
594
  public func uploadToSignedURL(
595
    _ path: String,
596
    token: String,
597
    data: Data,
598
    options: FileOptions? = nil
599
  ) async throws -> SignedURLUploadResponse {
×
600
    try await _uploadToSignedURL(
×
601
      path: path,
×
602
      token: token,
×
603
      file: .data(data),
×
604
      options: options
×
605
    )
×
606
  }
×
607

608
  /// Upload a file with a token generated from ``StorageFileApi/createSignedUploadURL(path:)``.
609
  /// - Parameters:
610
  ///   - path: The file path, including the file name. Should be of the format `folder/subfolder/filename.png`. The bucket must already exist before attempting to upload.
611
  ///   - token: The token generated from ``StorageFileApi/createSignedUploadURL(path:)``.
612
  ///   - fileURL: The file URL to be stored in the bucket.
613
  ///   - options: HTTP headers, for example `cacheControl`.
614
  /// - Returns: A key pointing to stored location.
615
  @discardableResult
616
  public func uploadToSignedURL(
617
    _ path: String,
618
    token: String,
619
    fileURL: URL,
620
    options: FileOptions? = nil
621
  ) async throws -> SignedURLUploadResponse {
×
622
    try await _uploadToSignedURL(
×
623
      path: path,
×
624
      token: token,
×
625
      file: .url(fileURL),
×
626
      options: options
×
627
    )
×
628
  }
×
629

630
  private func _uploadToSignedURL(
631
    path: String,
632
    token: String,
633
    file: FileUpload,
634
    options: FileOptions?
635
  ) async throws -> SignedURLUploadResponse {
×
636
    let options = options ?? defaultFileOptions
×
637
    var headers = options.headers.map { HTTPFields($0) } ?? HTTPFields()
×
638

×
639
    headers[.xUpsert] = "\(options.upsert)"
×
640
    headers[.duplex] = options.duplex
×
641

×
642
    let formData = MultipartFormData()
×
643
    file.encode(to: formData, withPath: path, options: options)
×
644

×
645
    struct UploadResponse: Decodable {
×
646
      let Key: String
×
647
    }
×
648

×
649
    let fullPath = try await execute(
×
650
      HTTPRequest(
×
651
        url: configuration.url
×
652
          .appendingPathComponent("object/upload/sign/\(bucketId)/\(path)"),
×
653
        method: .put,
×
654
        query: [URLQueryItem(name: "token", value: token)],
×
655
        formData: formData,
×
656
        options: options,
×
657
        headers: headers
×
658
      )
×
659
    )
×
660
    .decoded(as: UploadResponse.self, decoder: configuration.decoder)
×
661
    .Key
×
662

×
663
    return SignedURLUploadResponse(path: path, fullPath: fullPath)
×
664
  }
×
665

666
  private func _getFinalPath(_ path: String) -> String {
2✔
667
    "\(bucketId)/\(path)"
2✔
668
  }
2✔
669

670
  private func _removeEmptyFolders(_ path: String) -> String {
2✔
671
    let trimmedPath = path.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
2✔
672
    let cleanedPath = trimmedPath.replacingOccurrences(
2✔
673
      of: "/+", with: "/", options: .regularExpression
2✔
674
    )
2✔
675
    return cleanedPath
2✔
676
  }
2✔
677
}
678

679
extension HTTPField.Name {
680
  static let duplex = Self("duplex")!
681
  static let xUpsert = Self("x-upsert")!
682
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc