diff --git a/Sources/Storage/StorageApi.swift b/Sources/Storage/StorageApi.swift index a537b6ecf..c3f3ac422 100644 --- a/Sources/Storage/StorageApi.swift +++ b/Sources/Storage/StorageApi.swift @@ -15,6 +15,29 @@ public class StorageApi: @unchecked Sendable { if configuration.headers["X-Client-Info"] == nil { configuration.headers["X-Client-Info"] = "storage-swift/\(version)" } + + // if legacy uri is used, replace with new storage host (disables request buffering to allow > 50GB uploads) + // "project-ref.supabase.co" becomes "project-ref.storage.supabase.co" + if configuration.useNewHostname == true { + guard + var components = URLComponents(url: configuration.url, resolvingAgainstBaseURL: false), + let host = components.host + else { + fatalError("Client initialized with invalid URL: \(configuration.url)") + } + + let regex = try! NSRegularExpression(pattern: "supabase.(co|in|red)$") + + let isSupabaseHost = + regex.firstMatch(in: host, range: NSRange(location: 0, length: host.utf16.count)) != nil + + if isSupabaseHost, !host.contains("storage.supabase.") { + components.host = host.replacingOccurrences(of: "supabase.", with: "storage.supabase.") + } + + configuration.url = components.url! + } + self.configuration = configuration var interceptors: [any HTTPClientInterceptor] = [] diff --git a/Sources/Storage/SupabaseStorage.swift b/Sources/Storage/SupabaseStorage.swift index f4ed85f6c..ba043c8b8 100644 --- a/Sources/Storage/SupabaseStorage.swift +++ b/Sources/Storage/SupabaseStorage.swift @@ -1,12 +1,13 @@ import Foundation public struct StorageClientConfiguration: Sendable { - public let url: URL + public var url: URL public var headers: [String: String] public let encoder: JSONEncoder public let decoder: JSONDecoder public let session: StorageHTTPSession public let logger: (any SupabaseLogger)? + public let useNewHostname: Bool public init( url: URL, @@ -14,7 +15,8 @@ public struct StorageClientConfiguration: Sendable { encoder: JSONEncoder = .defaultStorageEncoder, decoder: JSONDecoder = .defaultStorageDecoder, session: StorageHTTPSession = .init(), - logger: (any SupabaseLogger)? = nil + logger: (any SupabaseLogger)? = nil, + useNewHostname: Bool = false ) { self.url = url self.headers = headers @@ -22,6 +24,7 @@ public struct StorageClientConfiguration: Sendable { self.decoder = decoder self.session = session self.logger = logger + self.useNewHostname = useNewHostname } } diff --git a/Sources/Supabase/SupabaseClient.swift b/Sources/Supabase/SupabaseClient.swift index 97b38b2f6..b419a94e8 100644 --- a/Sources/Supabase/SupabaseClient.swift +++ b/Sources/Supabase/SupabaseClient.swift @@ -58,7 +58,8 @@ public final class SupabaseClient: Sendable { url: storageURL, headers: headers, session: StorageHTTPSession(fetch: fetchWithAuth, upload: uploadWithAuth), - logger: options.global.logger + logger: options.global.logger, + useNewHostname: options.storage.useNewHostname ) ) } diff --git a/Sources/Supabase/Types.swift b/Sources/Supabase/Types.swift index 5d5bd9fb0..b567d7d34 100644 --- a/Sources/Supabase/Types.swift +++ b/Sources/Supabase/Types.swift @@ -10,6 +10,7 @@ public struct SupabaseClientOptions: Sendable { public let global: GlobalOptions public let functions: FunctionsOptions public let realtime: RealtimeClientOptions + public let storage: StorageOptions public struct DatabaseOptions: Sendable { /// The Postgres schema which your tables belong to. Must be on the list of exposed schemas in @@ -118,18 +119,29 @@ public struct SupabaseClientOptions: Sendable { } } + public struct StorageOptions: Sendable { + /// Whether storage client should be initialized with the new hostname format, i.e. `project-ref.storage.supabase.co` + public let useNewHostname: Bool + + public init(useNewHostname: Bool = false) { + self.useNewHostname = useNewHostname + } + } + public init( db: DatabaseOptions = .init(), auth: AuthOptions, global: GlobalOptions = .init(), functions: FunctionsOptions = .init(), - realtime: RealtimeClientOptions = .init() + realtime: RealtimeClientOptions = .init(), + storage: StorageOptions = .init() ) { self.db = db self.auth = auth self.global = global self.functions = functions self.realtime = realtime + self.storage = storage } } @@ -139,13 +151,15 @@ extension SupabaseClientOptions { db: DatabaseOptions = .init(), global: GlobalOptions = .init(), functions: FunctionsOptions = .init(), - realtime: RealtimeClientOptions = .init() + realtime: RealtimeClientOptions = .init(), + storage: StorageOptions = .init() ) { self.db = db auth = .init() self.global = global self.functions = functions self.realtime = realtime + self.storage = storage } #endif } diff --git a/Tests/StorageTests/StorageBucketAPITests.swift b/Tests/StorageTests/StorageBucketAPITests.swift index 7f8fcd7a0..d4de1cd4f 100644 --- a/Tests/StorageTests/StorageBucketAPITests.swift +++ b/Tests/StorageTests/StorageBucketAPITests.swift @@ -3,12 +3,12 @@ import Mocker import TestHelpers import XCTest +@testable import Storage + #if canImport(FoundationNetworking) import FoundationNetworking #endif -@testable import Storage - final class StorageBucketAPITests: XCTestCase { let url = URL(string: "http://localhost:54321/storage/v1")! var storage: SupabaseStorageClient! @@ -47,6 +47,60 @@ final class StorageBucketAPITests: XCTestCase { Mocker.removeAll() } + func testURLConstruction() { + let urlTestCases = [ + ( + "https://blah.supabase.co/storage/v1", + "https://blah.storage.supabase.co/storage/v1", + "update legacy prod host to new host" + ), + ( + "https://blah.supabase.red/storage/v1", + "https://blah.storage.supabase.red/storage/v1", + "update legacy staging host to new host" + ), + ( + "https://blah.storage.supabase.co/storage/v1", + "https://blah.storage.supabase.co/storage/v1", + "accept new host without modification" + ), + ( + "https://blah.supabase.co.example.com/storage/v1", + "https://blah.supabase.co.example.com/storage/v1", + "not modify non-platform hosts" + ), + ( + "http://localhost:1234/storage/v1", + "http://localhost:1234/storage/v1", + "support local host with port without modification" + ), + ] + + for (input, expect, description) in urlTestCases { + XCTContext.runActivity(named: "should \(description) if useNewHostname is true") { _ in + let storage = SupabaseStorageClient( + configuration: StorageClientConfiguration( + url: URL(string: input)!, + headers: [:], + useNewHostname: true + ) + ) + XCTAssertEqual(storage.configuration.url.absoluteString, expect) + } + + XCTContext.runActivity(named: "should not modify host if useNewHostname is false") { _ in + let storage = SupabaseStorageClient( + configuration: StorageClientConfiguration( + url: URL(string: input)!, + headers: [:], + useNewHostname: false + ) + ) + XCTAssertEqual(storage.configuration.url.absoluteString, input) + } + } + } + func testGetBucket() async throws { Mock( url: url.appendingPathComponent("bucket/bucket123"), @@ -132,7 +186,8 @@ final class StorageBucketAPITests: XCTestCase { "created_at": "2024-01-01T00:00:00.000Z", "updated_at": "2024-01-01T00:00:00.000Z" } - """.utf8) + """.utf8 + ) ] ) .snapshotRequest { @@ -171,7 +226,8 @@ final class StorageBucketAPITests: XCTestCase { "created_at": "2024-01-01T00:00:00.000Z", "updated_at": "2024-01-01T00:00:00.000Z" } - """.utf8) + """.utf8 + ) ] ) .snapshotRequest {