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

supabase / supabase-swift / 29018885975

09 Jul 2026 12:40PM UTC coverage: 77.87% (-5.2%) from 83.062%
29018885975

Pull #1099

github

web-flow
Merge 2bab4f734 into 27f31bfb7
Pull Request #1099: feat(storage): OpenAPI codegen tool + generated Storage HTTP client

428 of 1233 new or added lines in 18 files covered. (34.71%)

6 existing lines in 2 files now uncovered.

8322 of 10687 relevant lines covered (77.87%)

33.72 hits per line

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

61.26
/Sources/HTTPRuntime/MultipartFormData.swift
1
//
2
//  MultipartFormData.swift
3
//  HTTPRuntime
4
//
5
//  Created by Guilherme Souza on 08/07/26.
6
//
7
package import Foundation
8

9
/// Builds a `multipart/form-data` body by streaming its parts onto a temporary
10
/// file, so large file parts never load fully into memory. The resulting file
11
/// is then uploaded with `URLSession.upload(for:fromFile:)`.
12
///
13
/// This is the runtime's answer to "streaming multipart upload of large files
14
/// without loading into memory": file parts are copied chunk-by-chunk to the
15
/// staging file.
16
package struct MultipartFormData: Sendable {
17
  let boundary: String
18
  private var parts: [Part] = []
1✔
19

20
  package var contentType: String { "multipart/form-data; boundary=\(boundary)" }
1✔
21

22
  enum Part {
23
    case text(name: String, value: String)
24
    case data(name: String, data: Data, fileName: String?, mimeType: String?)
25
    case file(name: String, fileURL: URL, fileName: String?, mimeType: String?)
26
  }
27

28
  package init(boundary: String = "----sb-\(UUID().uuidString)") {
1✔
29
    self.boundary = boundary
1✔
30
  }
1✔
31

32
  /// Add a text field to the multipart payload
33
  package func addText(name: String, value: String) -> MultipartFormData {
1✔
34
    var builder = self
1✔
35
    builder.parts.append(.text(name: name, value: value))
1✔
36
    return builder
1✔
37
  }
1✔
38

39
  /// Add an optional text field (only adds if value is non-nil)
NEW
40
  package func addOptionalText(name: String, value: String?) -> MultipartFormData {
×
NEW
41
    if let value = value {
×
NEW
42
      return addText(name: name, value: value)
×
NEW
43
    }
×
NEW
44
    return self
×
NEW
45
  }
×
46

47
  /// Add a data field to the multipart payload.
48
  package func addData(
49
    name: String,
50
    data: Data,
51
    fileName: String? = nil,
52
    mimeType: String? = nil
NEW
53
  ) -> MultipartFormData {
×
NEW
54
    var builder = self
×
NEW
55
    builder.parts.append(.data(name: name, data: data, fileName: fileName, mimeType: mimeType))
×
NEW
56
    return builder
×
NEW
57
  }
×
58

59
  /// Add a file field to the multipart payload (loads entire file into memory)
60
  package func addFile(
61
    name: String,
62
    fileURL: URL,
63
    fileName: String? = nil,
64
    mimeType: String? = nil
65
  ) -> MultipartFormData {
1✔
66
    var builder = self
1✔
67
    builder.parts.append(
1✔
68
      .file(name: name, fileURL: fileURL, fileName: fileName, mimeType: mimeType))
1✔
69
    return builder
1✔
70
  }
1✔
71

72
  /// Build the multipart payload in memory
73
  /// - Note: Only suitable for small payloads. Use `buildToTempFile()` for large files.
74
  /// - Returns: Complete multipart body data
NEW
75
  package func buildInMemory() throws -> Data {
×
NEW
76
    guard !parts.isEmpty else { return Data() }
×
NEW
77

×
NEW
78
    var body = Data()
×
NEW
79

×
NEW
80
    for part in parts {
×
NEW
81
      switch part {
×
NEW
82
      case .text(let name, let value):
×
NEW
83
        body.append(textPart(name: name, value: value))
×
NEW
84
      case .data(let name, let data, let fileName, let mimeType):
×
NEW
85
        body.append(dataPart(name: name, data: data, fileName: fileName, mimeType: mimeType))
×
NEW
86
      case .file(let name, let fileURL, let fileName, let mimeType):
×
NEW
87
        body.append(
×
NEW
88
          try filePart(name: name, fileURL: fileURL, fileName: fileName, mimeType: mimeType)
×
NEW
89
        )
×
NEW
90
      }
×
NEW
91
    }
×
NEW
92

×
NEW
93
    body.append(closingBoundary())
×
NEW
94
    return body
×
NEW
95
  }
×
96

97
  /// Build the multipart payload to a temporary file
98
  /// - Note: Streams file contents to avoid memory pressure on large files
99
  /// - Returns: URL of temporary file containing multipart body
100
  package func buildToTempFile() throws -> URL {
1✔
101
    let tempDir = FileManager.default.temporaryDirectory
1✔
102
    let tempFile = tempDir.appendingPathComponent(UUID().uuidString)
1✔
103

1✔
104
    // Create temp file
1✔
105
    guard FileManager.default.createFile(atPath: tempFile.path, contents: nil) else {
1✔
NEW
106
      throw MultipartFormatDataError.createTempFileFailed
×
107
    }
1✔
108

1✔
109
    var didReturnTempFile = false
1✔
110
    defer {
1✔
111
      if !didReturnTempFile {
1✔
112
        try? FileManager.default.removeItem(at: tempFile)
1✔
113
      }
1✔
114
    }
1✔
115

1✔
116
    guard let handle = FileHandle(forWritingAtPath: tempFile.path) else {
1✔
NEW
117
      throw MultipartFormatDataError.openTempFileFailed
×
118
    }
1✔
119

1✔
120
    defer { try? handle.close() }
1✔
121

1✔
122
    guard !parts.isEmpty else {
1✔
NEW
123
      didReturnTempFile = true
×
NEW
124
      return tempFile
×
125
    }
1✔
126

1✔
127
    // Write parts
1✔
128
    for part in parts {
2✔
129
      switch part {
2✔
130
      case .text(let name, let value):
2✔
131
        let data = textPart(name: name, value: value)
1✔
132
        try handle.write(contentsOf: data)
1✔
133

2✔
134
      case .data(let name, let data, let fileName, let mimeType):
2✔
NEW
135
        try handle.write(contentsOf: partHeader(name: name, fileName: fileName, mimeType: mimeType))
×
NEW
136
        try handle.write(contentsOf: data)
×
NEW
137
        try handle.write(contentsOf: Data("\r\n".utf8))
×
138

2✔
139
      case .file(let name, let fileURL, let fileName, let mimeType):
2✔
140
        // Write file part header
1✔
141
        let header = partHeader(
1✔
142
          name: name,
1✔
143
          fileName: fileName ?? fileURL.lastPathComponent,
1✔
144
          mimeType: mimeType
1✔
145
        )
1✔
146
        try handle.write(contentsOf: header)
1✔
147

1✔
148
        // Stream file contents in chunks
1✔
149
        try streamFile(from: fileURL, to: handle)
1✔
150

1✔
151
        // Write trailing newline
1✔
152
        try handle.write(contentsOf: Data("\r\n".utf8))
1✔
153
      }
2✔
154
    }
2✔
155

1✔
156
    // Write closing boundary
1✔
157
    try handle.write(contentsOf: closingBoundary())
1✔
158

1✔
159
    didReturnTempFile = true
1✔
160
    return tempFile
1✔
161
  }
1✔
162

163
  // MARK: - Private Helpers
164

165
  private func textPart(name: String, value: String) -> Data {
1✔
166
    var data = Data()
1✔
167
    data.append(Data("--\(boundary)\r\n".utf8))
1✔
168
    data.append(Data("Content-Disposition: form-data; name=\"\(name)\"\r\n".utf8))
1✔
169
    data.append(Data("\r\n".utf8))
1✔
170
    data.append(Data("\(value)\r\n".utf8))
1✔
171
    return data
1✔
172
  }
1✔
173

174
  private func dataPart(
175
    name: String,
176
    data: Data,
177
    fileName: String?,
178
    mimeType: String?
NEW
179
  ) -> Data {
×
NEW
180
    var partData = Data()
×
NEW
181
    partData.append(partHeader(name: name, fileName: fileName, mimeType: mimeType))
×
NEW
182
    partData.append(data)
×
NEW
183
    partData.append(Data("\r\n".utf8))
×
NEW
184
    return partData
×
NEW
185
  }
×
186

187
  private func filePart(
188
    name: String,
189
    fileURL: URL,
190
    fileName: String?,
191
    mimeType: String?
NEW
192
  ) throws -> Data {
×
NEW
193
    var data = Data()
×
NEW
194

×
NEW
195
    data.append(
×
NEW
196
      partHeader(name: name, fileName: fileName ?? fileURL.lastPathComponent, mimeType: mimeType)
×
NEW
197
    )
×
NEW
198

×
NEW
199
    let fileData = try Data(contentsOf: fileURL)
×
NEW
200
    data.append(fileData)
×
NEW
201

×
NEW
202
    data.append(Data("\r\n".utf8))
×
NEW
203

×
NEW
204
    return data
×
NEW
205
  }
×
206

207
  private func partHeader(name: String, fileName: String?, mimeType: String?) -> Data {
1✔
208
    var header = Data()
1✔
209
    header.append(Data("--\(boundary)\r\n".utf8))
1✔
210
    var disposition = "Content-Disposition: form-data; name=\"\(name)\""
1✔
211
    if let fileName {
1✔
212
      disposition.append("; filename=\"\(fileName)\"")
1✔
213
    }
1✔
214
    header.append(Data("\(disposition)\r\n".utf8))
1✔
215

1✔
216
    if let mimeType = mimeType {
1✔
217
      header.append(Data("Content-Type: \(mimeType)\r\n".utf8))
1✔
218
    }
1✔
219

1✔
220
    header.append(Data("\r\n".utf8))
1✔
221
    return header
1✔
222
  }
1✔
223

224
  private func closingBoundary() -> Data {
1✔
225
    return Data("--\(boundary)--\r\n".utf8)
1✔
226
  }
1✔
227

228
  private func streamFile(from url: URL, to handle: FileHandle) throws {
1✔
229
    guard let input = InputStream(url: url) else {
1✔
NEW
230
      throw MultipartFormatDataError.openInputStreamFailed
×
231
    }
1✔
232

1✔
233
    input.open()
1✔
234
    defer { input.close() }
1✔
235

1✔
236
    let bufferSize = 64 * 1024  // 64KB chunks
1✔
237
    var buffer = [UInt8](repeating: 0, count: bufferSize)
1✔
238

1✔
239
    while input.hasBytesAvailable {
6✔
240
      let bytesRead = input.read(&buffer, maxLength: bufferSize)
5✔
241
      if bytesRead > 0 {
5✔
242
        let data = Data(bytes: buffer, count: bytesRead)
4✔
243
        try handle.write(contentsOf: data)
4✔
244
      } else if bytesRead < 0 {
4✔
NEW
245
        throw MultipartFormatDataError.readInputStreamFailed(underlying: input.streamError)
×
246
      }
5✔
247
    }
5✔
248
  }
1✔
249
}
250

251
// MARK: - Errors
252

253
/// Errors that can occur during multipart building operations.
254
package enum MultipartFormatDataError: LocalizedError {
255
  case createTempFileFailed
256
  case openTempFileFailed
257
  case openInputStreamFailed
258
  case readInputStreamFailed(underlying: (any Error)?)
259

NEW
260
  package var errorDescription: String? {
×
NEW
261
    switch self {
×
NEW
262
    case .createTempFileFailed:
×
NEW
263
      return "Failed to create temp file"
×
NEW
264
    case .openTempFileFailed:
×
NEW
265
      return "Failed to create temp file"
×
NEW
266
    case .openInputStreamFailed:
×
NEW
267
      return "Failed to open file for reading"
×
NEW
268
    case .readInputStreamFailed:
×
NEW
269
      return "Error reading file"
×
NEW
270
    }
×
NEW
271
  }
×
272
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc