Skip to content

Commit 1e2c51d

Browse files
committed
chore: refactored download readable stream to use Save as
1 parent 2c40a2f commit 1e2c51d

File tree

2 files changed

+57
-73
lines changed

2 files changed

+57
-73
lines changed

web-app/src/App.tsx

Lines changed: 3 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -5,77 +5,7 @@ import './App.css';
55
import { type Chunker, type Source, OpenTDF } from '@opentdf/sdk';
66
import { type SessionInformation, OidcClient } from './session.js';
77
import { c } from './config.js';
8-
9-
/**
10-
* Downloads a ReadableStream as a file by collecting its data and triggering a browser download.
11-
* Supports aborting via AbortSignal in options.
12-
*
13-
* @param stream - The ReadableStream of Uint8Array data to download as a file.
14-
* @param filename - The name for the downloaded file (default: 'download.tdf').
15-
* @param options - Optional StreamPipeOptions, supports AbortSignal for cancellation.
16-
* @returns Promise that resolves when the download is triggered or rejects if aborted.
17-
*/
18-
export async function toFile(
19-
stream: ReadableStream<Uint8Array>,
20-
filename = 'download.tdf',
21-
options?: StreamPipeOptions
22-
): Promise<void> {
23-
// Get a reader for the stream
24-
const reader = stream.getReader();
25-
const chunks: Uint8Array[] = [];
26-
let done = false;
27-
let aborted = false;
28-
const signal = options?.signal;
29-
let abortHandler: (() => void) | undefined;
30-
31-
// Setup abort handling if a signal is provided
32-
if (signal) {
33-
if (signal.aborted) {
34-
throw new DOMException('Aborted', 'AbortError');
35-
}
36-
abortHandler = () => {
37-
aborted = true;
38-
reader.cancel();
39-
};
40-
signal.addEventListener('abort', abortHandler);
41-
}
42-
try {
43-
// Read the stream chunk by chunk
44-
while (!done) {
45-
if (aborted) {
46-
throw new DOMException('Aborted', 'AbortError');
47-
}
48-
const { value, done: streamDone } = await reader.read();
49-
if (value) {
50-
chunks.push(value); // Collect each chunk
51-
}
52-
done = streamDone;
53-
}
54-
} finally {
55-
// Clean up abort event listener
56-
if (signal && abortHandler) {
57-
signal.removeEventListener('abort', abortHandler);
58-
}
59-
}
60-
if (aborted) {
61-
throw new DOMException('Aborted', 'AbortError');
62-
}
63-
// Create a Blob from the collected chunks
64-
const blob = new Blob(chunks);
65-
// Create a temporary object URL for the Blob
66-
const url = URL.createObjectURL(blob);
67-
// Create an anchor element and trigger the download
68-
const a = document.createElement('a');
69-
a.href = url;
70-
a.download = filename;
71-
document.body.appendChild(a);
72-
a.click();
73-
// Clean up the anchor and object URL after download is triggered
74-
setTimeout(() => {
75-
document.body.removeChild(a);
76-
URL.revokeObjectURL(url);
77-
}, 0);
78-
}
8+
import { downloadReadableStream } from './utils/download-readable-stream';
799

8010
function decryptedFileName(encryptedFileName: string): string {
8111
// Groups: 1 file 'name' bit
@@ -450,7 +380,7 @@ function App() {
450380
try {
451381
switch (sinkType) {
452382
case 'file':
453-
await toFile(cipherTextWithProgress, downloadName, { signal: sc.signal });
383+
await downloadReadableStream(cipherTextWithProgress, downloadName, { signal: sc.signal });
454384
break;
455385
case 'fsapi':
456386
if (!f) {
@@ -525,7 +455,7 @@ function App() {
525455
.pipeThrough(progressTransformers.writer);
526456
switch (sinkType) {
527457
case 'file':
528-
await toFile(plainTextStream, dfn, { signal: sc.signal });
458+
await downloadReadableStream(plainTextStream, dfn, { signal: sc.signal });
529459
break;
530460
case 'fsapi':
531461
if (!f) {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* Downloads a ReadableStream as a file by streaming it directly to a file handle
3+
* using the File System Access API. Supports aborting via AbortSignal in options.
4+
*
5+
* @param stream - The ReadableStream of Uint8Array data to download as a file.
6+
* @param filename - The name for the downloaded file (default: 'download.tdf').
7+
* @param options - Optional StreamPipeOptions, supports AbortSignal for cancellation.
8+
* @returns Promise that resolves when the download is triggered or rejects if aborted.
9+
*/
10+
export async function downloadReadableStream(
11+
stream: ReadableStream<Uint8Array>,
12+
filename = 'download.tdf',
13+
options?: { signal?: AbortSignal }
14+
): Promise<void> {
15+
// Use the File System Access API to prompt the user for a save location
16+
const fileHandle = await window.showSaveFilePicker({
17+
suggestedName: filename,
18+
types: [
19+
{
20+
description: 'TDF File',
21+
accept: { 'application/octet-stream': ['.tdf'] },
22+
},
23+
],
24+
});
25+
const writable = await fileHandle.createWritable();
26+
let aborted = false;
27+
const signal = options?.signal;
28+
let abortHandler: (() => void) | undefined;
29+
30+
if (signal) {
31+
if (signal.aborted) {
32+
await writable.abort();
33+
throw new DOMException('Aborted', 'AbortError');
34+
}
35+
abortHandler = () => {
36+
aborted = true;
37+
writable.abort();
38+
};
39+
signal.addEventListener('abort', abortHandler);
40+
}
41+
42+
try {
43+
await stream.pipeTo(writable, { signal });
44+
} catch (err) {
45+
if (aborted) {
46+
throw new DOMException('Aborted', 'AbortError');
47+
}
48+
throw err;
49+
} finally {
50+
if (signal && abortHandler) {
51+
signal.removeEventListener('abort', abortHandler);
52+
}
53+
}
54+
}

0 commit comments

Comments
 (0)