From 871b04159aa47c0b0e62e2520c2f30e81f0f024b Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Thu, 16 Jul 2026 01:06:46 -0500 Subject: fix: download artifacts through the API instead of handing them to Safari MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tapping download opened Artifact.url in the browser, which answered with "Authorization header is required". That URL is not a web page: git.sr.ht resolves it to /query/artifact//, which demands a bearer token. Safari has none and no way to get one, so the download could never have worked — this predates the upload work. Fetch it with the client that already holds the token and hand the user the file through a share sheet. fetchData mirrors fetchText, including its host guard, so an authenticated request still cannot be aimed anywhere but *.sr.ht over https. Also guards zero-byte uploads. sr.ht streams into S3, which rejects a zero-part multipart completion with "MalformedXML: UnknownError" — an error that says nothing about the cause and cost a round of testing to identify. Empty files are now refused by name before the request is made. --- Hutch/Views/Repositories/ArtifactsView.swift | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) (limited to 'Hutch/Views/Repositories/ArtifactsView.swift') diff --git a/Hutch/Views/Repositories/ArtifactsView.swift b/Hutch/Views/Repositories/ArtifactsView.swift index 8843c6f..1d9fc6c 100644 --- a/Hutch/Views/Repositories/ArtifactsView.swift +++ b/Hutch/Views/Repositories/ArtifactsView.swift @@ -12,11 +12,11 @@ struct ArtifactsView: View { /// Passed in rather than recomputed: RepositoryDetailView already owns this /// check and gates its other management surfaces on it. var canManage: Bool = false - @Environment(\.openURL) private var openURL @State private var uploadTargetRef: String? @State private var isImporting = false @State private var pendingDeletion: ArtifactInfo? + @State private var downloadedFile: DownloadedArtifact? private var isOwnedByCurrentUser: Bool { canManage } @@ -63,7 +63,14 @@ struct ArtifactsView: View { Section { ForEach(refArtifacts.artifacts) { artifact in ArtifactRow(artifact: artifact) { - openURL(artifact.url) + Task { + // Artifact.url is on the API origin and 401s + // without a bearer token, so it cannot be handed + // to a browser. Fetch it and share the file. + if let fileURL = await viewModel.downloadArtifact(artifact) { + downloadedFile = DownloadedArtifact(url: fileURL) + } + } } // See MailingListListView: a full-swipe destructive // action animates the row out before the confirmation. @@ -131,6 +138,9 @@ struct ArtifactsView: View { .themedList() .listStyle(.insetGrouped) .srhtErrorBanner(error: $vm.error) + .sheet(item: $downloadedFile) { download in + FileContentShareSheet(activityItems: [download.url]) + } .task { // Tags drive the picker above and are not otherwise needed by this tab. if isOwnedByCurrentUser, viewModel.tags.isEmpty { @@ -184,6 +194,13 @@ struct ArtifactsView: View { } } +/// Wraps the downloaded file for `.sheet(item:)`. URL is not Identifiable, and +/// conforming a stdlib type retroactively is worse than a four-line struct. +private struct DownloadedArtifact: Identifiable { + let id = UUID() + let url: URL +} + private struct ArtifactRow: View { let artifact: ArtifactInfo let onDownload: () -> Void -- cgit v1.2.3