mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-17 18:16:15 +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:
@@ -7,6 +7,7 @@ import { byokStorageHandlers } from './byok-storage/handlers';
|
||||
import { clipboardHandlers } from './clipboard';
|
||||
import { configStorageHandlers } from './config-storage';
|
||||
import { findInPageHandlers } from './find-in-page';
|
||||
import { importHandlers } from './import';
|
||||
import { getLogFilePath, logger, revealLogFile } from './logger';
|
||||
import { recordingHandlers } from './recording';
|
||||
import { checkSource } from './security-restrictions';
|
||||
@@ -39,6 +40,7 @@ export const allHandlers = {
|
||||
updater: updaterHandlers,
|
||||
configStorage: configStorageHandlers,
|
||||
findInPage: findInPageHandlers,
|
||||
import: importHandlers,
|
||||
sharedStorage: sharedStorageHandlers,
|
||||
worker: workerHandlers,
|
||||
recording: recordingHandlers,
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { CreateImportSessionOptions } from '@affine/native';
|
||||
import {
|
||||
cancelImportSession,
|
||||
createImportSession,
|
||||
disposeImportSession,
|
||||
nextImportBatch,
|
||||
} from '@affine/native';
|
||||
|
||||
export const importHandlers = {
|
||||
createImportSession: (
|
||||
event: Electron.IpcMainInvokeEvent,
|
||||
options: CreateImportSessionOptions
|
||||
) => {
|
||||
void event;
|
||||
return createImportSession({
|
||||
format: options.format,
|
||||
source: options.source,
|
||||
batchLimits: options.batchLimits,
|
||||
});
|
||||
},
|
||||
nextImportBatch: (event: Electron.IpcMainInvokeEvent, sessionId: string) => {
|
||||
void event;
|
||||
return nextImportBatch(sessionId);
|
||||
},
|
||||
cancelImportSession: (
|
||||
event: Electron.IpcMainInvokeEvent,
|
||||
sessionId: string
|
||||
) => {
|
||||
void event;
|
||||
return cancelImportSession(sessionId);
|
||||
},
|
||||
disposeImportSession: (
|
||||
event: Electron.IpcMainInvokeEvent,
|
||||
sessionId: string
|
||||
) => {
|
||||
void event;
|
||||
return disposeImportSession(sessionId);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { importHandlers } from './handlers';
|
||||
@@ -3,10 +3,14 @@ import type { MessagePort } from 'node:worker_threads';
|
||||
|
||||
import type { EventBasedChannel } from 'async-call-rpc';
|
||||
import { AsyncCall } from 'async-call-rpc';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { ipcRenderer, webUtils } from 'electron';
|
||||
import { Subject } from 'rxjs';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type {
|
||||
CreateImportSessionFromSourceOptions,
|
||||
NativeImportBrowserSource,
|
||||
} from '../shared/import';
|
||||
import {
|
||||
AFFINE_API_CHANNEL_NAME,
|
||||
AFFINE_EVENT_CHANNEL_NAME,
|
||||
@@ -275,9 +279,61 @@ function getHelperAPIs() {
|
||||
const mainAPIs = getMainAPIs();
|
||||
const helperAPIs = getHelperAPIs();
|
||||
|
||||
type DirectoryImportFile = File & { webkitRelativePath?: string };
|
||||
|
||||
function filePathFromFile(file: File) {
|
||||
return webUtils.getPathForFile(file);
|
||||
}
|
||||
|
||||
function directoryPathFromFiles(files: File[]) {
|
||||
const first = files.find(
|
||||
(file): file is DirectoryImportFile =>
|
||||
!!(file as DirectoryImportFile).webkitRelativePath
|
||||
);
|
||||
if (!first) return null;
|
||||
const filePath = filePathFromFile(first);
|
||||
if (!filePath) return null;
|
||||
const relativePath = first.webkitRelativePath;
|
||||
if (!relativePath) return null;
|
||||
const relativeParts = relativePath.split('/');
|
||||
let rootPath = filePath.replaceAll('\\', '/');
|
||||
for (let i = relativeParts.length - 1; i > 0; i--) {
|
||||
const part = relativeParts[i];
|
||||
if (part && rootPath.endsWith(`/${part}`)) {
|
||||
rootPath = rootPath.slice(0, -part.length - 1);
|
||||
}
|
||||
}
|
||||
return rootPath || null;
|
||||
}
|
||||
|
||||
function resolveNativeImportSource(source: NativeImportBrowserSource) {
|
||||
if (source.kind === 'file') {
|
||||
const path = filePathFromFile(source.file);
|
||||
return path ? { kind: 'filePath', path } : null;
|
||||
}
|
||||
const path = directoryPathFromFiles(source.files);
|
||||
return path ? { kind: 'directoryPath', path } : null;
|
||||
}
|
||||
|
||||
export const apis = {
|
||||
...mainAPIs.apis,
|
||||
...helperAPIs.apis,
|
||||
import: {
|
||||
...mainAPIs.apis.import,
|
||||
createImportSessionFromSource(
|
||||
options: CreateImportSessionFromSourceOptions
|
||||
) {
|
||||
const source = resolveNativeImportSource(options.source);
|
||||
if (!source) {
|
||||
throw new Error('Native import requires a local file source');
|
||||
}
|
||||
return mainAPIs.apis.import.createImportSession({
|
||||
format: options.format,
|
||||
source,
|
||||
batchLimits: options.batchLimits,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const events = {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
export type NativeImportFormat =
|
||||
| 'markdownZip'
|
||||
| 'notionZip'
|
||||
| 'obsidian'
|
||||
| 'bearZip';
|
||||
|
||||
export type NativeImportBrowserSource =
|
||||
| { kind: 'file'; file: File }
|
||||
| { kind: 'directory'; files: File[] };
|
||||
|
||||
export type CreateImportSessionFromSourceOptions = {
|
||||
format: NativeImportFormat;
|
||||
source: NativeImportBrowserSource;
|
||||
batchLimits?: {
|
||||
maxDocs?: number;
|
||||
maxBlobs?: number;
|
||||
maxBlobBytes?: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type NativeImportSessionHandlers = {
|
||||
createImportSession(
|
||||
options: CreateImportSessionFromSourceOptions
|
||||
): Promise<string> | string;
|
||||
nextImportBatch(sessionId: string): Promise<string | null> | string | null;
|
||||
cancelImportSession(sessionId: string): Promise<void> | void;
|
||||
disposeImportSession(sessionId: string): Promise<void> | void;
|
||||
};
|
||||
Reference in New Issue
Block a user