mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-18 18:46:19 +08:00
feat(core): import progress & perf (#15197)
#### PR Dependency Tree * **PR #15197** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a new import pipeline with “plan then commit” batch handling for Markdown, Notion HTML, Obsidian, and Bear backups. * Enabled native import sessions with progress, cancellation, and batch-by-batch committing (including assets, folders, icons, and tags). * Added web preflight limits for ZIP and multi-file imports. * **Bug Fixes** * Improved import error/warning reporting and continued processing when some items fail. * Strengthened snapshot-based file/directory picking to preserve paths. * **Chores** * Updated project packaging/configuration for the new import workflow. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -142,6 +142,55 @@ type AcceptTypes =
|
||||
| 'Docx'
|
||||
| 'MindMap';
|
||||
|
||||
type OpenFileOptions = {
|
||||
fileSystemAccess?: boolean;
|
||||
snapshot?: boolean;
|
||||
};
|
||||
|
||||
function copyToArrayBuffer(bytes: Uint8Array) {
|
||||
const copy = new Uint8Array(bytes.byteLength);
|
||||
copy.set(bytes);
|
||||
return copy.buffer;
|
||||
}
|
||||
|
||||
function sleep(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export async function snapshotFile(file: File, relativePath?: string) {
|
||||
let bytes: Uint8Array | null = null;
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
try {
|
||||
bytes = new Uint8Array(await file.arrayBuffer());
|
||||
break;
|
||||
} catch (error) {
|
||||
if (!isNotReadableError(error) || attempt === 2) {
|
||||
throw error;
|
||||
}
|
||||
await sleep(100 * (attempt + 1));
|
||||
}
|
||||
}
|
||||
if (!bytes) {
|
||||
throw new DOMException('File could not be read', 'NotReadableError');
|
||||
}
|
||||
const snapshot = new File([copyToArrayBuffer(bytes)], file.name, {
|
||||
type: file.type,
|
||||
lastModified: file.lastModified,
|
||||
});
|
||||
const path = relativePath ?? file.webkitRelativePath;
|
||||
if (path) {
|
||||
Object.defineProperty(snapshot, 'webkitRelativePath', {
|
||||
value: path,
|
||||
writable: false,
|
||||
});
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
export async function snapshotFiles(files: File[]) {
|
||||
return Promise.all(files.map(file => snapshotFile(file)));
|
||||
}
|
||||
|
||||
function canUseFileSystemAccessAPI(
|
||||
api: 'showOpenFilePicker' | 'showDirectoryPicker'
|
||||
) {
|
||||
@@ -157,11 +206,21 @@ function canUseFileSystemAccessAPI(
|
||||
);
|
||||
}
|
||||
|
||||
function isAbortError(error: unknown) {
|
||||
return error instanceof Error && error.name === 'AbortError';
|
||||
}
|
||||
|
||||
function isNotReadableError(error: unknown) {
|
||||
return error instanceof Error && error.name === 'NotReadableError';
|
||||
}
|
||||
|
||||
export async function openFilesWith(
|
||||
acceptType: AcceptTypes = 'Any',
|
||||
multiple: boolean = true
|
||||
multiple: boolean = true,
|
||||
options?: OpenFileOptions
|
||||
): Promise<File[] | null> {
|
||||
const supportsFileSystemAccess =
|
||||
options?.fileSystemAccess !== false &&
|
||||
canUseFileSystemAccessAPI('showOpenFilePicker');
|
||||
|
||||
// If the File System Access API is supported…
|
||||
@@ -180,15 +239,19 @@ export async function openFilesWith(
|
||||
// Show the file picker, optionally allowing multiple files.
|
||||
const handles = await window.showOpenFilePicker(pickerOpts);
|
||||
|
||||
return await Promise.all(handles.map(handle => handle.getFile()));
|
||||
const files = await Promise.all(handles.map(handle => handle.getFile()));
|
||||
return options?.snapshot ? await snapshotFiles(files) : files;
|
||||
} catch (err) {
|
||||
if (options?.snapshot && !isAbortError(err)) {
|
||||
throw err;
|
||||
}
|
||||
console.error(err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback if the File System Access API is not supported.
|
||||
return new Promise(resolve => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Append a new `<input type="file" multiple? />` and hide it.
|
||||
const input = document.createElement('input');
|
||||
input.classList.add('affine-upload-input');
|
||||
@@ -205,13 +268,27 @@ export async function openFilesWith(
|
||||
document.body.append(input);
|
||||
// The `change` event fires when the user interacts with the dialog.
|
||||
input.addEventListener('change', () => {
|
||||
// Remove the `<input type="file" multiple? />` again from the DOM.
|
||||
input.remove();
|
||||
const files = input.files ? Array.from(input.files) : null;
|
||||
const finish = (result: File[] | null) => {
|
||||
// Remove the `<input type="file" multiple? />` again from the DOM.
|
||||
input.remove();
|
||||
resolve(result);
|
||||
};
|
||||
|
||||
resolve(input.files ? Array.from(input.files) : null);
|
||||
if (files && options?.snapshot) {
|
||||
snapshotFiles(files).then(finish, error => {
|
||||
input.remove();
|
||||
reject(error);
|
||||
});
|
||||
} else {
|
||||
finish(files);
|
||||
}
|
||||
});
|
||||
// The `cancel` event fires when the user cancels the dialog.
|
||||
input.addEventListener('cancel', () => resolve(null));
|
||||
input.addEventListener('cancel', () => {
|
||||
input.remove();
|
||||
resolve(null);
|
||||
});
|
||||
// Show the picker.
|
||||
if ('showPicker' in HTMLInputElement.prototype) {
|
||||
input.showPicker();
|
||||
@@ -221,12 +298,18 @@ export async function openFilesWith(
|
||||
});
|
||||
}
|
||||
|
||||
export async function openDirectory(): Promise<File[] | null> {
|
||||
export async function openDirectory(
|
||||
options?: OpenFileOptions
|
||||
): Promise<File[] | null> {
|
||||
const supportsFileSystemAccess = canUseFileSystemAccessAPI(
|
||||
'showDirectoryPicker'
|
||||
);
|
||||
|
||||
if (supportsFileSystemAccess && window.showDirectoryPicker) {
|
||||
if (
|
||||
options?.fileSystemAccess !== false &&
|
||||
supportsFileSystemAccess &&
|
||||
window.showDirectoryPicker
|
||||
) {
|
||||
try {
|
||||
const dirHandle = await window.showDirectoryPicker();
|
||||
const files: File[] = [];
|
||||
@@ -241,11 +324,15 @@ export async function openDirectory(): Promise<File[] | null> {
|
||||
const fileHandle = handle as FileSystemFileHandle;
|
||||
if (fileHandle.getFile) {
|
||||
const file = await fileHandle.getFile();
|
||||
Object.defineProperty(file, 'webkitRelativePath', {
|
||||
value: relativePath,
|
||||
writable: false,
|
||||
});
|
||||
files.push(file);
|
||||
if (options?.snapshot) {
|
||||
files.push(await snapshotFile(file, relativePath));
|
||||
} else {
|
||||
Object.defineProperty(file, 'webkitRelativePath', {
|
||||
value: relativePath,
|
||||
writable: false,
|
||||
});
|
||||
files.push(file);
|
||||
}
|
||||
}
|
||||
} else if (handle.kind === 'directory') {
|
||||
await readDirectory(
|
||||
@@ -259,12 +346,15 @@ export async function openDirectory(): Promise<File[] | null> {
|
||||
await readDirectory(dirHandle, '');
|
||||
return files;
|
||||
} catch (err) {
|
||||
if (options?.snapshot && !isAbortError(err)) {
|
||||
throw err;
|
||||
}
|
||||
console.error(err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const input = document.createElement('input');
|
||||
input.classList.add('affine-upload-input');
|
||||
input.style.display = 'none';
|
||||
@@ -276,11 +366,26 @@ export async function openDirectory(): Promise<File[] | null> {
|
||||
document.body.append(input);
|
||||
|
||||
input.addEventListener('change', () => {
|
||||
input.remove();
|
||||
resolve(input.files ? Array.from(input.files) : null);
|
||||
const files = input.files ? Array.from(input.files) : null;
|
||||
const finish = (result: File[] | null) => {
|
||||
input.remove();
|
||||
resolve(result);
|
||||
};
|
||||
|
||||
if (files && options?.snapshot) {
|
||||
snapshotFiles(files).then(finish, error => {
|
||||
input.remove();
|
||||
reject(error);
|
||||
});
|
||||
} else {
|
||||
finish(files);
|
||||
}
|
||||
});
|
||||
|
||||
input.addEventListener('cancel', () => resolve(null));
|
||||
input.addEventListener('cancel', () => {
|
||||
input.remove();
|
||||
resolve(null);
|
||||
});
|
||||
|
||||
if ('showPicker' in HTMLInputElement.prototype) {
|
||||
input.showPicker();
|
||||
|
||||
Reference in New Issue
Block a user