Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 54 additions & 14 deletions apps/web/app/shared/[token]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,28 +54,53 @@ export default function SharedPage({
);

const getDownloadUrl = trpc.shares.getDownloadUrl.useMutation();
const [isDownloadingAll, setIsDownloadingAll] = useState(false);

const downloadFile = async (fileId?: string) => {
const result = await getDownloadUrl.mutateAsync({
token,
fileId,
password: enteredPassword,
});
const response = await fetch(result.url);
if (!response.ok) throw new Error(`Download failed (${response.status})`);
const blob = await response.blob();
const blobUrl = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = blobUrl;
a.download = result.filename;
a.click();
setTimeout(() => URL.revokeObjectURL(blobUrl), 100);
};

const handleDownload = async (fileId?: string) => {
try {
const result = await getDownloadUrl.mutateAsync({
token,
fileId,
password: enteredPassword,
});
const response = await fetch(result.url);
if (!response.ok) throw new Error(`Download failed (${response.status})`);
const blob = await response.blob();
const blobUrl = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = blobUrl;
a.download = result.filename;
a.click();
setTimeout(() => URL.revokeObjectURL(blobUrl), 100);
await downloadFile(fileId);
} catch (err) {
toast.error((err as Error).message);
}
};

const handleDownloadAll = async (files: { id: string }[]) => {
setIsDownloadingAll(true);
let failed = 0;
// Sequential: each file consumes one of the link's maxDownloads budget,
// and parallel blob clicks get throttled by the browser.
for (const file of files) {
try {
await downloadFile(file.id);
} catch {
failed++;
}
Comment on lines +89 to +94

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Blocked Downloads Count As Success

Browsers can block or prompt for repeated automatic downloads after the first asynchronous a.click(), but a blocked click does not reject downloadFile. The loop then reports success even when only some files were delivered, while each requested URL has already consumed the share link's download allowance.

}
setIsDownloadingAll(false);
if (failed > 0) {
toast.error(
`${failed} of ${files.length} ${failed === 1 ? "file" : "files"} failed to download`,
);
}
};

const navigateToFolder = (folderId: string) => {
setCurrentFolderId(folderId);
};
Expand Down Expand Up @@ -231,6 +256,21 @@ export default function SharedPage({
))}
</div>

{displayFiles && displayFiles.length > 0 && (
<Button
variant="outline"
size="sm"
className="w-full"
disabled={isDownloadingAll}
onClick={() => handleDownloadAll(displayFiles)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Previous Folder Files Stay Active

When the user moves directly between subfolders, the query can retain the previous folder's data while fetching the next folder. Because this button remains enabled with those stale file IDs, clicking it during the transition downloads files from the previous folder and consumes their download allowance.

>
<Download className="size-3.5" />
{isDownloadingAll
? "Downloading…"
: `Download all (${displayFiles.length})`}
</Button>
)}

{browseQuery.isLoading && isBrowsing ? (
<div className="border rounded-sm p-6 text-center">
<div className="skeleton h-4 w-24 mx-auto rounded-sm" />
Expand Down