Files
andros 225e9a58ec Add GitHub, Codeberg, WebDAV upload backends; fix vfile download bug
- 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)
2026-04-19 11:13:04 +02:00

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) }
}
}