mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00:00
fix(electron): export and import (#9767)
This commit is contained in:
@@ -1,12 +1,19 @@
|
||||
import { ValidationResult } from '@affine/native';
|
||||
import { parse } from 'node:path';
|
||||
|
||||
import { DocStorage, ValidationResult } from '@affine/native';
|
||||
import { parseUniversalId } from '@affine/nbstore';
|
||||
import fs from 'fs-extra';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { logger } from '../logger';
|
||||
import { mainRPC } from '../main-rpc';
|
||||
import { ensureSQLiteDB } from '../nbstore/v1';
|
||||
import { getDocStoragePool } from '../nbstore';
|
||||
import { storeWorkspaceMeta } from '../workspace';
|
||||
import { getWorkspaceDBPath, getWorkspacesBasePath } from '../workspace/meta';
|
||||
import {
|
||||
getSpaceDBPath,
|
||||
getWorkspaceDBPath,
|
||||
getWorkspacesBasePath,
|
||||
} from '../workspace/meta';
|
||||
|
||||
export type ErrorMessage =
|
||||
| 'DB_FILE_PATH_INVALID'
|
||||
@@ -69,20 +76,26 @@ function getDefaultDBFileName(name: string, id: string) {
|
||||
*
|
||||
* It will just copy the file to the given path
|
||||
*/
|
||||
export async function saveDBFileAs(id: string): Promise<SaveDBFileResult> {
|
||||
export async function saveDBFileAs(
|
||||
universalId: string,
|
||||
name: string
|
||||
): Promise<SaveDBFileResult> {
|
||||
try {
|
||||
// TODO(@forehalo): use `nbstore` when it is ready
|
||||
// const storage = await ensureStorage(id);
|
||||
const { peer, type, id } = parseUniversalId(universalId);
|
||||
const dbPath = await getSpaceDBPath(peer, type, id);
|
||||
|
||||
// connect to the pool and make sure all changes (WAL) are written to db
|
||||
const pool = getDocStoragePool();
|
||||
await pool.connect(universalId, dbPath);
|
||||
await pool.checkpoint(universalId); // make sure all changes (WAL) are written to db
|
||||
|
||||
const storage = await ensureSQLiteDB('workspace', id);
|
||||
await storage.checkpoint(); // make sure all changes (WAL) are written to db
|
||||
const fakedResult = getFakedResult();
|
||||
const dbPath = storage.path;
|
||||
if (!dbPath) {
|
||||
return {
|
||||
error: 'DB_FILE_PATH_INVALID',
|
||||
};
|
||||
}
|
||||
|
||||
const ret =
|
||||
fakedResult ??
|
||||
(await mainRPC.showSaveDialog({
|
||||
@@ -96,12 +109,10 @@ export async function saveDBFileAs(id: string): Promise<SaveDBFileResult> {
|
||||
name: '',
|
||||
},
|
||||
],
|
||||
defaultPath: getDefaultDBFileName(
|
||||
(await storage.getWorkspaceName()) ?? 'db',
|
||||
id
|
||||
),
|
||||
defaultPath: getDefaultDBFileName(name, id),
|
||||
message: 'Save Workspace as a SQLite Database file',
|
||||
}));
|
||||
|
||||
const filePath = ret.filePath;
|
||||
if (ret.canceled || !filePath) {
|
||||
return {
|
||||
@@ -159,7 +170,7 @@ export async function selectDBFileLocation(): Promise<SelectDBFileLocationResult
|
||||
* - return the new workspace id
|
||||
*
|
||||
* eg, it will create a new folder in app-data:
|
||||
* <app-data>/<app-name>/workspaces/<workspace-id>/storage.db
|
||||
* <app-data>/<app-name>/<workspaces|userspaces>/<peer>/<workspace-id>/storage.db
|
||||
*
|
||||
* On the renderer side, after the UI got a new workspace id, it will
|
||||
* update the local workspace id list and then connect to it.
|
||||
@@ -195,34 +206,29 @@ export async function loadDBFile(): Promise<LoadDBFileResult> {
|
||||
}
|
||||
|
||||
const workspaceId = nanoid(10);
|
||||
return await loadV1DBFile(originalPath, workspaceId);
|
||||
let storage = new DocStorage(originalPath);
|
||||
|
||||
// TODO(forehalo): use `nbstore` when it is ready
|
||||
// let storage = new DocStorage(originalPath);
|
||||
// if imported db is not a valid v2 db, we will treat it as a v1 db
|
||||
if (!(await storage.validate())) {
|
||||
return await cpV1DBFile(originalPath, workspaceId);
|
||||
}
|
||||
|
||||
// // if imported db is not a valid v2 db, we will treat it as a v1 db
|
||||
// if (!(await storage.validate())) {
|
||||
// return loadV1DBFile(originalPath, workspaceId);
|
||||
// }
|
||||
// v2 import logic
|
||||
const internalFilePath = await getSpaceDBPath(
|
||||
'local',
|
||||
'workspace',
|
||||
workspaceId
|
||||
);
|
||||
await fs.ensureDir(parse(internalFilePath).dir);
|
||||
await fs.copy(originalPath, internalFilePath);
|
||||
logger.info(`loadDBFile, copy: ${originalPath} -> ${internalFilePath}`);
|
||||
|
||||
// // v2 import logic
|
||||
// const internalFilePath = await getSpaceDBPath(
|
||||
// 'local',
|
||||
// 'workspace',
|
||||
// workspaceId
|
||||
// );
|
||||
// await fs.ensureDir(await getWorkspacesBasePath());
|
||||
// await fs.copy(originalPath, internalFilePath);
|
||||
// logger.info(`loadDBFile, copy: ${originalPath} -> ${internalFilePath}`);
|
||||
storage = new DocStorage(internalFilePath);
|
||||
await storage.setSpaceId(workspaceId);
|
||||
|
||||
// storage = new DocStorage(internalFilePath);
|
||||
// await storage.connect();
|
||||
// await storage.setSpaceId(workspaceId);
|
||||
// await storage.close();
|
||||
|
||||
// return {
|
||||
// workspaceId,
|
||||
// };
|
||||
return {
|
||||
workspaceId,
|
||||
};
|
||||
} catch (err) {
|
||||
logger.error('loadDBFile', err);
|
||||
return {
|
||||
@@ -231,7 +237,7 @@ export async function loadDBFile(): Promise<LoadDBFileResult> {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadV1DBFile(
|
||||
async function cpV1DBFile(
|
||||
originalPath: string,
|
||||
workspaceId: string
|
||||
): Promise<LoadDBFileResult> {
|
||||
|
||||
@@ -9,8 +9,8 @@ export const dialogHandlers = {
|
||||
loadDBFile: async () => {
|
||||
return loadDBFile();
|
||||
},
|
||||
saveDBFileAs: async (id: string) => {
|
||||
return saveDBFileAs(id);
|
||||
saveDBFileAs: async (universalId: string, name: string) => {
|
||||
return saveDBFileAs(universalId, name);
|
||||
},
|
||||
selectDBFileLocation: async () => {
|
||||
return selectDBFileLocation();
|
||||
|
||||
@@ -9,12 +9,17 @@ import { getSpaceDBPath } from '../workspace/meta';
|
||||
|
||||
const POOL = new DocStoragePool();
|
||||
|
||||
export function getDocStoragePool() {
|
||||
return POOL;
|
||||
}
|
||||
|
||||
export const nbstoreHandlers: NativeDBApis = {
|
||||
connect: async (universalId: string) => {
|
||||
const { peer, type, id } = parseUniversalId(universalId);
|
||||
const dbPath = await getSpaceDBPath(peer, type, id);
|
||||
await fs.ensureDir(path.dirname(dbPath));
|
||||
await POOL.connect(universalId, dbPath);
|
||||
await POOL.setSpaceId(universalId, id);
|
||||
},
|
||||
disconnect: POOL.disconnect.bind(POOL),
|
||||
pushUpdate: POOL.pushUpdate.bind(POOL),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { nbstoreHandlers } from './handlers';
|
||||
export { getDocStoragePool } from './handlers';
|
||||
export { dbEvents as dbEventsV1, dbHandlers as dbHandlersV1 } from './v1';
|
||||
export { universalId } from '@affine/nbstore';
|
||||
|
||||
@@ -255,4 +255,12 @@ export class SQLiteAdapter {
|
||||
await this.db.clearSyncMetadata();
|
||||
},
|
||||
};
|
||||
|
||||
async getDocTimestamps() {
|
||||
if (!this.db) {
|
||||
logger.warn(`${this.path} is not connected`);
|
||||
return [];
|
||||
}
|
||||
return await this.db.getDocTimestamps();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { existsSync } from 'node:fs';
|
||||
|
||||
import type { SpaceType } from '@affine/nbstore';
|
||||
|
||||
import { logger } from '../../logger';
|
||||
import { getWorkspaceMeta } from '../../workspace/meta';
|
||||
import type { WorkspaceSQLiteDB } from './workspace-db-adapter';
|
||||
import { openWorkspaceDatabase } from './workspace-db-adapter';
|
||||
|
||||
@@ -35,10 +38,32 @@ async function getWorkspaceDB(spaceType: SpaceType, id: string) {
|
||||
|
||||
process.on('beforeExit', cleanup);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
|
||||
// oxlint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return db!;
|
||||
}
|
||||
|
||||
export function ensureSQLiteDB(spaceType: SpaceType, id: string) {
|
||||
export async function ensureSQLiteDB(
|
||||
spaceType: SpaceType,
|
||||
id: string
|
||||
): Promise<WorkspaceSQLiteDB | null> {
|
||||
const meta = await getWorkspaceMeta(spaceType, id);
|
||||
|
||||
// do not auto create v1 db anymore
|
||||
if (!existsSync(meta.mainDBPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getWorkspaceDB(spaceType, id);
|
||||
}
|
||||
|
||||
export async function ensureSQLiteDisconnected(
|
||||
spaceType: SpaceType,
|
||||
id: string
|
||||
) {
|
||||
const db = await ensureSQLiteDB(spaceType, id);
|
||||
|
||||
if (db) {
|
||||
await db.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { SpaceType } from '@affine/nbstore';
|
||||
|
||||
import { mainRPC } from '../../main-rpc';
|
||||
import type { MainEventRegister } from '../../type';
|
||||
import { ensureSQLiteDB } from './ensure-db';
|
||||
|
||||
@@ -13,119 +12,41 @@ export const dbHandlers = {
|
||||
subdocId: string
|
||||
) => {
|
||||
const spaceDB = await ensureSQLiteDB(spaceType, workspaceId);
|
||||
|
||||
if (!spaceDB) {
|
||||
// means empty update in yjs
|
||||
return new Uint8Array([0, 0]);
|
||||
}
|
||||
|
||||
return spaceDB.getDocAsUpdates(subdocId);
|
||||
},
|
||||
applyDocUpdate: async (
|
||||
spaceType: SpaceType,
|
||||
workspaceId: string,
|
||||
update: Uint8Array,
|
||||
subdocId: string
|
||||
) => {
|
||||
getDocTimestamps: async (spaceType: SpaceType, workspaceId: string) => {
|
||||
const spaceDB = await ensureSQLiteDB(spaceType, workspaceId);
|
||||
return spaceDB.addUpdateToSQLite(update, subdocId);
|
||||
},
|
||||
deleteDoc: async (
|
||||
spaceType: SpaceType,
|
||||
workspaceId: string,
|
||||
subdocId: string
|
||||
) => {
|
||||
const spaceDB = await ensureSQLiteDB(spaceType, workspaceId);
|
||||
return spaceDB.deleteUpdate(subdocId);
|
||||
},
|
||||
addBlob: async (
|
||||
spaceType: SpaceType,
|
||||
workspaceId: string,
|
||||
key: string,
|
||||
data: Uint8Array
|
||||
) => {
|
||||
const spaceDB = await ensureSQLiteDB(spaceType, workspaceId);
|
||||
return spaceDB.addBlob(key, data);
|
||||
|
||||
if (!spaceDB) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return spaceDB.getDocTimestamps();
|
||||
},
|
||||
getBlob: async (spaceType: SpaceType, workspaceId: string, key: string) => {
|
||||
const spaceDB = await ensureSQLiteDB(spaceType, workspaceId);
|
||||
|
||||
if (!spaceDB) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return spaceDB.getBlob(key);
|
||||
},
|
||||
deleteBlob: async (
|
||||
spaceType: SpaceType,
|
||||
workspaceId: string,
|
||||
key: string
|
||||
) => {
|
||||
const spaceDB = await ensureSQLiteDB(spaceType, workspaceId);
|
||||
return spaceDB.deleteBlob(key);
|
||||
},
|
||||
getBlobKeys: async (spaceType: SpaceType, workspaceId: string) => {
|
||||
const spaceDB = await ensureSQLiteDB(spaceType, workspaceId);
|
||||
|
||||
if (!spaceDB) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return spaceDB.getBlobKeys();
|
||||
},
|
||||
getDefaultStorageLocation: async () => {
|
||||
return await mainRPC.getPath('sessionData');
|
||||
},
|
||||
getServerClock: async (
|
||||
spaceType: SpaceType,
|
||||
workspaceId: string,
|
||||
key: string
|
||||
) => {
|
||||
const spaceDB = await ensureSQLiteDB(spaceType, workspaceId);
|
||||
return spaceDB.adapter.serverClock.get(key);
|
||||
},
|
||||
setServerClock: async (
|
||||
spaceType: SpaceType,
|
||||
workspaceId: string,
|
||||
key: string,
|
||||
data: Uint8Array
|
||||
) => {
|
||||
const spaceDB = await ensureSQLiteDB(spaceType, workspaceId);
|
||||
return spaceDB.adapter.serverClock.set(key, data);
|
||||
},
|
||||
getServerClockKeys: async (spaceType: SpaceType, workspaceId: string) => {
|
||||
const spaceDB = await ensureSQLiteDB(spaceType, workspaceId);
|
||||
return spaceDB.adapter.serverClock.keys();
|
||||
},
|
||||
clearServerClock: async (spaceType: SpaceType, workspaceId: string) => {
|
||||
const spaceDB = await ensureSQLiteDB(spaceType, workspaceId);
|
||||
return spaceDB.adapter.serverClock.clear();
|
||||
},
|
||||
delServerClock: async (
|
||||
spaceType: SpaceType,
|
||||
workspaceId: string,
|
||||
key: string
|
||||
) => {
|
||||
const spaceDB = await ensureSQLiteDB(spaceType, workspaceId);
|
||||
return spaceDB.adapter.serverClock.del(key);
|
||||
},
|
||||
getSyncMetadata: async (
|
||||
spaceType: SpaceType,
|
||||
workspaceId: string,
|
||||
key: string
|
||||
) => {
|
||||
const spaceDB = await ensureSQLiteDB(spaceType, workspaceId);
|
||||
return spaceDB.adapter.syncMetadata.get(key);
|
||||
},
|
||||
setSyncMetadata: async (
|
||||
spaceType: SpaceType,
|
||||
workspaceId: string,
|
||||
key: string,
|
||||
data: Uint8Array
|
||||
) => {
|
||||
const spaceDB = await ensureSQLiteDB(spaceType, workspaceId);
|
||||
return spaceDB.adapter.syncMetadata.set(key, data);
|
||||
},
|
||||
getSyncMetadataKeys: async (spaceType: SpaceType, workspaceId: string) => {
|
||||
const spaceDB = await ensureSQLiteDB(spaceType, workspaceId);
|
||||
return spaceDB.adapter.syncMetadata.keys();
|
||||
},
|
||||
clearSyncMetadata: async (spaceType: SpaceType, workspaceId: string) => {
|
||||
const spaceDB = await ensureSQLiteDB(spaceType, workspaceId);
|
||||
return spaceDB.adapter.syncMetadata.clear();
|
||||
},
|
||||
delSyncMetadata: async (
|
||||
spaceType: SpaceType,
|
||||
workspaceId: string,
|
||||
key: string
|
||||
) => {
|
||||
const spaceDB = await ensureSQLiteDB(spaceType, workspaceId);
|
||||
return spaceDB.adapter.syncMetadata.del(key);
|
||||
},
|
||||
};
|
||||
|
||||
export const dbEvents = {} satisfies Record<string, MainEventRegister>;
|
||||
|
||||
@@ -67,6 +67,10 @@ export class WorkspaceSQLiteDB {
|
||||
}
|
||||
};
|
||||
|
||||
async getDocTimestamps() {
|
||||
return this.adapter.getDocTimestamps();
|
||||
}
|
||||
|
||||
async addBlob(key: string, value: Uint8Array) {
|
||||
this.update$.next();
|
||||
const res = await this.adapter.addBlob(key, value);
|
||||
|
||||
@@ -1,27 +1,63 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import { parseUniversalId } from '@affine/nbstore';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import { logger } from '../logger';
|
||||
import { ensureSQLiteDB } from '../nbstore/v1/ensure-db';
|
||||
import { getDocStoragePool } from '../nbstore';
|
||||
import { ensureSQLiteDisconnected } from '../nbstore/v1/ensure-db';
|
||||
import type { WorkspaceMeta } from '../type';
|
||||
import {
|
||||
getDeletedWorkspacesBasePath,
|
||||
getSpaceDBPath,
|
||||
getWorkspaceBasePathV1,
|
||||
getWorkspaceMeta,
|
||||
} from './meta';
|
||||
|
||||
export async function deleteWorkspace(id: string) {
|
||||
const basePath = await getWorkspaceBasePathV1('workspace', id);
|
||||
async function deleteWorkspaceV1(workspaceId: string) {
|
||||
try {
|
||||
await ensureSQLiteDisconnected('workspace', workspaceId);
|
||||
const basePath = await getWorkspaceBasePathV1('workspace', workspaceId);
|
||||
await fs.rmdir(basePath, { recursive: true });
|
||||
} catch (error) {
|
||||
logger.error('deleteWorkspaceV1', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Permanently delete the workspace data
|
||||
*/
|
||||
export async function deleteWorkspace(universalId: string) {
|
||||
const { peer, type, id } = parseUniversalId(universalId);
|
||||
await deleteWorkspaceV1(id);
|
||||
|
||||
const dbPath = await getSpaceDBPath(peer, type, id);
|
||||
try {
|
||||
await getDocStoragePool().disconnect(universalId);
|
||||
await fs.rmdir(path.dirname(dbPath), { recursive: true });
|
||||
} catch (e) {
|
||||
logger.error('deleteWorkspace', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the workspace folder to `deleted-workspaces`
|
||||
* At the same time, permanently delete the v1 workspace folder if it's id exists in nbstore,
|
||||
* because trashing always happens after full sync from v1 to nbstore.
|
||||
*/
|
||||
export async function trashWorkspace(universalId: string) {
|
||||
const { peer, type, id } = parseUniversalId(universalId);
|
||||
await deleteWorkspaceV1(id);
|
||||
|
||||
const dbPath = await getSpaceDBPath(peer, type, id);
|
||||
const movedPath = path.join(await getDeletedWorkspacesBasePath(), `${id}`);
|
||||
try {
|
||||
const db = await ensureSQLiteDB('workspace', id);
|
||||
await db.destroy();
|
||||
return await fs.move(basePath, movedPath, {
|
||||
await getDocStoragePool().disconnect(universalId);
|
||||
return await fs.move(path.dirname(dbPath), movedPath, {
|
||||
overwrite: true,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('deleteWorkspace', error);
|
||||
logger.error('trashWorkspace', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { MainEventRegister } from '../type';
|
||||
import { deleteWorkspace } from './handlers';
|
||||
import { deleteWorkspace, trashWorkspace } from './handlers';
|
||||
|
||||
export * from './handlers';
|
||||
export * from './subjects';
|
||||
@@ -7,5 +7,6 @@ export * from './subjects';
|
||||
export const workspaceEvents = {} as Record<string, MainEventRegister>;
|
||||
|
||||
export const workspaceHandlers = {
|
||||
delete: async (id: string) => deleteWorkspace(id),
|
||||
delete: deleteWorkspace,
|
||||
moveToTrash: trashWorkspace,
|
||||
};
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import type { SpaceType } from '@affine/nbstore';
|
||||
import fs from 'fs-extra';
|
||||
import { type SpaceType } from '@affine/nbstore';
|
||||
|
||||
import { isWindows } from '../../shared/utils';
|
||||
import { logger } from '../logger';
|
||||
import { mainRPC } from '../main-rpc';
|
||||
import type { WorkspaceMeta } from '../type';
|
||||
|
||||
@@ -94,31 +92,10 @@ export async function getWorkspaceMeta(
|
||||
spaceType: SpaceType,
|
||||
workspaceId: string
|
||||
): Promise<WorkspaceMeta> {
|
||||
try {
|
||||
const basePath = await getWorkspaceBasePathV1(spaceType, workspaceId);
|
||||
const metaPath = await getWorkspaceMetaPath(spaceType, workspaceId);
|
||||
if (
|
||||
!(await fs
|
||||
.access(metaPath)
|
||||
.then(() => true)
|
||||
.catch(() => false))
|
||||
) {
|
||||
await fs.ensureDir(basePath);
|
||||
const dbPath = await getWorkspaceDBPath(spaceType, workspaceId);
|
||||
// create one if not exists
|
||||
const meta = {
|
||||
id: workspaceId,
|
||||
mainDBPath: dbPath,
|
||||
type: spaceType,
|
||||
};
|
||||
await fs.writeJSON(metaPath, meta);
|
||||
return meta;
|
||||
} else {
|
||||
const meta = await fs.readJSON(metaPath);
|
||||
return meta;
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('getWorkspaceMeta failed', err);
|
||||
throw err;
|
||||
}
|
||||
const dbPath = await getWorkspaceDBPath(spaceType, workspaceId);
|
||||
|
||||
return {
|
||||
mainDBPath: dbPath,
|
||||
id: workspaceId,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user