Probe archive download support on demand

The source and commit views hit the /raw archive endpoint with a
HEAD request on every page load to decide whether to show the
download UI. That endpoint runs `git archive` server-side.

Move detection to user interaction: the clone popover probes when
the Download tab is opened, and the commit view checks on click,
navigating on success or showing a message on failure.

Cache only successful results per URL so transient or moving-ref 404s
aren't latched for the session.
This commit is contained in:
Rūdolfs Ošiņš
2026-06-11 13:38:49 +02:00
parent 09cb50bbf1
commit 8e2148599e
3 changed files with 105 additions and 52 deletions
+15
View File
@@ -0,0 +1,15 @@
export const archiveUnsupportedMessage =
"The radicle-httpd on this node doesn't support archive downloads.";
const supportedUrls = new Set<string>();
export async function archiveDownloadSupported(url: string): Promise<boolean> {
if (supportedUrls.has(url)) {
return true;
}
const response = await fetch(url, { method: "HEAD" });
if (response.ok) {
supportedUrls.add(url);
}
return response.ok;
}
+44 -15
View File
@@ -1,6 +1,10 @@
<script lang="ts">
import type { BaseUrl, Commit, Repo } from "@http-client";
import {
archiveDownloadSupported,
archiveUnsupportedMessage,
} from "@app/lib/archive";
import { baseUrlToString, formatObjectId } from "@app/lib/utils";
import { renderCommitDescription } from "@app/lib/commit";
@@ -13,6 +17,7 @@
import JobCob from "@app/components/JobCob.svelte";
import Layout from "./Layout.svelte";
import Link from "@app/components/Link.svelte";
import Popover from "@app/components/Popover.svelte";
import Separator from "./Separator.svelte";
export let baseUrl: BaseUrl;
@@ -21,16 +26,20 @@
export let nodeId: string;
export let nodeAvatarUrl: string | undefined;
let enabledArchiveDownload = false;
let downloading = false;
void fetch(
`${baseUrlToString(baseUrl)}/raw/${repo.rid}/${commit.commit.id}.tar.gz`,
{
method: "HEAD",
},
).then(response => {
enabledArchiveDownload = response.ok;
});
async function downloadArchive(): Promise<boolean> {
const url = `${baseUrlToString(baseUrl)}/raw/${repo.rid}/${commit.commit.id}.tar.gz`;
try {
if (await archiveDownloadSupported(url)) {
window.location.href = url;
return true;
}
} catch {
return false;
}
return false;
}
$: header = commit.commit;
</script>
@@ -59,6 +68,10 @@
display: flex;
gap: 0.5rem;
}
.download-error {
font: var(--txt-body-m-regular);
width: 18rem;
}
</style>
<Layout {nodeId} {nodeAvatarUrl} {baseUrl} {repo}>
@@ -108,14 +121,30 @@
<Icon name="chevron-left-right" />
</Button>
</Link>
{#if enabledArchiveDownload}
<a
href={`${baseUrlToString(baseUrl)}/raw/${repo.rid}/${commit.commit.id}.tar.gz`}>
<Button variant="outline">
<Popover popoverPositionTop="2.5rem" popoverPositionRight="0">
<Button
slot="toggle"
let:toggle
let:expanded
on:click={async () => {
if (expanded) {
toggle();
} else if (!downloading) {
downloading = true;
const ok = await downloadArchive();
downloading = false;
if (!ok) {
toggle();
}
}
}}
variant="outline">
<Icon name="archive" />Download
</Button>
</a>
{/if}
<div slot="popover" class="download-error">
{archiveUnsupportedMessage}
</div>
</Popover>
</div>
</span>
<CommitAuthorship {header}>
+29 -20
View File
@@ -2,12 +2,17 @@
import type { BaseUrl } from "@http-client";
import config from "@app/lib/config";
import {
archiveDownloadSupported,
archiveUnsupportedMessage,
} from "@app/lib/archive";
import { parseRepositoryId } from "@app/lib/utils";
import Button from "@app/components/Button.svelte";
import Command from "@app/components/Command.svelte";
import ExternalLink from "@app/components/ExternalLink.svelte";
import Icon from "@app/components/Icon.svelte";
import Loading from "@app/components/Loading.svelte";
import Popover from "@app/components/Popover.svelte";
import Radio from "@app/components/Radio.svelte";
@@ -18,18 +23,15 @@
export let name: string;
export let currentRefname: string;
let enabledArchiveDownload = false;
void fetch(
`${baseUrlToString(baseUrl)}/raw/${id}/archive/${currentRefname}`,
{
method: "HEAD",
},
).then(response => {
enabledArchiveDownload = response.ok;
});
let activeTab: "radicle" | "git" | "archive" = "radicle";
let archiveProbe: Promise<boolean> | undefined;
function showArchiveTab() {
activeTab = "archive";
archiveProbe = archiveDownloadSupported(
`${baseUrlToString(baseUrl)}/raw/${id}/archive/${currentRefname}`,
);
}
$: radCloneUrl = `rad clone ${id}`;
$: portFragment =
@@ -83,19 +85,15 @@
<Icon name="git" />
Git
</Button>
{#if enabledArchiveDownload}
<div class="global-spacer"></div>
<Button
styleWidth="100%"
styleBorderRadius="0"
variant={activeTab === "archive" ? "selected" : "not-selected"}
on:click={() => {
activeTab = "archive";
}}>
on:click={showArchiveTab}>
<Icon name="archive" />
Download
</Button>
{/if}
</Radio>
</div>
@@ -119,17 +117,28 @@
</div>
</div>
{:else if activeTab === "archive"}
{#await archiveProbe}
<Loading small center />
{:then supported}
{#if supported}
<div>
<label for="git-clone-url">
If you don't have Radicle installed, you can still download an archive
of the repository.
If you don't have Radicle installed, you can still download an
archive of the repository.
</label>
<Command command={archiveUrl} />
<div style:margin-top="1.5rem">
Note that a compressed archive of the source code does not include any
of the social artifacts such as issues or patches nor the git history.
Note that a compressed archive of the source code does not include
any of the social artifacts such as issues or patches nor the git
history.
</div>
</div>
{:else}
{archiveUnsupportedMessage}
{/if}
{:catch}
{archiveUnsupportedMessage}
{/await}
{/if}
</div>
</Popover>