import Foundation
extension IPFSAPI {
/// Grant → raw-byte POST to the node → poll until the upload settles. Returns the
/// completed upload (with its `cid` and `pinId`); `failed`/`expired` throw
/// `IrdError.uploadFailed`.
public func addFile(data: Data, name: String? = nil, meta: [String: String]? = nil,
format: UploadFormat? = nil, options: PollOptions = .uploads) async throws -> Upload {
let grant = try await grant(name: name, meta: meta, format: format)
try await client.uploadBytes(to: grant.url, token: grant.token, data: data, format: format)
return try await settle(grant, options: options)
}
/// Same, streaming the file from disk (`URLSession.upload(for:fromFile:)`), so a
/// large file never sits in memory.
public func addFile(url: URL, name: String? = nil, meta: [String: String]? = nil,
format: UploadFormat? = nil, options: PollOptions = .uploads) async throws -> Upload {
let grant = try await grant(name: name ?? url.lastPathComponent, meta: meta, format: format)
try await client.uploadBytes(to: grant.url, token: grant.token, file: url, format: format)
return try await settle(grant, options: options)
}
private func grant(name: String?, meta: [String: String]?, format: UploadFormat?) async throws -> (url: URL, token: String?, id: String) {
let body = (name == nil && meta == nil && format == nil) ? nil : StartUploadBody(name: name, meta: meta, format: format)
let upload = try await startUpload(body: body).upload
guard let url = URL(string: upload.url) else { throw IrdError.transport("upload grant carries an invalid URL: \(upload.url)") }
return (url, upload.token, upload.id)
}
private func settle(_ grant: (url: URL, token: String?, id: String), options: PollOptions) async throws -> Upload {
let upload = try await pollUntil(options, clock: client.configuration.clock,
step: { try await getUpload(id: grant.id).upload },
done: { [.completed, .failed, .expired].contains($0.status) })
if upload.status != .completed { throw IrdError.uploadFailed(upload) }
return upload
}
}
extension IrdClient {
/// POSTs raw bytes to an upload grant URL: `application/octet-stream`, the grant's own
/// bearer when non-nil, `?format=car` for a CAR archive. Non-2xx answers throw `.api`.
public func uploadBytes(to url: URL, token: String?, data: Data, format: UploadFormat? = nil) async throws {
let request = try uploadRequest(to: url, token: token, format: format)
var r = request
r.httpBody = data
r.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
let response = try await configuration.transport.send(r)
_ = try ResponseDecoder.decode(status: response.status, body: response.body)
}
/// Same, streaming `file` from disk through the transport.
public func uploadBytes(to url: URL, token: String?, file: URL, format: UploadFormat? = nil) async throws {
var request = try uploadRequest(to: url, token: token, format: format)
request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
let response = try await configuration.transport.upload(request, fromFile: file)
_ = try ResponseDecoder.decode(status: response.status, body: response.body)
}
private func uploadRequest(to url: URL, token: String?, format: UploadFormat?) throws -> URLRequest {
let query: [QueryItem] = format == .car ? [QueryItem("format", "car")] : []
guard let full = URL(string: url.absoluteString + RequestBuilder.queryString(query)) else {
throw IrdError.config("invalid upload URL \(url)")
}
return RequestBuilder.build(url: full, method: .post, token: token, body: nil,
headers: configuration.headers, timeout: configuration.timeout)
}
}