summaryrefslogtreecommitdiff
path: root/Hutch/Views/Repositories
diff options
context:
space:
mode:
Diffstat (limited to 'Hutch/Views/Repositories')
-rw-r--r--Hutch/Views/Repositories/ArtifactsView.swift21
-rw-r--r--Hutch/Views/Repositories/RepositoryDetailViewModel.swift31
2 files changed, 50 insertions, 2 deletions
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
diff --git a/Hutch/Views/Repositories/RepositoryDetailViewModel.swift b/Hutch/Views/Repositories/RepositoryDetailViewModel.swift
index dce39c1..9b6b942 100644
--- a/Hutch/Views/Repositories/RepositoryDetailViewModel.swift
+++ b/Hutch/Views/Repositories/RepositoryDetailViewModel.swift
@@ -542,6 +542,14 @@ final class RepositoryDetailViewModel {
return false
}
+ // sr.ht streams the upload into S3, which rejects a zero-part multipart
+ // completion with "MalformedXML" — an error that says nothing about the
+ // actual problem. Catch it here where we can name it.
+ guard !fileData.isEmpty else {
+ self.error = "\(fileURL.lastPathComponent) is empty. SourceHut rejects zero-byte artifacts."
+ return false
+ }
+
do {
_ = try await client.executeMultipart(
service: service,
@@ -567,6 +575,29 @@ final class RepositoryDetailViewModel {
}
}
+ /// Downloads an artifact and returns a local file URL to share.
+ ///
+ /// `Artifact.url` points at the API origin, not the web one, and returns an
+ /// auth error to anything without a bearer token — so it cannot be opened in
+ /// a browser. Fetch it here and hand the user the file instead.
+ func downloadArtifact(_ artifact: ArtifactInfo) async -> URL? {
+ guard !isMutatingArtifact else { return nil }
+ isMutatingArtifact = true
+ error = nil
+ defer { isMutatingArtifact = false }
+
+ do {
+ let data = try await client.fetchData(url: artifact.url)
+ let destination = FileManager.default.temporaryDirectory
+ .appendingPathComponent(artifact.filename)
+ try data.write(to: destination, options: .atomic)
+ return destination
+ } catch {
+ self.error = "Couldn't download \(artifact.filename). \(error.userFacingMessage)"
+ return nil
+ }
+ }
+
@discardableResult
func deleteArtifact(id: Int) async -> Bool {
guard !isMutatingArtifact else { return false }