225e9a58ec
- New FeedUploader protocol with VFileUploader, GitHubUploader, CodebergUploader, WebDAVUploader - GitHub/Codeberg: fetch current file SHA via API before PUT (handles create + update) - WebDAV: HTTP PUT with optional Basic Auth - ComposeViewModel: always downloads from publicFeedURL (vfile token URL is upload-only, GET returns 404) - SettingsViewModel: per-method config fields + derived public URL hints for GitHub/Codeberg - SettingsView: method picker with conditional sections; VFile shows signup link; SFTP/FTP excluded (no native iOS support)
61 lines
2.4 KiB
Swift
61 lines
2.4 KiB
Swift
import Foundation
|
|
|
|
/// Uploads via the Org Social vhost multipart endpoint.
|
|
///
|
|
/// The vfile token URL is sent as the `vfile` form field so the host knows
|
|
/// which file to replace. Only `POST /upload` is used; GET on a vfile URL
|
|
/// always returns 404 and must not be used for downloading.
|
|
public struct VFileUploader: FeedUploader {
|
|
|
|
private let tokenURL: URL
|
|
private let session: URLSession
|
|
|
|
public init(tokenURL: URL, session: URLSession = .shared) {
|
|
self.tokenURL = tokenURL
|
|
self.session = session
|
|
}
|
|
|
|
public func upload(content: String) async throws {
|
|
guard let scheme = tokenURL.scheme, let host = tokenURL.host else {
|
|
throw UploadError.invalidConfiguration("Cannot derive host from vfile token URL.")
|
|
}
|
|
let base = tokenURL.port.map { "\(scheme)://\(host):\($0)" } ?? "\(scheme)://\(host)"
|
|
guard let uploadURL = URL(string: "\(base)/upload") else {
|
|
throw UploadError.invalidConfiguration("Could not build upload URL.")
|
|
}
|
|
|
|
let boundary = "----OrgSocialBoundary\(UInt32.random(in: 0..<UInt32.max))"
|
|
var body = Data()
|
|
|
|
func append(_ s: String) { body.append(Data(s.utf8)) }
|
|
let crlf = "\r\n"
|
|
|
|
append("--\(boundary)\(crlf)")
|
|
append("Content-Disposition: form-data; name=\"vfile\"\(crlf)\(crlf)")
|
|
append(tokenURL.absoluteString)
|
|
append(crlf)
|
|
|
|
append("--\(boundary)\(crlf)")
|
|
append("Content-Disposition: form-data; name=\"file\"; filename=\"social.org\"\(crlf)")
|
|
append("Content-Type: text/plain; charset=utf-8\(crlf)\(crlf)")
|
|
body.append(Data(content.utf8))
|
|
append(crlf)
|
|
append("--\(boundary)--\(crlf)")
|
|
|
|
var request = URLRequest(url: uploadURL)
|
|
request.httpMethod = "POST"
|
|
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
|
|
request.httpBody = body
|
|
|
|
let (_, response) = try await perform(request)
|
|
if let http = response as? HTTPURLResponse, !(200..<300).contains(http.statusCode) {
|
|
throw UploadError.uploadFailed(statusCode: http.statusCode)
|
|
}
|
|
}
|
|
|
|
private func perform(_ request: URLRequest) async throws -> (Data, URLResponse) {
|
|
do { return try await session.data(for: request) }
|
|
catch { throw UploadError.networkError(underlying: error.localizedDescription) }
|
|
}
|
|
}
|