Fix bulk download edge cases and improve robustness
- FolderPickerWriter: throw AbortError on cancel instead of returning null - FolderPickerWriter: add try/catch with abort() to release file locks on failure - ZipNeutralinoWriter: move writeBinaryFile after response.body validation - bulkDownloadSettings: migrate legacy key and validate stored values - download-utils: catch ffmpeg cancellation via signal.aborted - downloads.js: use consistent Neutralino detection with bridge module - download-utils: use strict equality for flac extension check
This commit is contained in:
parent
a776e24aee
commit
b31be7dc80
4 changed files with 57 additions and 28 deletions
|
|
@ -98,11 +98,11 @@ export class ZipNeutralinoWriter implements IBulkDownloadWriter {
|
|||
}
|
||||
|
||||
const { downloadZip } = await loadClientZip();
|
||||
await bridge.filesystem.writeBinaryFile(savePath, new ArrayBuffer(0));
|
||||
|
||||
const response = downloadZip(files);
|
||||
if (!response.body) throw new Error('ZIP response body is null');
|
||||
|
||||
await bridge.filesystem.writeBinaryFile(savePath, new ArrayBuffer(0));
|
||||
|
||||
const reader = response.body.getReader();
|
||||
let receivedLength = 0;
|
||||
|
||||
|
|
@ -138,10 +138,17 @@ export class FolderPickerWriter implements IBulkDownloadWriter {
|
|||
static async create(): Promise<FolderPickerWriter> {
|
||||
// showDirectoryPicker is part of the File System Access API (not yet in all TS DOM libs)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const dirHandle: FileSystemDirectoryHandle = await (window as any).showDirectoryPicker({
|
||||
mode: 'readwrite',
|
||||
});
|
||||
return new FolderPickerWriter(dirHandle);
|
||||
try {
|
||||
const dirHandle: FileSystemDirectoryHandle = await (window as any).showDirectoryPicker({
|
||||
mode: 'readwrite',
|
||||
});
|
||||
return new FolderPickerWriter(dirHandle);
|
||||
} catch (error) {
|
||||
if (error instanceof DOMException && error.name === 'AbortError') {
|
||||
throw error;
|
||||
}
|
||||
throw new DOMException('User cancelled directory picker', 'AbortError');
|
||||
}
|
||||
}
|
||||
|
||||
async write(files: AsyncIterable<WriterEntry>): Promise<void> {
|
||||
|
|
@ -158,23 +165,25 @@ export class FolderPickerWriter implements IBulkDownloadWriter {
|
|||
const fileHandle = await currentDir.getFileHandle(filename, { create: true });
|
||||
const writable = await fileHandle.createWritable();
|
||||
|
||||
const { input } = file;
|
||||
if (input instanceof Blob) {
|
||||
await writable.write(input);
|
||||
} else if (typeof input === 'string') {
|
||||
await writable.write(new Blob([input], { type: 'text/plain' }));
|
||||
} else {
|
||||
// ArrayBuffer or Uint8Array – wrap in a Blob to guarantee strict typing.
|
||||
// Use byteOffset/byteLength so only the view's range is included, not the
|
||||
// whole backing ArrayBuffer (which may be larger due to pooling).
|
||||
const buf =
|
||||
input instanceof Uint8Array
|
||||
? input.buffer.slice(input.byteOffset, input.byteOffset + input.byteLength)
|
||||
: input;
|
||||
await writable.write(new Blob([buf as ArrayBuffer]));
|
||||
}
|
||||
try {
|
||||
const { input } = file;
|
||||
if (input instanceof Blob) {
|
||||
await writable.write(input);
|
||||
} else if (typeof input === 'string') {
|
||||
await writable.write(new Blob([input], { type: 'text/plain' }));
|
||||
} else {
|
||||
const buf =
|
||||
input instanceof Uint8Array
|
||||
? input.buffer.slice(input.byteOffset, input.byteOffset + input.byteLength)
|
||||
: input;
|
||||
await writable.write(new Blob([buf as ArrayBuffer]));
|
||||
}
|
||||
|
||||
await writable.close();
|
||||
await writable.close();
|
||||
} catch (error) {
|
||||
await writable.abort();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ export async function applyAudioPostProcessing(
|
|||
|
||||
if (await containerFmt?.needsTranscode(blob)) {
|
||||
blob = await transcodeWithContainerFormat(blob, containerFmt, onProgress, signal);
|
||||
} else if (extension == 'flac') {
|
||||
} else if (extension === 'flac') {
|
||||
blob = await rebuildFlacWithoutMetadata(blob);
|
||||
} else {
|
||||
blob = await ffmpegNewContainer(
|
||||
|
|
@ -76,7 +76,7 @@ export async function applyAudioPostProcessing(
|
|||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if ((error as Error)?.name === 'AbortError') {
|
||||
if ((error as Error)?.name === 'AbortError' || signal?.aborted) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -669,7 +669,9 @@ async function bulkDownloadToZip(
|
|||
* or null when individual sequential downloads should be used.
|
||||
*/
|
||||
async function createBulkWriter(folderName) {
|
||||
const isNeutralino = window.NL_MODE === true;
|
||||
const isNeutralino =
|
||||
typeof window !== 'undefined' &&
|
||||
(window.NL_MODE || window.location.search.includes('mode=neutralino') || window.parent !== window);
|
||||
const method = bulkDownloadSettings.getMethod();
|
||||
const forceZipBlob = bulkDownloadSettings.shouldForceZipBlob();
|
||||
const hasFileSystemAccess = 'showSaveFilePicker' in window && 'createWritable' in FileSystemFileHandle.prototype;
|
||||
|
|
@ -679,8 +681,14 @@ async function createBulkWriter(folderName) {
|
|||
return new ZipNeutralinoWriter(folderName);
|
||||
}
|
||||
if (method === 'folder' && hasFolderPicker) {
|
||||
// FolderPickerWriter.create() throws AbortError if the user cancels
|
||||
return await FolderPickerWriter.create();
|
||||
try {
|
||||
return await FolderPickerWriter.create();
|
||||
} catch (error) {
|
||||
if (error instanceof DOMException && error.name === 'AbortError') {
|
||||
throw error;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (method === 'individual') {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -637,11 +637,23 @@ export const trackDateSettings = {
|
|||
export const bulkDownloadSettings = {
|
||||
METHOD_KEY: 'bulk-download-method',
|
||||
FORCE_ZIP_BLOB_KEY: 'bulk-download-force-zip-blob',
|
||||
LEGACY_INDIVIDUAL_KEY: 'force-individual-downloads',
|
||||
VALID_METHODS: ['zip', 'folder', 'individual'],
|
||||
|
||||
/** Returns the selected bulk download method: 'zip' | 'folder' | 'individual' */
|
||||
getMethod() {
|
||||
try {
|
||||
return localStorage.getItem(this.METHOD_KEY) || 'zip';
|
||||
const stored = localStorage.getItem(this.METHOD_KEY);
|
||||
if (stored && this.VALID_METHODS.includes(stored)) {
|
||||
return stored;
|
||||
}
|
||||
const legacy = localStorage.getItem(this.LEGACY_INDIVIDUAL_KEY);
|
||||
if (legacy === 'true') {
|
||||
localStorage.setItem(this.METHOD_KEY, 'individual');
|
||||
localStorage.removeItem(this.LEGACY_INDIVIDUAL_KEY);
|
||||
return 'individual';
|
||||
}
|
||||
return 'zip';
|
||||
} catch {
|
||||
return 'zip';
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue