|
1 | 1 | import { clsx } from 'clsx';
|
2 | 2 | import { useState, useEffect, type ChangeEvent } from 'react';
|
3 |
| -import streamsaver from 'streamsaver'; |
4 | 3 | import { showSaveFilePicker } from 'native-file-system-adapter';
|
5 | 4 | import './App.css';
|
6 | 5 | import { type Chunker, type Source, OpenTDF } from '@opentdf/sdk';
|
7 | 6 | import { type SessionInformation, OidcClient } from './session.js';
|
8 | 7 | import { c } from './config.js';
|
9 | 8 |
|
10 |
| -async function toFile( |
| 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( |
11 | 19 | stream: ReadableStream<Uint8Array>,
|
12 |
| - filepath = 'download.tdf', |
| 20 | + filename = 'download.tdf', |
13 | 21 | options?: StreamPipeOptions
|
14 | 22 | ): Promise<void> {
|
15 |
| - const fileStream = streamsaver.createWriteStream(filepath, { |
16 |
| - writableStrategy: { highWaterMark: 1 }, |
17 |
| - readableStrategy: { highWaterMark: 1 }, |
18 |
| - }); |
| 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; |
19 | 30 |
|
20 |
| - return stream.pipeTo(fileStream, options); |
| 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); |
21 | 78 | }
|
22 | 79 |
|
23 | 80 | function decryptedFileName(encryptedFileName: string): string {
|
|
0 commit comments