Skip to content

Commit 9272f1e

Browse files
authored
fix(storage): use dedicated storage host (#761)
1 parent 6a45515 commit 9272f1e

File tree

5 files changed

+106
-9
lines changed

5 files changed

+106
-9
lines changed

Sources/Storage/StorageApi.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,29 @@ public class StorageApi: @unchecked Sendable {
1515
if configuration.headers["X-Client-Info"] == nil {
1616
configuration.headers["X-Client-Info"] = "storage-swift/\(version)"
1717
}
18+
19+
// if legacy uri is used, replace with new storage host (disables request buffering to allow > 50GB uploads)
20+
// "project-ref.supabase.co" becomes "project-ref.storage.supabase.co"
21+
if configuration.useNewHostname == true {
22+
guard
23+
var components = URLComponents(url: configuration.url, resolvingAgainstBaseURL: false),
24+
let host = components.host
25+
else {
26+
fatalError("Client initialized with invalid URL: \(configuration.url)")
27+
}
28+
29+
let regex = try! NSRegularExpression(pattern: "supabase.(co|in|red)$")
30+
31+
let isSupabaseHost =
32+
regex.firstMatch(in: host, range: NSRange(location: 0, length: host.utf16.count)) != nil
33+
34+
if isSupabaseHost, !host.contains("storage.supabase.") {
35+
components.host = host.replacingOccurrences(of: "supabase.", with: "storage.supabase.")
36+
}
37+
38+
configuration.url = components.url!
39+
}
40+
1841
self.configuration = configuration
1942

2043
var interceptors: [any HTTPClientInterceptor] = []

Sources/Storage/SupabaseStorage.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,30 @@
11
import Foundation
22

33
public struct StorageClientConfiguration: Sendable {
4-
public let url: URL
4+
public var url: URL
55
public var headers: [String: String]
66
public let encoder: JSONEncoder
77
public let decoder: JSONDecoder
88
public let session: StorageHTTPSession
99
public let logger: (any SupabaseLogger)?
10+
public let useNewHostname: Bool
1011

1112
public init(
1213
url: URL,
1314
headers: [String: String],
1415
encoder: JSONEncoder = .defaultStorageEncoder,
1516
decoder: JSONDecoder = .defaultStorageDecoder,
1617
session: StorageHTTPSession = .init(),
17-
logger: (any SupabaseLogger)? = nil
18+
logger: (any SupabaseLogger)? = nil,
19+
useNewHostname: Bool = false
1820
) {
1921
self.url = url
2022
self.headers = headers
2123
self.encoder = encoder
2224
self.decoder = decoder
2325
self.session = session
2426
self.logger = logger
27+
self.useNewHostname = useNewHostname
2528
}
2629
}
2730

Sources/Supabase/SupabaseClient.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ public final class SupabaseClient: Sendable {
5858
url: storageURL,
5959
headers: headers,
6060
session: StorageHTTPSession(fetch: fetchWithAuth, upload: uploadWithAuth),
61-
logger: options.global.logger
61+
logger: options.global.logger,
62+
useNewHostname: options.storage.useNewHostname
6263
)
6364
)
6465
}

Sources/Supabase/Types.swift

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public struct SupabaseClientOptions: Sendable {
1010
public let global: GlobalOptions
1111
public let functions: FunctionsOptions
1212
public let realtime: RealtimeClientOptions
13+
public let storage: StorageOptions
1314

1415
public struct DatabaseOptions: Sendable {
1516
/// 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 {
118119
}
119120
}
120121

122+
public struct StorageOptions: Sendable {
123+
/// Whether storage client should be initialized with the new hostname format, i.e. `project-ref.storage.supabase.co`
124+
public let useNewHostname: Bool
125+
126+
public init(useNewHostname: Bool = false) {
127+
self.useNewHostname = useNewHostname
128+
}
129+
}
130+
121131
public init(
122132
db: DatabaseOptions = .init(),
123133
auth: AuthOptions,
124134
global: GlobalOptions = .init(),
125135
functions: FunctionsOptions = .init(),
126-
realtime: RealtimeClientOptions = .init()
136+
realtime: RealtimeClientOptions = .init(),
137+
storage: StorageOptions = .init()
127138
) {
128139
self.db = db
129140
self.auth = auth
130141
self.global = global
131142
self.functions = functions
132143
self.realtime = realtime
144+
self.storage = storage
133145
}
134146
}
135147

@@ -139,13 +151,15 @@ extension SupabaseClientOptions {
139151
db: DatabaseOptions = .init(),
140152
global: GlobalOptions = .init(),
141153
functions: FunctionsOptions = .init(),
142-
realtime: RealtimeClientOptions = .init()
154+
realtime: RealtimeClientOptions = .init(),
155+
storage: StorageOptions = .init()
143156
) {
144157
self.db = db
145158
auth = .init()
146159
self.global = global
147160
self.functions = functions
148161
self.realtime = realtime
162+
self.storage = storage
149163
}
150164
#endif
151165
}

Tests/StorageTests/StorageBucketAPITests.swift

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ import Mocker
33
import TestHelpers
44
import XCTest
55

6+
@testable import Storage
7+
68
#if canImport(FoundationNetworking)
79
import FoundationNetworking
810
#endif
911

10-
@testable import Storage
11-
1212
final class StorageBucketAPITests: XCTestCase {
1313
let url = URL(string: "http://localhost:54321/storage/v1")!
1414
var storage: SupabaseStorageClient!
@@ -47,6 +47,60 @@ final class StorageBucketAPITests: XCTestCase {
4747
Mocker.removeAll()
4848
}
4949

50+
func testURLConstruction() {
51+
let urlTestCases = [
52+
(
53+
"https://blah.supabase.co/storage/v1",
54+
"https://blah.storage.supabase.co/storage/v1",
55+
"update legacy prod host to new host"
56+
),
57+
(
58+
"https://blah.supabase.red/storage/v1",
59+
"https://blah.storage.supabase.red/storage/v1",
60+
"update legacy staging host to new host"
61+
),
62+
(
63+
"https://blah.storage.supabase.co/storage/v1",
64+
"https://blah.storage.supabase.co/storage/v1",
65+
"accept new host without modification"
66+
),
67+
(
68+
"https://blah.supabase.co.example.com/storage/v1",
69+
"https://blah.supabase.co.example.com/storage/v1",
70+
"not modify non-platform hosts"
71+
),
72+
(
73+
"http://localhost:1234/storage/v1",
74+
"http://localhost:1234/storage/v1",
75+
"support local host with port without modification"
76+
),
77+
]
78+
79+
for (input, expect, description) in urlTestCases {
80+
XCTContext.runActivity(named: "should \(description) if useNewHostname is true") { _ in
81+
let storage = SupabaseStorageClient(
82+
configuration: StorageClientConfiguration(
83+
url: URL(string: input)!,
84+
headers: [:],
85+
useNewHostname: true
86+
)
87+
)
88+
XCTAssertEqual(storage.configuration.url.absoluteString, expect)
89+
}
90+
91+
XCTContext.runActivity(named: "should not modify host if useNewHostname is false") { _ in
92+
let storage = SupabaseStorageClient(
93+
configuration: StorageClientConfiguration(
94+
url: URL(string: input)!,
95+
headers: [:],
96+
useNewHostname: false
97+
)
98+
)
99+
XCTAssertEqual(storage.configuration.url.absoluteString, input)
100+
}
101+
}
102+
}
103+
50104
func testGetBucket() async throws {
51105
Mock(
52106
url: url.appendingPathComponent("bucket/bucket123"),
@@ -132,7 +186,8 @@ final class StorageBucketAPITests: XCTestCase {
132186
"created_at": "2024-01-01T00:00:00.000Z",
133187
"updated_at": "2024-01-01T00:00:00.000Z"
134188
}
135-
""".utf8)
189+
""".utf8
190+
)
136191
]
137192
)
138193
.snapshotRequest {
@@ -171,7 +226,8 @@ final class StorageBucketAPITests: XCTestCase {
171226
"created_at": "2024-01-01T00:00:00.000Z",
172227
"updated_at": "2024-01-01T00:00:00.000Z"
173228
}
174-
""".utf8)
229+
""".utf8
230+
)
175231
]
176232
)
177233
.snapshotRequest {

0 commit comments

Comments
 (0)