├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── Makefile ├── Package.swift ├── README.md ├── Sources └── SupabaseStorage │ ├── Bucket.swift │ ├── BucketOptions.swift │ ├── FileObject.swift │ ├── FileOptions.swift │ ├── MultipartFile.swift │ ├── SearchOptions.swift │ ├── SortBy.swift │ ├── StorageApi.swift │ ├── StorageBucketApi.swift │ ├── StorageError.swift │ ├── StorageFileApi.swift │ ├── StorageHTTPClient.swift │ ├── SupabaseStorage.swift │ └── TransformOptions.swift ├── Tests └── SupabaseStorageTests │ └── SupabaseStorageTests.swift └── supabase ├── .gitignore ├── config.toml ├── functions └── .vscode │ ├── extensions.json │ └── settings.json ├── migrations └── 20230608140749_storage_permissions.sql └── seed.sql /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - "*" 10 | 11 | concurrency: 12 | group: ci-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | mac: 17 | name: MacOS 12 18 | runs-on: macos-12 19 | strategy: 20 | matrix: 21 | xcode: ["14.2"] 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Select Xcode ${{ matrix.xcode }} 25 | run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app 26 | - name: Print Swift version 27 | run: swift --version 28 | - name: Run build 29 | run: make build-swift 30 | 31 | linux: 32 | name: Ubuntu 33 | runs-on: ubuntu-latest 34 | steps: 35 | - uses: actions/checkout@v2 36 | - name: Run build 37 | run: make build-linux 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | 92 | .DS_Store 93 | /.build 94 | /Packages 95 | /*.xcodeproj 96 | /.swiftpm -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Satish Babariya 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build-all: build-swift build-linux 2 | 3 | build-swift: 4 | swift build -c release 5 | 6 | build-linux: 7 | docker run \ 8 | --rm \ 9 | -v "$(PWD):$(PWD)" \ 10 | -w "$(PWD)" \ 11 | swift:5.5 \ 12 | bash -c 'make build-swift' 13 | 14 | format: 15 | swift format --in-place --recursive . 16 | 17 | 18 | .PHONY: build-swift build-linux format 19 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "SupabaseStorage", 8 | platforms: [ 9 | .iOS(.v13), 10 | .macCatalyst(.v13), 11 | .macOS(.v10_15), 12 | .watchOS(.v6), 13 | .tvOS(.v13), 14 | ], 15 | products: [ 16 | .library( 17 | name: "SupabaseStorage", 18 | targets: ["SupabaseStorage"] 19 | ) 20 | ], 21 | dependencies: [], 22 | targets: [ 23 | .target( 24 | name: "SupabaseStorage", 25 | dependencies: [] 26 | ), 27 | .testTarget( 28 | name: "SupabaseStorageTests", 29 | dependencies: ["SupabaseStorage"] 30 | ), 31 | ] 32 | ) 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `storage-swift` 2 | 3 | > [!WARNING] 4 | > This repository is deprecated and it was moved to the [monorepo](https://github.com/supabase-community/supabase-swift). 5 | > Repository will remain live to support old versions of the library, but any new updates **MUST** be done on the monorepo. 6 | 7 | Swift Client library to interact with Supabase Storage. 8 | 9 | ## `Prelude: RLS` 10 | 11 | A lot of bug reports get filed towards all the Supabase Storage libraries because 12 | RLS is easy to get wrong for the un-initiated. 13 | 14 | Before you can use any client service, you should follow the RLS guides on 15 | Supabase's website. You will need to create policies on each bucket individually 16 | or use global ones on the `storage/object` and `storage/bucket` sections. 17 | 18 | For image/video uploads from users, there is a key example on how to setup basic 19 | RLS for all CRUD operations [here](https://supabase.com/docs/guides/storage/access-control). 20 | Pay particular attention to the difference between `USING` and `WITH CHECK` keyword 21 | for POSTGRES SQL. 22 | 23 | Once you have RLS setup for ALL CRUD operations you should be able to use the 24 | bucket with the clients. There are some issues with uploading or downloading with 25 | buckets that change their permission levels from public to private and vice versa, 26 | so use caution when playing around with those settings if you're experimenting. If 27 | you experience a bunch of 404's with uploads and downloads, try deleting the 28 | bucket and re-initializing it. 29 | 30 | ## `Instantiating the client` 31 | 32 | If you're using the full `supabase-swift` experience, you can create a reference 33 | to the storage client by accessing the following: 34 | 35 | ```Swift 36 | import Supabase 37 | import SupabaseStorage 38 | 39 | lazy var supabaseClient: SupabaseClient = { 40 | return SupabaseClient(supabaseURL: supabaseUrl, supabaseKey: apiKey) 41 | }() 42 | 43 | lazy var supabaseStorageClient: SupabaseStorageClient = { 44 | return supabaseClient.storage 45 | }() 46 | ``` 47 | 48 | If you're experiencing errors using this instance, or would just like to construct 49 | your own, you may do so via: 50 | 51 | ```Swift 52 | func storageClient(bucketName: String = "photos") async -> StorageFileApi? { 53 | guard let jwt = try? await supabaseClient.auth.session.accessToken else { return nil} 54 | return SupabaseStorageClient( 55 | url: "\(supabaseUrl)/storage/v1", 56 | headers: [ 57 | "Authorization": "Bearer \(jwt)", 58 | "apikey": apiKey, 59 | ] 60 | ).from(id: bucketName) 61 | } 62 | ``` 63 | 64 | Architecturally, a pattern you can follow is holding these values and functions in a provider class using a simple Singleton pattern 65 | or inject it with something like `Resolver`. 66 | 67 | ```Swift 68 | class SupabaseProvider { 69 | 70 | private let apiDictionaryKey = "supabase-key" 71 | private let supabaseUrlKey = "supabase-url" 72 | 73 | private init() {} 74 | 75 | static let shared = SupabaseProvider() 76 | 77 | lazy var supabaseClient: SupabaseClient = { 78 | return SupabaseClient(supabaseURL: supabaseUrl, supabaseKey: apiKey) 79 | }() 80 | 81 | func loggedInUserId() async -> String? { 82 | return try? await SupabaseProvider.shared.supabaseClient.auth.session.user.id.uuidString 83 | } 84 | 85 | private var keysPlist: NSDictionary { 86 | if 87 | let path = Bundle.main.path(forResource: "Keys", ofType: "plist"), 88 | let dictionary = NSDictionary(contentsOfFile: path) { 89 | return dictionary 90 | } 91 | fatalError("You must have a Keys.plist file in your application codebase.") 92 | } 93 | 94 | private var apiKey: String { 95 | guard let apiKey = keysPlist[apiDictionaryKey] as? String else { 96 | fatalError("Your Keys.plist must have a key of: \(apiDictionaryKey) and a corresponding value of type String.") 97 | } 98 | return apiKey 99 | } 100 | 101 | var supabaseUrl: URL { 102 | guard let url = keysPlist[supabaseUrlKey] as? String else { 103 | fatalError("Your Keys.plist must have a key of: \(supabaseUrlKey) and a corresponding value of type String.") 104 | } 105 | return URL(string: url)! 106 | } 107 | 108 | // Storage 109 | 110 | func storageClient(bucketName: String = "photos") async -> StorageFileApi? { 111 | guard let jwt = try? await supabaseClient.auth.session.accessToken else { return nil} 112 | return SupabaseStorageClient( 113 | url: "\(supabaseUrl)/storage/v1", 114 | headers: [ 115 | "Authorization": "Bearer \(jwt)", 116 | "apikey": apiKey, 117 | ] 118 | ).from(id: bucketName) 119 | } 120 | } 121 | 122 | ``` 123 | 124 | ## `Uploading and Downloading` 125 | 126 | With your client of choice, you can download via the following given some RLS and 127 | naming conventions illustrated above. 128 | 129 | Example of converting a `.png` image download to a `UIImage` for a `UIImageView`: 130 | 131 | ```Swift 132 | if let data = try? await SupabaseProvider.shared.storageClient()?.download( 133 | path: profilePhotoUrl 134 | ) { 135 | imageView.image = UIImage(data: data) 136 | } 137 | ``` 138 | 139 | Example of uploading an image via a `UIImageView's` current `UIImage` and saving 140 | it to a user's bucket folder: 141 | 142 | ```Swift 143 | 144 | func loggedInUserId() async -> String? { 145 | return try? await SupabaseProvider.shared.client.auth.session.user.id.uuidString 146 | } 147 | 148 | guard let image = imageView.image?.pngData() else { return } 149 | 150 | // Note that Swift has UUID's all capitalized, but Supabase will always lowercase 151 | // them. 152 | 153 | guard let userId = loggedInUserId().lowercased() else { return } 154 | 155 | let fileId = UUID().uuidString 156 | let filename = "\(fileId).png" 157 | let storageClient = await SupabaseProvider.shared.storageClient() 158 | guard let uploadResponseData = try? await storageClient?.upload( 159 | path: "\(userId)/\(filename)", 160 | file: File( 161 | name: filename, 162 | data: image, 163 | fileName: filename, 164 | contentType: "image/png"), 165 | fileOptions: FileOptions(cacheControl: "3600") 166 | ) 167 | ) else { return } 168 | ``` 169 | 170 | ## `URL Creation` 171 | 172 | Signed URL creation is fairly straightforward and is the recommended way to grab URLs from storage devices. For larger files, you should 173 | incorporate `CoreData` to keep them on device, probably with the help of an LRU Cache. 174 | 175 | ```Swift 176 | let storageClient = await SupabaseProvider.shared.storageClient(bucketName: "bucket_name") 177 | // URL will expire in 1 hour (3600 seconds) 178 | guard let url = try? await storageClient?.createSignedURL(path: imageUrl, expiresIn: 3600) else { 179 | return 180 | } 181 | DispatchQueue.main.async { 182 | guard let data = try? Data(contentsOf: url) else { return } 183 | self.imageView.image = UIImage(data: data) 184 | } 185 | ``` 186 | 187 | ## Sponsors 188 | 189 | We are building the features of Firebase using enterprise-grade, open source products. We support existing communities wherever possible, and if the products don’t exist we build them and open source them ourselves. Thanks to these sponsors who are making the OSS ecosystem better for everyone. 190 | 191 | [![New Sponsor](https://user-images.githubusercontent.com/10214025/90518111-e74bbb00-e198-11ea-8f88-c9e3c1aa4b5b.png)](https://github.com/sponsors/supabase) 192 | -------------------------------------------------------------------------------- /Sources/SupabaseStorage/Bucket.swift: -------------------------------------------------------------------------------- 1 | public struct Bucket: Hashable { 2 | public var id: String 3 | public var name: String 4 | public var owner: String 5 | public var isPublic: Bool 6 | public var createdAt: String 7 | public var updatedAt: String 8 | 9 | init?(from dictionary: [String: Any]) { 10 | guard 11 | let id = dictionary["id"] as? String, 12 | let name = dictionary["name"] as? String, 13 | let owner = dictionary["owner"] as? String, 14 | let createdAt = dictionary["created_at"] as? String, 15 | let updatedAt = dictionary["updated_at"] as? String, 16 | let isPublic = dictionary["public"] as? Bool 17 | else { 18 | return nil 19 | } 20 | 21 | self.id = id 22 | self.name = name 23 | self.owner = owner 24 | self.isPublic = isPublic 25 | self.createdAt = createdAt 26 | self.updatedAt = updatedAt 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/SupabaseStorage/BucketOptions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct BucketOptions { 4 | public let `public`: Bool 5 | public let fileSizeLimit: Int? 6 | public let allowedMimeTypes: [String]? 7 | 8 | public init(public: Bool = false, fileSizeLimit: Int? = nil, allowedMimeTypes: [String]? = nil) { 9 | self.public = `public` 10 | self.fileSizeLimit = fileSizeLimit 11 | self.allowedMimeTypes = allowedMimeTypes 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/SupabaseStorage/FileObject.swift: -------------------------------------------------------------------------------- 1 | public struct FileObject { 2 | public var name: String 3 | public var bucketId: String? 4 | public var owner: String? 5 | public var id: String 6 | public var updatedAt: String 7 | public var createdAt: String 8 | public var lastAccessedAt: String 9 | public var metadata: [String: Any] 10 | public var buckets: Bucket? 11 | 12 | public init?(from dictionary: [String: Any]) { 13 | guard 14 | let name = dictionary["name"] as? String, 15 | let id = dictionary["id"] as? String, 16 | let updatedAt = dictionary["updated_at"] as? String, 17 | let createdAt = dictionary["created_at"] as? String, 18 | let lastAccessedAt = dictionary["last_accessed_at"] as? String, 19 | let metadata = dictionary["metadata"] as? [String: Any] 20 | else { 21 | return nil 22 | } 23 | 24 | self.name = name 25 | self.bucketId = dictionary["bucket_id"] as? String 26 | self.owner = dictionary["owner"] as? String 27 | self.id = id 28 | self.updatedAt = updatedAt 29 | self.createdAt = createdAt 30 | self.lastAccessedAt = lastAccessedAt 31 | self.metadata = metadata 32 | 33 | if let buckets = dictionary["buckets"] as? [String: Any] { 34 | self.buckets = Bucket(from: buckets) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/SupabaseStorage/FileOptions.swift: -------------------------------------------------------------------------------- 1 | public struct FileOptions { 2 | public var cacheControl: String 3 | 4 | public init(cacheControl: String) { 5 | self.cacheControl = cacheControl 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Sources/SupabaseStorage/MultipartFile.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct File: Hashable, Equatable { 4 | public var name: String 5 | public var data: Data 6 | public var fileName: String? 7 | public var contentType: String? 8 | 9 | public init(name: String, data: Data, fileName: String?, contentType: String?) { 10 | self.name = name 11 | self.data = data 12 | self.fileName = fileName 13 | self.contentType = contentType 14 | } 15 | } 16 | 17 | public class FormData { 18 | var files: [File] = [] 19 | var boundary: String 20 | 21 | public init(boundary: String = UUID().uuidString) { 22 | self.boundary = boundary 23 | } 24 | 25 | public func append(file: File) { 26 | files.append(file) 27 | } 28 | 29 | public var contentType: String { 30 | return "multipart/form-data; boundary=\(boundary)" 31 | } 32 | 33 | public var data: Data { 34 | var data = Data() 35 | 36 | for file in files { 37 | data.append("--\(boundary)\r\n") 38 | data.append("Content-Disposition: form-data; name=\"\(file.name)\"") 39 | if let filename = file.fileName?.replacingOccurrences(of: "\"", with: "_") { 40 | data.append("; filename=\"\(filename)\"") 41 | } 42 | data.append("\r\n") 43 | if let contentType = file.contentType { 44 | data.append("Content-Type: \(contentType)\r\n") 45 | } 46 | data.append("\r\n") 47 | data.append(file.data) 48 | data.append("\r\n") 49 | } 50 | 51 | data.append("--\(boundary)--\r\n") 52 | return data 53 | } 54 | } 55 | 56 | extension Data { 57 | mutating func append(_ string: String) { 58 | let data = string.data( 59 | using: String.Encoding.utf8, 60 | allowLossyConversion: true 61 | ) 62 | append(data!) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/SupabaseStorage/SearchOptions.swift: -------------------------------------------------------------------------------- 1 | public struct SearchOptions { 2 | /// The number of files you want to be returned. 3 | public var limit: Int? 4 | 5 | /// The starting position. 6 | public var offset: Int? 7 | 8 | /// The column to sort by. Can be any column inside a ``FileObject``. 9 | public var sortBy: SortBy? 10 | 11 | /// The search string to filter files by. 12 | public var search: String? 13 | 14 | public init(limit: Int? = nil, offset: Int? = nil, sortBy: SortBy? = nil, search: String? = nil) { 15 | self.limit = limit 16 | self.offset = offset 17 | self.sortBy = sortBy 18 | self.search = search 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/SupabaseStorage/SortBy.swift: -------------------------------------------------------------------------------- 1 | public struct SortBy { 2 | public var column: String? 3 | public var order: String? 4 | 5 | public init(column: String? = nil, order: String? = nil) { 6 | self.column = column 7 | self.order = order 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Sources/SupabaseStorage/StorageApi.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | #if canImport(FoundationNetworking) 4 | import FoundationNetworking 5 | #endif 6 | 7 | public class StorageApi { 8 | var url: String 9 | var headers: [String: String] 10 | var http: StorageHTTPClient 11 | 12 | init(url: String, headers: [String: String], http: StorageHTTPClient) { 13 | self.url = url 14 | self.headers = headers 15 | self.http = http 16 | // self.headers.merge(["Content-Type": "application/json"]) { $1 } 17 | } 18 | 19 | internal enum HTTPMethod: String { 20 | case get = "GET" 21 | case head = "HEAD" 22 | case post = "POST" 23 | case put = "PUT" 24 | case delete = "DELETE" 25 | case connect = "CONNECT" 26 | case options = "OPTIONS" 27 | case trace = "TRACE" 28 | case patch = "PATCH" 29 | } 30 | 31 | @discardableResult 32 | internal func fetch( 33 | url: URL, 34 | method: HTTPMethod = .get, 35 | parameters: [String: Any]?, 36 | headers: [String: String]? = nil 37 | ) async throws -> Any { 38 | var request = URLRequest(url: url) 39 | request.httpMethod = method.rawValue 40 | 41 | if var headers = headers { 42 | headers.merge(self.headers) { $1 } 43 | request.allHTTPHeaderFields = headers 44 | } else { 45 | request.allHTTPHeaderFields = self.headers 46 | } 47 | 48 | if let parameters = parameters { 49 | request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: []) 50 | } 51 | 52 | let (data, response) = try await http.fetch(request) 53 | if let mimeType = response.mimeType { 54 | switch mimeType { 55 | case "application/json": 56 | let json = try JSONSerialization.jsonObject(with: data, options: []) 57 | return try parse(response: json, statusCode: response.statusCode) 58 | default: 59 | return try parse(response: data, statusCode: response.statusCode) 60 | } 61 | } else { 62 | throw StorageError(message: "failed to get response") 63 | } 64 | } 65 | 66 | internal func fetch( 67 | url: URL, 68 | method: HTTPMethod = .post, 69 | formData: FormData, 70 | headers: [String: String]? = nil, 71 | fileOptions: FileOptions? = nil, 72 | jsonSerialization: Bool = true 73 | ) async throws -> Any { 74 | var request = URLRequest(url: url) 75 | request.httpMethod = method.rawValue 76 | 77 | if let fileOptions = fileOptions { 78 | request.setValue(fileOptions.cacheControl, forHTTPHeaderField: "cacheControl") 79 | } 80 | 81 | var allHTTPHeaderFields = self.headers 82 | if let headers = headers { 83 | allHTTPHeaderFields.merge(headers) { $1 } 84 | } 85 | 86 | allHTTPHeaderFields.forEach { key, value in 87 | request.setValue(value, forHTTPHeaderField: key) 88 | } 89 | 90 | request.setValue(formData.contentType, forHTTPHeaderField: "Content-Type") 91 | 92 | let (data, response) = try await http.upload(request, from: formData.data) 93 | 94 | if jsonSerialization { 95 | let json = try JSONSerialization.jsonObject(with: data, options: []) 96 | return try parse(response: json, statusCode: response.statusCode) 97 | } 98 | 99 | if let dataString = String(data: data, encoding: .utf8) { 100 | return dataString 101 | } 102 | 103 | throw StorageError(message: "failed to get response") 104 | } 105 | 106 | private func parse(response: Any, statusCode: Int) throws -> Any { 107 | if statusCode == 200 || 200..<300 ~= statusCode { 108 | return response 109 | } else if let dict = response as? [String: Any], let message = dict["message"] as? String { 110 | throw StorageError(statusCode: statusCode, message: message) 111 | } else if let dict = response as? [String: Any], let error = dict["error"] as? String { 112 | throw StorageError(statusCode: statusCode, message: error) 113 | } else { 114 | throw StorageError(statusCode: statusCode, message: "something went wrong") 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Sources/SupabaseStorage/StorageBucketApi.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | #if canImport(FoundationNetworking) 4 | import FoundationNetworking 5 | #endif 6 | 7 | /// Storage Bucket API 8 | public class StorageBucketApi: StorageApi { 9 | /// StorageBucketApi initializer 10 | /// - Parameters: 11 | /// - url: Storage HTTP URL 12 | /// - headers: HTTP headers. 13 | override init(url: String, headers: [String: String], http: StorageHTTPClient) { 14 | super.init(url: url, headers: headers, http: http) 15 | self.headers.merge(["Content-Type": "application/json"]) { $1 } 16 | } 17 | 18 | /// Retrieves the details of all Storage buckets within an existing product. 19 | public func listBuckets() async throws -> [Bucket] { 20 | guard let url = URL(string: "\(url)/bucket") else { 21 | throw StorageError(message: "badURL") 22 | } 23 | 24 | let response = try await fetch(url: url, method: .get, parameters: nil, headers: headers) 25 | guard let dict = response as? [[String: Any]] else { 26 | throw StorageError(message: "failed to parse response") 27 | } 28 | 29 | return dict.compactMap { Bucket(from: $0) } 30 | } 31 | 32 | /// Retrieves the details of an existing Storage bucket. 33 | /// - Parameters: 34 | /// - id: The unique identifier of the bucket you would like to retrieve. 35 | public func getBucket(id: String) async throws -> Bucket { 36 | guard let url = URL(string: "\(url)/bucket/\(id)") else { 37 | throw StorageError(message: "badURL") 38 | } 39 | 40 | let response = try await fetch(url: url, method: .get, parameters: nil, headers: headers) 41 | guard 42 | let dict = response as? [String: Any], 43 | let bucket = Bucket(from: dict) 44 | else { 45 | throw StorageError(message: "failed to parse response") 46 | } 47 | 48 | return bucket 49 | } 50 | 51 | /// Creates a new Storage bucket 52 | /// - Parameters: 53 | /// - id: A unique identifier for the bucket you are creating. 54 | /// - completion: newly created bucket id 55 | public func createBucket( 56 | id: String, 57 | options: BucketOptions = .init() 58 | ) async throws -> [String: Any] { 59 | guard let url = URL(string: "\(url)/bucket") else { 60 | throw StorageError(message: "badURL") 61 | } 62 | 63 | var params: [String: Any] = [ 64 | "id": id, 65 | "name": id, 66 | ] 67 | 68 | params["public"] = options.public 69 | params["file_size_limit"] = options.fileSizeLimit 70 | params["allowed_mime_types"] = options.allowedMimeTypes 71 | 72 | let response = try await fetch( 73 | url: url, 74 | method: .post, 75 | parameters: params, 76 | headers: headers 77 | ) 78 | 79 | guard let dict = response as? [String: Any] else { 80 | throw StorageError(message: "failed to parse response") 81 | } 82 | 83 | return dict 84 | } 85 | 86 | /// Removes all objects inside a single bucket. 87 | /// - Parameters: 88 | /// - id: The unique identifier of the bucket you would like to empty. 89 | @discardableResult 90 | public func emptyBucket(id: String) async throws -> [String: Any] { 91 | guard let url = URL(string: "\(url)/bucket/\(id)/empty") else { 92 | throw StorageError(message: "badURL") 93 | } 94 | 95 | let response = try await fetch(url: url, method: .post, parameters: [:], headers: headers) 96 | guard let dict = response as? [String: Any] else { 97 | throw StorageError(message: "failed to parse response") 98 | } 99 | return dict 100 | } 101 | 102 | /// Deletes an existing bucket. A bucket can't be deleted with existing objects inside it. 103 | /// You must first `empty()` the bucket. 104 | /// - Parameters: 105 | /// - id: The unique identifier of the bucket you would like to delete. 106 | public func deleteBucket(id: String) async throws -> [String: Any] { 107 | guard let url = URL(string: "\(url)/bucket/\(id)") else { 108 | throw StorageError(message: "badURL") 109 | } 110 | 111 | let response = try await fetch(url: url, method: .delete, parameters: [:], headers: headers) 112 | guard let dict = response as? [String: Any] else { 113 | throw StorageError(message: "failed to parse response") 114 | } 115 | return dict 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Sources/SupabaseStorage/StorageError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct StorageError: Error { 4 | public var statusCode: Int? 5 | public var message: String? 6 | 7 | public init(statusCode: Int? = nil, message: String? = nil) { 8 | self.statusCode = statusCode 9 | self.message = message 10 | } 11 | } 12 | 13 | extension StorageError: LocalizedError { 14 | public var errorDescription: String? { 15 | return message 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/SupabaseStorage/StorageFileApi.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | #if canImport(FoundationNetworking) 4 | import FoundationNetworking 5 | #endif 6 | 7 | let DEFAULT_SEARCH_OPTIONS = SearchOptions( 8 | limit: 100, 9 | offset: 0, 10 | sortBy: SortBy( 11 | column: "name", 12 | order: "asc" 13 | ) 14 | ) 15 | 16 | /// Supabase Storage File API 17 | public class StorageFileApi: StorageApi { 18 | /// The bucket id to operate on. 19 | var bucketId: String 20 | 21 | /// StorageFileApi initializer 22 | /// - Parameters: 23 | /// - url: Storage HTTP URL 24 | /// - headers: HTTP headers. 25 | /// - bucketId: The bucket id to operate on. 26 | init(url: String, headers: [String: String], bucketId: String, http: StorageHTTPClient) { 27 | self.bucketId = bucketId 28 | super.init(url: url, headers: headers, http: http) 29 | } 30 | 31 | /// Uploads a file to an existing bucket. 32 | /// - Parameters: 33 | /// - path: The relative file path. Should be of the format `folder/subfolder/filename.png`. The 34 | /// bucket must already exist before attempting to upload. 35 | /// - file: The File object to be stored in the bucket. 36 | /// - fileOptions: HTTP headers. For example `cacheControl` 37 | public func upload(path: String, file: File, fileOptions: FileOptions?) async throws -> Any { 38 | guard let url = URL(string: "\(url)/object/\(bucketId)/\(path)") else { 39 | throw StorageError(message: "badURL") 40 | } 41 | 42 | let formData = FormData() 43 | formData.append(file: file) 44 | 45 | return try await fetch( 46 | url: url, 47 | method: .post, 48 | formData: formData, 49 | headers: headers, 50 | fileOptions: fileOptions 51 | ) 52 | } 53 | 54 | /// Replaces an existing file at the specified path with a new one. 55 | /// - Parameters: 56 | /// - path: The relative file path. Should be of the format `folder/subfolder`. The bucket 57 | /// already exist before attempting to upload. 58 | /// - file: The file object to be stored in the bucket. 59 | /// - fileOptions: HTTP headers. For example `cacheControl` 60 | public func update(path: String, file: File, fileOptions: FileOptions?) async throws -> Any { 61 | guard let url = URL(string: "\(url)/object/\(bucketId)/\(path)") else { 62 | throw StorageError(message: "badURL") 63 | } 64 | 65 | let formData = FormData() 66 | formData.append(file: file) 67 | 68 | return try await fetch( 69 | url: url, 70 | method: .put, 71 | formData: formData, 72 | headers: headers, 73 | fileOptions: fileOptions 74 | ) 75 | } 76 | 77 | /// Moves an existing file, optionally renaming it at the same time. 78 | /// - Parameters: 79 | /// - fromPath: The original file path, including the current file name. For example 80 | /// `folder/image.png`. 81 | /// - toPath: The new file path, including the new file name. For example 82 | /// `folder/image-copy.png`. 83 | public func move(fromPath: String, toPath: String) async throws -> [String: Any] { 84 | guard let url = URL(string: "\(url)/object/move") else { 85 | throw StorageError(message: "badURL") 86 | } 87 | 88 | let response = try await fetch( 89 | url: url, method: .post, 90 | parameters: ["bucketId": bucketId, "sourceKey": fromPath, "destinationKey": toPath], 91 | headers: headers 92 | ) 93 | 94 | guard let dict = response as? [String: Any] else { 95 | throw StorageError(message: "failed to parse response") 96 | } 97 | 98 | return dict 99 | } 100 | 101 | /// Create signed url to download file without requiring permissions. This URL can be valid for a 102 | /// set number of seconds. 103 | /// - Parameters: 104 | /// - path: The file path to be downloaded, including the current file name. For example 105 | /// `folder/image.png`. 106 | /// - expiresIn: The number of seconds until the signed URL expires. For example, `60` for a URL 107 | /// which is valid for one minute. 108 | public func createSignedURL(path: String, expiresIn: Int) async throws -> URL { 109 | guard let url = URL(string: "\(url)/object/sign/\(bucketId)/\(path)") else { 110 | throw StorageError(message: "badURL") 111 | } 112 | 113 | let response = try await fetch( 114 | url: url, 115 | method: .post, 116 | parameters: ["expiresIn": expiresIn], 117 | headers: headers 118 | ) 119 | guard 120 | let dict = response as? [String: Any], 121 | let signedURLString = dict["signedURL"] as? String, 122 | let signedURL = URL(string: self.url.appending(signedURLString)) 123 | else { 124 | throw StorageError(message: "failed to parse response") 125 | } 126 | return signedURL 127 | } 128 | 129 | /// Deletes files within the same bucket 130 | /// - Parameters: 131 | /// - paths: An array of files to be deletes, including the path and file name. For example 132 | /// [`folder/image.png`]. 133 | public func remove(paths: [String]) async throws -> [FileObject] { 134 | guard let url = URL(string: "\(url)/object/\(bucketId)") else { 135 | throw StorageError(message: "badURL") 136 | } 137 | 138 | let response = try await fetch( 139 | url: url, 140 | method: .delete, 141 | parameters: ["prefixes": paths], 142 | headers: headers 143 | ) 144 | guard let array = response as? [[String: Any]] else { 145 | throw StorageError(message: "failed to parse response") 146 | } 147 | 148 | return array.compactMap { FileObject(from: $0) } 149 | } 150 | 151 | /// Lists all the files within a bucket. 152 | /// - Parameters: 153 | /// - path: The folder path. 154 | /// - options: Search options, including `limit`, `offset`, and `sortBy`. 155 | public func list( 156 | path: String? = nil, 157 | options: SearchOptions? = nil 158 | ) async throws -> [FileObject] { 159 | guard let url = URL(string: "\(url)/object/list/\(bucketId)") else { 160 | throw StorageError(message: "badURL") 161 | } 162 | 163 | var parameters: [String: Any] = ["prefix": path ?? ""] 164 | parameters["limit"] = options?.limit ?? DEFAULT_SEARCH_OPTIONS.limit 165 | parameters["offset"] = options?.offset ?? DEFAULT_SEARCH_OPTIONS.offset 166 | parameters["search"] = options?.search ?? DEFAULT_SEARCH_OPTIONS.search 167 | 168 | if let sortBy = options?.sortBy ?? DEFAULT_SEARCH_OPTIONS.sortBy { 169 | parameters["sortBy"] = [ 170 | "column": sortBy.column, 171 | "order": sortBy.order, 172 | ] 173 | } 174 | 175 | let response = try await fetch( 176 | url: url, method: .post, parameters: parameters, headers: headers) 177 | 178 | guard let array = response as? [[String: Any]] else { 179 | throw StorageError(message: "failed to parse response") 180 | } 181 | 182 | return array.compactMap { FileObject(from: $0) } 183 | } 184 | 185 | /// Downloads a file. 186 | /// - Parameters: 187 | /// - path: The file path to be downloaded, including the path and file name. For example 188 | /// `folder/image.png`. 189 | @discardableResult 190 | public func download(path: String) async throws -> Data { 191 | guard let url = URL(string: "\(url)/object/\(bucketId)/\(path)") else { 192 | throw StorageError(message: "badURL") 193 | } 194 | 195 | let response = try await fetch(url: url, parameters: nil) 196 | guard let data = response as? Data else { 197 | throw StorageError(message: "failed to parse response") 198 | } 199 | return data 200 | } 201 | 202 | /// Returns a public url for an asset. 203 | /// - Parameters: 204 | /// - path: The file path to the asset. For example `folder/image.png`. 205 | /// - download: Whether the asset should be downloaded. 206 | /// - fileName: If specified, the file name for the asset that is downloaded. 207 | /// - options: Transform the asset before retrieving it on the client. 208 | public func getPublicURL( 209 | path: String, 210 | download: Bool = false, 211 | fileName: String = "", 212 | options: TransformOptions? = nil 213 | ) throws -> URL { 214 | var queryItems: [URLQueryItem] = [] 215 | 216 | guard var components = URLComponents(string: url) else { 217 | throw StorageError(message: "badURL") 218 | } 219 | 220 | if download { 221 | queryItems.append(URLQueryItem(name: "download", value: fileName)) 222 | } 223 | 224 | if let optionsQueryItems = options?.queryItems { 225 | queryItems.append(contentsOf: optionsQueryItems) 226 | } 227 | 228 | let renderPath = options != nil ? "render/image" : "object" 229 | 230 | components.path += "/\(renderPath)/public/\(bucketId)/\(path)" 231 | components.queryItems = !queryItems.isEmpty ? queryItems : nil 232 | 233 | guard let generatedUrl = components.url else { 234 | throw StorageError(message: "badUrl") 235 | } 236 | 237 | return generatedUrl 238 | } 239 | 240 | @available(*, deprecated, renamed: "getPublicURL") 241 | public func getPublicUrl( 242 | path: String, 243 | download: Bool = false, 244 | fileName: String = "", 245 | options: TransformOptions? = nil 246 | ) throws -> URL { 247 | try getPublicURL(path: path, download: download, fileName: fileName, options: options) 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /Sources/SupabaseStorage/StorageHTTPClient.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | #if canImport(FoundationNetworking) 4 | import FoundationNetworking 5 | #endif 6 | 7 | public protocol StorageHTTPClient { 8 | func fetch(_ request: URLRequest) async throws -> (Data, HTTPURLResponse) 9 | func upload(_ request: URLRequest, from data: Data) async throws -> (Data, HTTPURLResponse) 10 | } 11 | 12 | public struct DefaultStorageHTTPClient: StorageHTTPClient { 13 | public init() {} 14 | 15 | public func fetch(_ request: URLRequest) async throws -> (Data, HTTPURLResponse) { 16 | try await withCheckedThrowingContinuation { continuation in 17 | let dataTask = URLSession.shared.dataTask(with: request) { data, response, error in 18 | if let error = error { 19 | continuation.resume(throwing: error) 20 | return 21 | } 22 | 23 | guard 24 | let data = data, 25 | let httpResponse = response as? HTTPURLResponse 26 | else { 27 | continuation.resume(throwing: URLError(.badServerResponse)) 28 | return 29 | } 30 | 31 | continuation.resume(returning: (data, httpResponse)) 32 | } 33 | 34 | dataTask.resume() 35 | } 36 | } 37 | 38 | public func upload( 39 | _ request: URLRequest, 40 | from data: Data 41 | ) async throws -> (Data, HTTPURLResponse) { 42 | try await withCheckedThrowingContinuation { continuation in 43 | let task = URLSession.shared.uploadTask(with: request, from: data) { data, response, error in 44 | if let error = error { 45 | continuation.resume(throwing: error) 46 | return 47 | } 48 | 49 | guard 50 | let data = data, 51 | let httpResponse = response as? HTTPURLResponse 52 | else { 53 | continuation.resume(throwing: URLError(.badServerResponse)) 54 | return 55 | } 56 | 57 | continuation.resume(returning: (data, httpResponse)) 58 | } 59 | task.resume() 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/SupabaseStorage/SupabaseStorage.swift: -------------------------------------------------------------------------------- 1 | public class SupabaseStorageClient: StorageBucketApi { 2 | /// Storage Client initializer 3 | /// - Parameters: 4 | /// - url: Storage HTTP URL 5 | /// - headers: HTTP headers. 6 | override public init(url: String, headers: [String: String], http: StorageHTTPClient? = nil) { 7 | super.init(url: url, headers: headers, http: http ?? DefaultStorageHTTPClient()) 8 | } 9 | 10 | /// Perform file operation in a bucket. 11 | /// - Parameter id: The bucket id to operate on. 12 | /// - Returns: StorageFileApi object 13 | public func from(id: String) -> StorageFileApi { 14 | StorageFileApi(url: url, headers: headers, bucketId: id, http: http) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/SupabaseStorage/TransformOptions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct TransformOptions { 4 | public var width: Int? 5 | public var height: Int? 6 | public var resize: String? 7 | public var quality: Int? 8 | public var format: String? 9 | 10 | public init( 11 | width: Int? = nil, 12 | height: Int? = nil, 13 | resize: String? = "cover", 14 | quality: Int? = 80, 15 | format: String? = "origin" 16 | ) { 17 | self.width = width 18 | self.height = height 19 | self.resize = resize 20 | self.quality = quality 21 | self.format = format 22 | } 23 | 24 | var queryItems: [URLQueryItem] { 25 | var items = [URLQueryItem]() 26 | 27 | if let width = width { 28 | items.append(URLQueryItem(name: "width", value: String(width))) 29 | } 30 | 31 | if let height = height { 32 | items.append(URLQueryItem(name: "height", value: String(height))) 33 | } 34 | 35 | if let resize = resize { 36 | items.append(URLQueryItem(name: "resize", value: resize)) 37 | } 38 | 39 | if let quality = quality { 40 | items.append(URLQueryItem(name: "quality", value: String(quality))) 41 | } 42 | 43 | if let format = format { 44 | items.append(URLQueryItem(name: "format", value: format)) 45 | } 46 | 47 | return items 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Tests/SupabaseStorageTests/SupabaseStorageTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | 4 | @testable import SupabaseStorage 5 | 6 | #if canImport(FoundationNetworking) 7 | import FoundationNetworking 8 | #endif 9 | 10 | final class SupabaseStorageTests: XCTestCase { 11 | static var apiKey: String { 12 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" 13 | } 14 | 15 | static var supabaseURL: String { 16 | "http://localhost:54321/storage/v1" 17 | } 18 | 19 | let bucket = "public" 20 | 21 | let storage = SupabaseStorageClient( 22 | url: supabaseURL, 23 | headers: [ 24 | "Authorization": "Bearer \(apiKey)", 25 | "apikey": apiKey, 26 | ] 27 | ) 28 | 29 | let uploadData = try! Data( 30 | contentsOf: URL( 31 | string: "https://raw.githubusercontent.com/supabase-community/storage-swift/main/README.md" 32 | )! 33 | ) 34 | 35 | override func setUp() async throws { 36 | try await super.setUp() 37 | _ = try? await storage.emptyBucket(id: bucket) 38 | _ = try? await storage.deleteBucket(id: bucket) 39 | 40 | _ = try await storage.createBucket(id: bucket, options: BucketOptions(public: true)) 41 | } 42 | 43 | func testListBuckets() async throws { 44 | let buckets = try await storage.listBuckets() 45 | XCTAssertEqual(buckets.map(\.name), [bucket]) 46 | } 47 | 48 | func testFileIntegration() async throws { 49 | try await uploadTestData() 50 | 51 | let files = try await storage.from(id: bucket).list() 52 | XCTAssertEqual(files.map(\.name), ["README.md"]) 53 | 54 | let downloadedData = try await storage.from(id: bucket).download(path: "README.md") 55 | XCTAssertEqual(downloadedData, uploadData) 56 | 57 | let removedFiles = try await storage.from(id: bucket).remove(paths: ["README.md"]) 58 | XCTAssertEqual(removedFiles.map(\.name), ["README.md"]) 59 | } 60 | 61 | func testGetPublicURL() async throws { 62 | try await uploadTestData() 63 | 64 | let path = "README.md" 65 | 66 | let baseUrl = try storage.from(id: bucket).getPublicURL(path: path) 67 | XCTAssertEqual(baseUrl.absoluteString, "\(Self.supabaseURL)/object/public/\(bucket)/\(path)") 68 | 69 | let baseUrlWithDownload = try storage.from(id: bucket).getPublicURL(path: path, download: true) 70 | XCTAssertEqual( 71 | baseUrlWithDownload.absoluteString, 72 | "\(Self.supabaseURL)/object/public/\(bucket)/\(path)?download=") 73 | 74 | let baseUrlWithDownloadAndFileName = try storage.from(id: bucket).getPublicURL( 75 | path: path, download: true, fileName: "test") 76 | XCTAssertEqual( 77 | baseUrlWithDownloadAndFileName.absoluteString, 78 | "\(Self.supabaseURL)/object/public/\(bucket)/\(path)?download=test") 79 | 80 | let baseUrlWithAllOptions = try storage.from(id: bucket).getPublicURL( 81 | path: path, download: true, fileName: "test", 82 | options: TransformOptions(width: 300, height: 300)) 83 | XCTAssertEqual( 84 | baseUrlWithAllOptions.absoluteString, 85 | "\(Self.supabaseURL)/render/image/public/\(bucket)/\(path)?download=test&width=300&height=300&resize=cover&quality=80&format=origin" 86 | ) 87 | } 88 | 89 | private func uploadTestData() async throws { 90 | let file = File( 91 | name: "README.md", data: uploadData, fileName: "README.md", contentType: "text/html") 92 | _ = try await storage.from(id: bucket).upload( 93 | path: "README.md", file: file, fileOptions: FileOptions(cacheControl: "3600") 94 | ) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /supabase/.gitignore: -------------------------------------------------------------------------------- 1 | # Supabase 2 | .branches 3 | .temp 4 | -------------------------------------------------------------------------------- /supabase/config.toml: -------------------------------------------------------------------------------- 1 | # A string used to distinguish different Supabase projects on the same host. Defaults to the working 2 | # directory name when running `supabase init`. 3 | project_id = "storage-swift" 4 | 5 | [api] 6 | # Port to use for the API URL. 7 | port = 54321 8 | # Schemas to expose in your API. Tables, views and stored procedures in this schema will get API 9 | # endpoints. public and storage are always included. 10 | schemas = ["public", "storage", "graphql_public"] 11 | # Extra schemas to add to the search_path of every request. public is always included. 12 | extra_search_path = ["public", "extensions"] 13 | # The maximum number of rows returns from a view, table, or stored procedure. Limits payload size 14 | # for accidental or malicious requests. 15 | max_rows = 1000 16 | 17 | [db] 18 | # Port to use for the local database URL. 19 | port = 54322 20 | # The database major version to use. This has to be the same as your remote database's. Run `SHOW 21 | # server_version;` on the remote database to check. 22 | major_version = 15 23 | 24 | [studio] 25 | # Port to use for Supabase Studio. 26 | port = 54323 27 | 28 | # Email testing server. Emails sent with the local dev setup are not actually sent - rather, they 29 | # are monitored, and you can view the emails that would have been sent from the web interface. 30 | [inbucket] 31 | # Port to use for the email testing server web interface. 32 | port = 54324 33 | smtp_port = 54325 34 | pop3_port = 54326 35 | 36 | [storage] 37 | # The maximum file size allowed (e.g. "5MB", "500KB"). 38 | file_size_limit = "50MiB" 39 | 40 | [auth] 41 | # The base URL of your website. Used as an allow-list for redirects and for constructing URLs used 42 | # in emails. 43 | site_url = "http://localhost:3000" 44 | # A list of *exact* URLs that auth providers are permitted to redirect to post authentication. 45 | additional_redirect_urls = ["https://localhost:3000"] 46 | # How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 seconds (one 47 | # week). 48 | jwt_expiry = 3600 49 | # Allow/disallow new user signups to your project. 50 | enable_signup = true 51 | 52 | [auth.email] 53 | # Allow/disallow new user signups via email to your project. 54 | enable_signup = true 55 | # If enabled, a user will be required to confirm any email change on both the old, and new email 56 | # addresses. If disabled, only the new email is required to confirm. 57 | double_confirm_changes = true 58 | # If enabled, users need to confirm their email address before signing in. 59 | enable_confirmations = false 60 | 61 | # Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, 62 | # `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin`, `notion`, `twitch`, 63 | # `twitter`, `slack`, `spotify`, `workos`, `zoom`. 64 | [auth.external.apple] 65 | enabled = false 66 | client_id = "" 67 | secret = "" 68 | # Overrides the default auth redirectUrl. 69 | redirect_uri = "" 70 | # Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, 71 | # or any other third-party OIDC providers. 72 | url = "" 73 | 74 | [analytics] 75 | enabled = false 76 | port = 54327 77 | vector_port = 54328 78 | # Setup BigQuery project to enable log viewer on local development stack. 79 | # See: https://supabase.com/docs/guides/getting-started/local-development#enabling-local-logging 80 | gcp_project_id = "" 81 | gcp_project_number = "" 82 | gcp_jwt_path = "supabase/gcloud.json" 83 | -------------------------------------------------------------------------------- /supabase/functions/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["denoland.vscode-deno"] 3 | } 4 | -------------------------------------------------------------------------------- /supabase/functions/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.lint": true, 4 | "editor.defaultFormatter": "denoland.vscode-deno" 5 | } 6 | -------------------------------------------------------------------------------- /supabase/migrations/20230608140749_storage_permissions.sql: -------------------------------------------------------------------------------- 1 | create policy "Allow all" on "storage"."objects" as PERMISSIVE 2 | for all to public 3 | using (true) 4 | with check (true); 5 | 6 | create policy "Allow all" on "storage"."buckets" as PERMISSIVE 7 | for all to public 8 | using (true) 9 | with check (true); 10 | 11 | -------------------------------------------------------------------------------- /supabase/seed.sql: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/storage-swift/efb9c7c225cdc4a82787c3984ff66d2bceb97167/supabase/seed.sql --------------------------------------------------------------------------------