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

supabase / supabase-swift / 28167379888

25 Jun 2026 11:38AM UTC coverage: 80.743%. First build
28167379888

Pull #987

github

web-flow
Merge 693c0012d into c08ceb649
Pull Request #987: feat(storage)!: storage v3

1784 of 2046 new or added lines in 14 files covered. (87.19%)

8264 of 10235 relevant lines covered (80.74%)

34.21 hits per line

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

86.25
/Sources/Storage/StorageClient.swift
1
import Foundation
2
import Helpers
3

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

8
/// Configuration for ``StorageClient``.
9
///
10
/// Pass an instance to ``StorageClient/init(url:configuration:)`` to customise the HTTP layer,
11
/// request headers, and optional diagnostics logging.
12
///
13
/// ## Example
14
///
15
/// ```swift
16
/// let config = StorageClientConfiguration(
17
///   headers: [
18
///     "apikey": "<anon-key>",
19
///     "Authorization": "Bearer <access-token>"
20
///   ],
21
///   logger: myLogger
22
/// )
23
/// let storage = StorageClient(
24
///   url: URL(string: "https://<project-ref>.supabase.co/storage/v1")!,
25
///   configuration: config
26
/// )
27
/// ```
28
public struct StorageClientConfiguration: Sendable {
29
  /// HTTP headers included in every request made by the client.
30
  ///
31
  /// An `X-Client-Info` header is appended automatically if not already present. The
32
  /// `Authorization` header set here is used unless a `TokenProvider` is configured via the
33
  /// package-level initialiser, in which case it is refreshed automatically.
34
  public var headers: [String: String]
35

36
  /// The `URLSession` used for all HTTP requests.
37
  ///
38
  /// Defaults to a session created with `.default` configuration.
39
  public let session: URLSession
40

41
  /// An optional logger that receives verbose request and response diagnostics.
42
  ///
43
  /// Implement `SupabaseLogger` and supply it here to observe all storage HTTP traffic.
44
  /// Pass `nil` (the default) to disable logging.
45
  public let logger: (any SupabaseLogger)?
46

47
  /// When `true`, rewrites legacy Supabase hostnames to use the dedicated storage subdomain,
48
  /// which disables request buffering and enables uploads larger than 50 GB.
49
  ///
50
  /// `<project-ref>.supabase.co` is rewritten to `<project-ref>.storage.supabase.co`.
51
  /// Defaults to `false`.
52
  public let useNewHostname: Bool
53

54
  /// When set, downloads use `URLSessionConfiguration.background(withIdentifier:)`,
55
  /// allowing transfers to continue while the app is suspended.
56
  ///
57
  /// Requires wiring `handleBackgroundEvents(forSessionIdentifier:completionHandler:)` in
58
  /// your `AppDelegate`.
59
  ///
60
  /// When `nil` (the default), a standard foreground session is used.
61
  public let backgroundDownloadSessionIdentifier: String?
62

63
  /// The TUS upload chunk size in bytes.
64
  ///
65
  /// Files uploaded via the TUS resumable protocol are split into chunks of this size.
66
  /// The smart-default ``StorageFileAPI/upload(_:data:options:)`` also uses this threshold to
67
  /// decide between multipart (≤ chunk size) and TUS (> chunk size).
68
  ///
69
  /// Defaults to 6 MB, matching the minimum part size for S3 multipart uploads.
70
  public let tusChunkSize: Int
71

72
  /// Creates a ``StorageClientConfiguration``.
73
  ///
74
  /// - Parameters:
75
  ///   - headers: HTTP headers included in every request. An `X-Client-Info` header is added
76
  ///     automatically when absent.
77
  ///   - session: The `URLSession` to use. Defaults to a `.default` session.
78
  ///   - logger: An optional `SupabaseLogger` for request/response diagnostics. Defaults to `nil`.
79
  ///   - useNewHostname: When `true`, rewrites the host to the dedicated storage subdomain for
80
  ///     large-file upload support. Defaults to `false`.
81
  ///   - backgroundDownloadSessionIdentifier: When set, downloads use a background
82
  ///     `URLSessionConfiguration` with this identifier. Defaults to `nil`.
83
  ///   - tusChunkSize: TUS upload chunk size in bytes. Also used as the threshold for the smart
84
  ///     default `upload()`/`update()` methods. Defaults to 6 MB.
85
  public init(
86
    headers: [String: String],
87
    session: URLSession = URLSession(configuration: .default),
88
    logger: (any SupabaseLogger)? = nil,
89
    useNewHostname: Bool = false,
90
    backgroundDownloadSessionIdentifier: String? = nil,
91
    tusChunkSize: Int = 6 * 1024 * 1024
92
  ) {
84✔
93
    self.headers = headers
84✔
94
    self.session = session
84✔
95
    self.logger = logger
84✔
96
    self.useNewHostname = useNewHostname
84✔
97
    self.backgroundDownloadSessionIdentifier = backgroundDownloadSessionIdentifier
84✔
98
    self.tusChunkSize = tusChunkSize
84✔
99
  }
84✔
100
}
101

102
/// A client for managing Supabase Storage buckets and files.
103
///
104
/// `StorageClient` is the entry point for all Storage operations. Use it to manage buckets
105
/// directly, or call ``from(_:)`` to obtain a ``StorageFileAPI`` for file operations within a
106
/// specific bucket.
107
///
108
/// ## Basic usage
109
///
110
/// ```swift
111
/// let storage = StorageClient(
112
///   url: URL(string: "https://<project-ref>.supabase.co/storage/v1")!,
113
///   configuration: StorageClientConfiguration(
114
///     headers: ["apikey": "<anon-key>", "Authorization": "Bearer <access-token>"]
115
///   )
116
/// )
117
///
118
/// // Create a bucket
119
/// try await storage.createBucket("avatars", options: BucketOptions(isPublic: true))
120
///
121
/// // Upload a file
122
/// try await storage.from("avatars").upload("user.png", data: imageData)
123
///
124
/// // Get public URL
125
/// let url = try storage.from("avatars").getPublicURL(path: "user.png")
126
/// ```
127
///
128
/// When using ``SupabaseClient``, the storage client is pre-configured and accessible via
129
/// `supabase.storage`.
130
///
131
/// - Note: All state is set at initialisation and never mutated, making `StorageClient` safe to
132
///   share across concurrency boundaries.
133
public final class StorageClient: Sendable {
134
  /// The base URL of the Storage API, e.g. `https://<project-ref>.supabase.co/storage/v1`.
135
  public let url: URL
136

137
  /// The configuration used by this client, including headers, session, and logging preferences.
138
  public let configuration: StorageClientConfiguration
139

140
  package let http: _HTTPClient
141
  private let usesTokenProvider: Bool
142

143
  let downloadDelegate: DownloadSessionDelegate
144
  let downloadSession: URLSession
145

146
  let encoder: JSONEncoder = {
84✔
147
    let encoder = JSONEncoder.supabase()
84✔
148
    encoder.keyEncodingStrategy = .convertToSnakeCase
84✔
149
    return encoder
84✔
150
  }()
84✔
151

152
  let decoder = JSONDecoder.supabase()
84✔
153

154
  /// Creates a `StorageClient` for standalone use (without a ``SupabaseClient``).
155
  ///
156
  /// Use this initialiser when you want to interact with Supabase Storage independently, without
157
  /// the broader Supabase client stack. For most apps, create a ``SupabaseClient`` and access
158
  /// its `storage` property instead.
159
  ///
160
  /// - Parameters:
161
  ///   - url: The base URL for the Storage endpoint,
162
  ///     e.g. `https://<project-ref>.supabase.co/storage/v1`.
163
  ///   - configuration: The client configuration, including authentication headers, URL session,
164
  ///     and logging preferences.
165
  ///
166
  /// ## Example
167
  ///
168
  /// ```swift
169
  /// let storage = StorageClient(
170
  ///   url: URL(string: "https://<project-ref>.supabase.co/storage/v1")!,
171
  ///   configuration: StorageClientConfiguration(
172
  ///     headers: ["apikey": "<anon-key>", "Authorization": "Bearer <access-token>"]
173
  ///   )
174
  /// )
175
  /// ```
176
  public convenience init(url: URL, configuration: StorageClientConfiguration) {
83✔
177
    self.init(url: url, configuration: configuration, tokenProvider: nil)
83✔
178
  }
83✔
179

180
  package init(url: URL, configuration: StorageClientConfiguration, tokenProvider: TokenProvider?) {
84✔
181
    var configuration = configuration
84✔
182

84✔
183
    let clientInfoHeader = "X-Client-Info"
84✔
184
    let clientInfoHeaders = configuration.headers.keys.filter {
84✔
185
      $0.caseInsensitiveCompare(clientInfoHeader) == .orderedSame
80✔
186
    }
80✔
187

84✔
188
    if let firstClientInfoHeader = clientInfoHeaders.first {
84✔
189
      let clientInfo = configuration.headers[firstClientInfoHeader]
5✔
190
      for duplicateHeader in clientInfoHeaders.dropFirst() {
5✔
NEW
191
        configuration.headers.removeValue(forKey: duplicateHeader)
×
192
      }
5✔
193

5✔
194
      if firstClientInfoHeader != clientInfoHeader {
5✔
NEW
195
        configuration.headers.removeValue(forKey: firstClientInfoHeader)
×
NEW
196
        configuration.headers[clientInfoHeader] = clientInfo
×
NEW
197
      }
×
198
    } else {
79✔
199
      configuration.headers["X-Client-Info"] = "storage-swift/\(version)"
79✔
200
    }
79✔
201

84✔
202
    var resolvedURL = url
84✔
203

84✔
204
    // if legacy uri is used, replace with new storage host (disables request buffering to allow > 50GB uploads)
84✔
205
    // "project-ref.supabase.co" becomes "project-ref.storage.supabase.co"
84✔
206
    if configuration.useNewHostname == true {
84✔
207
      guard
5✔
208
        var components = URLComponents(url: url, resolvingAgainstBaseURL: false),
5✔
209
        let host = components.host
5✔
210
      else {
5✔
NEW
211
        fatalError("Client initialized with invalid URL: \(url)")
×
212
      }
5✔
213

5✔
214
      let regex = try! NSRegularExpression(pattern: "supabase.(co|in|red)$")
5✔
215

5✔
216
      let isSupabaseHost =
5✔
217
        regex.firstMatch(
5✔
218
          in: host,
5✔
219
          range: NSRange(location: 0, length: host.utf16.count)
5✔
220
        ) != nil
5✔
221

5✔
222
      if isSupabaseHost, !host.contains("storage.supabase.") {
5✔
223
        components.host = host.replacingOccurrences(
2✔
224
          of: "supabase.",
2✔
225
          with: "storage.supabase."
2✔
226
        )
2✔
227
      }
2✔
228

5✔
229
      resolvedURL = components.url!
5✔
230
    }
84✔
231

84✔
232
    self.url = resolvedURL
84✔
233
    self.configuration = configuration
84✔
234
    usesTokenProvider = tokenProvider != nil
84✔
235

84✔
236
    http = _HTTPClient(
84✔
237
      host: resolvedURL,
84✔
238
      session: configuration.session,
84✔
239
      tokenProvider: tokenProvider
84✔
240
    )
84✔
241

84✔
242
    let downloadDelegate = DownloadSessionDelegate()
84✔
243
    self.downloadDelegate = downloadDelegate
84✔
244

84✔
245
    #if canImport(Darwin)
246
      let downloadSessionConfig: URLSessionConfiguration =
84✔
247
        configuration.backgroundDownloadSessionIdentifier.map {
84✔
NEW
248
          .background(withIdentifier: $0)
×
249
        } ?? .default
84✔
250
    #else
251
      let downloadSessionConfig: URLSessionConfiguration = .default
252
    #endif
253
    // Propagate any custom protocol classes (e.g. for testing) from the HTTP session.
84✔
254
    if let protocolClasses = configuration.session.configuration.protocolClasses,
84✔
255
      !protocolClasses.isEmpty
84✔
256
    {
84✔
257
      downloadSessionConfig.protocolClasses = protocolClasses
84✔
258
    }
84✔
259
    self.downloadSession = URLSession(
84✔
260
      configuration: downloadSessionConfig,
84✔
261
      delegate: downloadDelegate,
84✔
262
      delegateQueue: nil
84✔
263
    )
84✔
264
  }
84✔
265

266
  func mergedHeaders(_ headers: [String: String]? = nil) -> [String: String] {
94✔
267
    var merged = configuration.headers
94✔
268

94✔
269
    for (key, value) in headers ?? [:] {
94✔
270
      if let existingKey = merged.keys.first(where: {
54✔
271
        $0.caseInsensitiveCompare(key) == .orderedSame
54✔
272
      }) {
54✔
NEW
273
        merged[existingKey] = value
×
274
      } else {
22✔
275
        merged[key] = value
22✔
276
      }
22✔
277
    }
94✔
278

94✔
279
    if usesTokenProvider {
94✔
NEW
280
      merged = merged.filter {
×
NEW
281
        $0.key.caseInsensitiveCompare("Authorization") != .orderedSame
×
NEW
282
      }
×
NEW
283
    }
×
284

94✔
285
    return merged
94✔
286
  }
94✔
287

288
  @discardableResult
289
  func fetchData(
290
    _ method: HTTPMethod,
291
    _ path: String,
292
    query: [String: String]? = nil,
293
    body: RequestBody? = nil,
294
    headers: [String: String]? = nil
295
  ) async throws -> (Data, HTTPURLResponse) {
26✔
296
    let url = self.url.appendingPathComponent(path)
26✔
297

26✔
298
    do {
26✔
299
      logRequest(method, url: url)
26✔
300
      let result = try await http.fetchData(
26✔
301
        method,
26✔
302
        url: url,
26✔
303
        query: query,
26✔
304
        body: body,
26✔
305
        headers: mergedHeaders(headers)
26✔
306
      )
26✔
307
      logResponse(result.1, data: result.0)
22✔
308
      return result
22✔
309
    } catch {
26✔
310
      logFailure(error)
4✔
311
      throw translateStorageError(error)
4✔
312
    }
4✔
313
  }
26✔
314

315
  @discardableResult
316
  func fetchData(
317
    _ method: HTTPMethod,
318
    url: URL,
319
    query: [String: String]? = nil,
320
    body: RequestBody? = nil,
321
    headers: [String: String]? = nil
NEW
322
  ) async throws -> (Data, HTTPURLResponse) {
×
NEW
323
    do {
×
NEW
324
      logRequest(method, url: url)
×
NEW
325
      let result = try await http.fetchData(
×
NEW
326
        method,
×
NEW
327
        url: url,
×
NEW
328
        query: query,
×
NEW
329
        body: body,
×
NEW
330
        headers: mergedHeaders(headers)
×
NEW
331
      )
×
NEW
332
      logResponse(result.1, data: result.0)
×
NEW
333
      return result
×
NEW
334
    } catch {
×
NEW
335
      logFailure(error)
×
NEW
336
      throw translateStorageError(error)
×
NEW
337
    }
×
NEW
338
  }
×
339

340
  func fetchDecoded<T: Decodable>(
341
    _ method: HTTPMethod,
342
    _ path: String,
343
    query: [String: String]? = nil,
344
    body: RequestBody? = nil,
345
    headers: [String: String]? = nil,
346
    as _: T.Type = T.self
347
  ) async throws -> T {
16✔
348
    let (data, _) = try await fetchData(method, path, query: query, body: body, headers: headers)
16✔
349
    return try decoder.decode(T.self, from: data)
16✔
350
  }
16✔
351

352
  func translateStorageError(_ error: any Error) -> any Error {
4✔
353
    guard case HTTPClientError.responseError(let response, let data) = error else {
4✔
NEW
354
      return error
×
355
    }
4✔
356

4✔
357
    let decoded = try? decoder.decode(ServerErrorResponse.self, from: data)
4✔
358
    return StorageError(
4✔
359
      message: decoded?.message ?? decoded?.error ?? String(data: data, encoding: .utf8)
4✔
360
        ?? "Unknown error",
3✔
361
      errorCode: decoded?.error.map(StorageErrorCode.init(_:)) ?? .unknown,
4✔
362
      statusCode: decoded?.statusCode.flatMap(Int.init) ?? response.statusCode,
4✔
363
      underlyingResponse: response,
4✔
364
      underlyingData: data
4✔
365
    )
4✔
366
  }
4✔
367

368
  private struct ServerErrorResponse: Decodable {
369
    let message: String?
370
    let error: String?
371
    /// The server sends the status code as a JSON string, e.g. `"404"`.
372
    let statusCode: String?
373
  }
374

375
  func logRequest(_ method: HTTPMethod, url: URL) {
29✔
376
    configuration.logger?.verbose(
29✔
377
      "Request: \(method.rawValue) \(url.absoluteString.removingPercentEncoding ?? url.absoluteString)"
29✔
378
    )
29✔
379
  }
29✔
380

381
  func logResponse(_ response: HTTPURLResponse, data: Data) {
36✔
382
    configuration.logger?.verbose(
36✔
383
      "Response: Status code: \(response.statusCode) Content-Length: \(data.count)"
36✔
384
    )
36✔
385
  }
36✔
386

387
  func logFailure(_ error: any Error) {
4✔
388
    configuration.logger?.error("Response: Failure \(error)")
4✔
389
  }
4✔
390

391
  /// Forward background URLSession events from your `AppDelegate` to the Storage client.
392
  ///
393
  /// Call this from `application(_:handleEventsForBackgroundURLSession:completionHandler:)`
394
  /// when the `identifier` matches the one configured in ``StorageClientConfiguration/backgroundDownloadSessionIdentifier``.
395
  public func handleBackgroundEvents(
396
    forSessionIdentifier identifier: String,
397
    completionHandler: @escaping @Sendable () -> Void
NEW
398
  ) {
×
NEW
399
    guard identifier == configuration.backgroundDownloadSessionIdentifier else { return }
×
NEW
400
    downloadDelegate.setBackgroundCompletionHandler(completionHandler)
×
NEW
401
  }
×
402

403
  /// Returns a ``StorageFileAPI`` scoped to the given bucket.
404
  ///
405
  /// All file operations — upload, download, list, delete, signed URLs — are performed through
406
  /// the returned ``StorageFileAPI`` instance.
407
  ///
408
  /// - Parameter id: The unique identifier of the bucket to operate on.
409
  /// - Returns: A ``StorageFileAPI`` bound to the specified bucket.
410
  ///
411
  /// ## Example
412
  ///
413
  /// ```swift
414
  /// let avatarsBucket = storage.from("avatars")
415
  /// let data = try await avatarsBucket.download(path: "user-123/photo.png")
416
  /// ```
417
  public func from(_ id: String) -> StorageFileAPI {
54✔
418
    StorageFileAPI(bucketId: id, client: self)
54✔
419
  }
54✔
420

421
  /// Retrieves the details of all Storage buckets within the project.
422
  ///
423
  /// - Returns: An array of ``Bucket`` values, one per existing bucket.
424
  /// - Throws: ``StorageError`` if the request fails.
425
  ///
426
  /// ## Example
427
  ///
428
  /// ```swift
429
  /// let buckets = try await storage.listBuckets()
430
  /// for bucket in buckets {
431
  ///   print("\(bucket.id) — public: \(bucket.isPublic)")
432
  /// }
433
  /// ```
434
  public func listBuckets() async throws -> [Bucket] {
1✔
435
    try await fetchDecoded(.get, "bucket")
1✔
436
  }
1✔
437

438
  /// Retrieves the details of a single Storage bucket.
439
  ///
440
  /// - Parameter id: The unique identifier of the bucket to retrieve.
441
  /// - Returns: The matching ``Bucket``.
442
  /// - Throws: ``StorageError`` if the bucket does not exist or the request fails.
443
  ///
444
  /// ## Example
445
  ///
446
  /// ```swift
447
  /// let bucket = try await storage.getBucket("avatars")
448
  /// print("Bucket is \(bucket.isPublic ? "public" : "private")")
449
  /// ```
450
  public func getBucket(_ id: String) async throws -> Bucket {
1✔
451
    try await fetchDecoded(.get, "bucket/\(id)")
1✔
452
  }
1✔
453

454
  struct BucketParameters: Encodable {
455
    var id: String
456
    var name: String
457
    var isPublic: Bool
458
    var fileSizeLimit: Int64?
459
    var allowedMimeTypes: [String]?
460

461
    // Explicit CodingKeys required: keyEncodingStrategy (.convertToSnakeCase) would map
462
    // `isPublic` → `is_public`, but the backend wire key must be `"public"`.
463
    enum CodingKeys: String, CodingKey {
464
      case id
465
      case name
466
      case isPublic = "public"
467
      case fileSizeLimit = "file_size_limit"
468
      case allowedMimeTypes = "allowed_mime_types"
469
    }
470
  }
471

472
  /// Creates a new Storage bucket.
473
  ///
474
  /// - Parameters:
475
  ///   - id: A unique identifier for the bucket. Used as both the ID and display name.
476
  ///   - options: Visibility, file-size limit, and MIME-type restrictions for the bucket.
477
  ///     Defaults to a private bucket with no restrictions.
478
  /// - Throws: ``StorageError`` if the bucket already exists or the request fails.
479
  ///
480
  /// ## Example
481
  ///
482
  /// ```swift
483
  /// // Create a private bucket restricted to images ≤ 5 MB
484
  /// try await storage.createBucket(
485
  ///   "avatars",
486
  ///   options: BucketOptions(
487
  ///     isPublic: false,
488
  ///     fileSizeLimit: .megabytes(5),
489
  ///     allowedMimeTypes: ["image/*"]
490
  ///   )
491
  /// )
492
  /// ```
493
  public func createBucket(_ id: String, options: BucketOptions = .init())
494
    async throws
495
  {
1✔
496
    try await fetchData(
1✔
497
      .post,
1✔
498
      "bucket",
1✔
499
      body: .data(
1✔
500
        encoder.encode(
1✔
501
          BucketParameters(
1✔
502
            id: id,
1✔
503
            name: id,
1✔
504
            isPublic: options.isPublic,
1✔
505
            fileSizeLimit: options.fileSizeLimit?.bytes,
1✔
506
            allowedMimeTypes: options.allowedMimeTypes
1✔
507
          )
1✔
508
        )
1✔
509
      )
1✔
510
    )
1✔
511
  }
1✔
512

513
  /// Updates the configuration of an existing Storage bucket.
514
  ///
515
  /// - Parameters:
516
  ///   - id: The unique identifier of the bucket to update.
517
  ///   - options: The new bucket options. All fields replace the existing configuration.
518
  /// - Throws: ``StorageError`` if the bucket does not exist or the request fails.
519
  ///
520
  /// ## Example
521
  ///
522
  /// ```swift
523
  /// // Make an existing bucket public
524
  /// try await storage.updateBucket("avatars", options: BucketOptions(isPublic: true))
525
  /// ```
526
  public func updateBucket(_ id: String, options: BucketOptions) async throws {
1✔
527
    try await fetchData(
1✔
528
      .put,
1✔
529
      "bucket/\(id)",
1✔
530
      body: .data(
1✔
531
        encoder.encode(
1✔
532
          BucketParameters(
1✔
533
            id: id,
1✔
534
            name: id,
1✔
535
            isPublic: options.isPublic,
1✔
536
            fileSizeLimit: options.fileSizeLimit?.bytes,
1✔
537
            allowedMimeTypes: options.allowedMimeTypes
1✔
538
          )
1✔
539
        )
1✔
540
      )
1✔
541
    )
1✔
542
  }
1✔
543

544
  /// Removes all objects inside a bucket without deleting the bucket itself.
545
  ///
546
  /// This is a prerequisite for ``deleteBucket(_:)``, which cannot remove a bucket that
547
  /// contains objects.
548
  ///
549
  /// - Parameter id: The unique identifier of the bucket to empty.
550
  /// - Throws: ``StorageError`` if the bucket does not exist or the request fails.
551
  ///
552
  /// ## Example
553
  ///
554
  /// ```swift
555
  /// try await storage.emptyBucket("temp-uploads")
556
  /// try await storage.deleteBucket("temp-uploads")
557
  /// ```
558
  public func emptyBucket(_ id: String) async throws {
1✔
559
    try await fetchData(.post, "bucket/\(id)/empty")
1✔
560
  }
1✔
561

562
  /// Deletes an existing Storage bucket.
563
  ///
564
  /// The bucket must be empty before it can be deleted. Call ``emptyBucket(_:)`` first to
565
  /// remove all objects, or delete individual files using ``StorageFileAPI/remove(paths:)``.
566
  ///
567
  /// - Parameter id: The unique identifier of the bucket to delete.
568
  /// - Throws: ``StorageError`` if the bucket is not empty, does not exist, or the request fails.
569
  ///
570
  /// ## Example
571
  ///
572
  /// ```swift
573
  /// try await storage.emptyBucket("old-bucket")
574
  /// try await storage.deleteBucket("old-bucket")
575
  /// ```
576
  public func deleteBucket(_ id: String) async throws {
1✔
577
    try await fetchData(.delete, "bucket/\(id)")
1✔
578
  }
1✔
579
}
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