Skip to content

Commit f754704

Browse files
committed
Add file uploads
1 parent 9523630 commit f754704

File tree

3 files changed

+205
-0
lines changed

3 files changed

+205
-0
lines changed

src/Models/File.swift

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import Foundation
2+
import MetaCodable
3+
import HelperCoders
4+
5+
@Codable @CodingKeys(.snake_case) public struct File: Equatable, Hashable, Sendable {
6+
public struct Upload: Equatable, Hashable, Sendable {
7+
/// The name of the file
8+
public var name: String
9+
/// The contents of the file
10+
public var contents: Data
11+
/// The mime type of the file
12+
public var contentType: String
13+
14+
/// Creates a new file upload
15+
///
16+
/// - Parameter name: The name of the file
17+
/// - Parameter contents: The contents of the file
18+
/// - Parameter contentType: The mime type of the file
19+
public init(name: String, contents: Data, contentType: String = "application/octet-stream") {
20+
self.name = name
21+
self.contents = contents
22+
self.contentType = contentType
23+
}
24+
}
25+
26+
/// The intended purpose of the file.
27+
public enum Purpose: String, CaseIterable, Equatable, Hashable, Codable, Sendable {
28+
/// Used in the Assistants API
29+
case assistants
30+
/// Used in the Assistants API
31+
case assistantsOutput = "assistants_output"
32+
/// Used in the Batch API
33+
case batch
34+
/// Used for fine-tuning
35+
case fineTune = "fine-tune"
36+
/// Images used for vision fine-tuning
37+
case vision
38+
/// Flexible file type for any purpose
39+
case userData = "user_data"
40+
/// Used for eval data sets
41+
case evals
42+
}
43+
44+
/// The current status of the file
45+
public enum Status: String, CaseIterable, Equatable, Codable, Sendable {
46+
case error
47+
case uploaded
48+
case processed
49+
}
50+
51+
/// The file identifier, which can be referenced in the API endpoints.
52+
public var id: String
53+
54+
/// The intended purpose of the file.
55+
public var purpose: Purpose
56+
57+
/// The name of the file.
58+
public var filename: String
59+
60+
/// The size of the file, in bytes.
61+
public var bytes: Int
62+
63+
/// The `Date` when the file was created.
64+
@CodedBy(Since1970DateCoder())
65+
public var createdAt: Date
66+
67+
/// The `Date` when the file will expire.
68+
@CodedBy(Since1970DateCoder())
69+
public var expiresAt: Date?
70+
71+
/// The current status of the file
72+
public var status: Status
73+
74+
/// Create a new `File` instance.
75+
///
76+
/// - Parameter id: The file identifier, which can be referenced in the API endpoints.
77+
/// - Parameter purpose: The intended purpose of the file.
78+
/// - Parameter filename: The name of the file.
79+
/// - Parameter bytes: The size of the file, in bytes.
80+
/// - Parameter createdAt: The `Date` when the file was created.
81+
/// - Parameter expiresAt: The `Date` when the file will expire.
82+
/// - Parameter status: The current status of the file
83+
public init(id: String, purpose: Purpose, filename: String, bytes: Int, createdAt: Date, expiresAt: Date? = nil, status: Status) {
84+
self.id = id
85+
self.bytes = bytes
86+
self.status = status
87+
self.purpose = purpose
88+
self.filename = filename
89+
self.createdAt = createdAt
90+
self.expiresAt = expiresAt
91+
}
92+
}
93+
94+
// MARK: - Creation helpers
95+
96+
public extension File.Upload {
97+
/// Creates a file upload from a local file or URL.
98+
///
99+
/// - Parameter url: The URL of the file to upload.
100+
/// - Parameter name: The name of the file.
101+
static func url(_ url: URL, name: String? = nil) async throws -> File.Upload {
102+
let name = name ?? url.lastPathComponent == "/" ? "unknown_file" : url.lastPathComponent
103+
104+
return try File.Upload(name: name, contents: Data(contentsOf: url))
105+
}
106+
107+
/// Creates a file upload from the given data.
108+
///
109+
/// - Parameter name: The name of the file.
110+
/// - Parameter contents: The contents of the file.
111+
/// - Parameter contentType: The mime type of the file.
112+
static func file(name: String, contents: Data, contentType: String = "application/octet-stream") -> File.Upload {
113+
return File.Upload(name: name, contents: contents, contentType: contentType)
114+
}
115+
}
116+
117+
// MARK: - Private helpers
118+
119+
extension File.Upload {
120+
func toFormEntry(paramName: String = "file") -> FormData.Entry {
121+
.file(paramName: paramName, fileName: name, fileData: contents, contentType: contentType)
122+
}
123+
}

src/OpenAI.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,20 @@ public final class ResponsesAPI: Sendable {
146146

147147
return try decoder.decode(Input.ItemList.self, from: await send(request: req))
148148
}
149+
150+
public func upload(file: File.Upload, purpose: File.Purpose = .userData) async throws -> File {
151+
let form = FormData(
152+
boundary: UUID().uuidString,
153+
entries: [file.toFormEntry(), .string(paramName: "purpose", value: purpose.rawValue)]
154+
)
155+
156+
var req = request
157+
req.httpMethod = "POST"
158+
req.attach(formData: form)
159+
req.url!.append(path: "v1/files")
160+
161+
return try decoder.decode(File.self, from: await send(request: req))
162+
}
149163
}
150164

151165
// MARK: - Private helpers

src/Support/MultiPartData.swift

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import Foundation
2+
3+
final class FormData {
4+
enum Entry {
5+
case string(paramName: String, value: Any)
6+
7+
case file(paramName: String, fileName: String, fileData: Data, contentType: String)
8+
9+
var data: Data {
10+
var body = Data()
11+
12+
switch self {
13+
case let .string(paramName, value):
14+
body.append("Content-Disposition: form-data; name=\"\(paramName)\"\r\n\r\n")
15+
body.append("\(value)\r\n")
16+
17+
case let .file(paramName, fileName, fileData, contentType):
18+
body.append("Content-Disposition: form-data; name=\"\(paramName)\"; filename=\"\(fileName)\"\r\n")
19+
body.append("Content-Type: \(contentType)\r\n\r\n")
20+
body.append(fileData)
21+
body.append("\r\n")
22+
}
23+
24+
return body
25+
}
26+
}
27+
28+
let boundary: String
29+
let entries: [Entry]
30+
31+
init(boundary: String, entries: [Entry]) {
32+
self.entries = entries
33+
self.boundary = boundary
34+
}
35+
36+
var data: Data {
37+
var httpData = entries.map(\.data).reduce(Data()) { result, element in
38+
var result = result
39+
40+
result.append("--\(boundary)\r\n")
41+
result.append(element)
42+
43+
return result
44+
}
45+
46+
httpData.append("--\(boundary)--\r\n")
47+
return httpData
48+
}
49+
50+
var header: String {
51+
return "multipart/form-data; boundary=\(boundary)"
52+
}
53+
}
54+
55+
extension URLRequest {
56+
mutating func attach(formData form: FormData) {
57+
httpBody = form.data
58+
addValue(form.header, forHTTPHeaderField: "Content-Type")
59+
}
60+
}
61+
62+
fileprivate extension Data {
63+
mutating func append(_ string: String) {
64+
if let data = string.data(using: .utf8, allowLossyConversion: true) {
65+
append(data)
66+
}
67+
}
68+
}

0 commit comments

Comments
 (0)