mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-02 02:00:49 +08:00
fix: old workspace migration (#14403)
fix #14395 #### PR Dependency Tree * **PR #14403** 👈 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 ability to enumerate and list local workspaces. * Improved workspace ID persistence with Electron global-state storage, automatic fallback to legacy storage, and one-time migration to consolidate IDs. * **Tests** * Added unit test validating listing behavior (includes/excludes workspaces based on presence of workspace DB file). <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -15,6 +15,7 @@ import { WorkspaceSQLiteDB } from '../nbstore/v1/workspace-db-adapter';
|
||||
import type { WorkspaceMeta } from '../type';
|
||||
import {
|
||||
getDeletedWorkspacesBasePath,
|
||||
getSpaceBasePath,
|
||||
getSpaceDBPath,
|
||||
getWorkspaceBasePathV1,
|
||||
getWorkspaceMeta,
|
||||
@@ -96,6 +97,33 @@ export async function storeWorkspaceMeta(
|
||||
}
|
||||
}
|
||||
|
||||
export async function listLocalWorkspaceIds(): Promise<string[]> {
|
||||
const localWorkspaceBasePath = path.join(
|
||||
await getSpaceBasePath('workspace'),
|
||||
'local'
|
||||
);
|
||||
if (!(await fs.pathExists(localWorkspaceBasePath))) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const entries = await fs.readdir(localWorkspaceBasePath);
|
||||
const ids = await Promise.all(
|
||||
entries.map(async entry => {
|
||||
const workspacePath = path.join(localWorkspaceBasePath, entry);
|
||||
const stat = await fs.stat(workspacePath).catch(() => null);
|
||||
if (!stat?.isDirectory()) {
|
||||
return null;
|
||||
}
|
||||
if (!(await fs.pathExists(path.join(workspacePath, 'storage.db')))) {
|
||||
return null;
|
||||
}
|
||||
return entry;
|
||||
})
|
||||
);
|
||||
|
||||
return ids.filter((id): id is string => typeof id === 'string');
|
||||
}
|
||||
|
||||
type WorkspaceDocMeta = {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
deleteBackupWorkspace,
|
||||
deleteWorkspace,
|
||||
getDeletedWorkspaces,
|
||||
listLocalWorkspaceIds,
|
||||
trashWorkspace,
|
||||
} from './handlers';
|
||||
|
||||
@@ -18,4 +19,5 @@ export const workspaceHandlers = {
|
||||
return getDeletedWorkspaces();
|
||||
},
|
||||
deleteBackupWorkspace: async (id: string) => deleteBackupWorkspace(id),
|
||||
listLocalWorkspaceIds: async () => listLocalWorkspaceIds(),
|
||||
};
|
||||
|
||||
@@ -33,6 +33,43 @@ afterAll(() => {
|
||||
});
|
||||
|
||||
describe('workspace db management', () => {
|
||||
test('list local workspace ids', async () => {
|
||||
const { listLocalWorkspaceIds } =
|
||||
await import('@affine/electron/helper/workspace/handlers');
|
||||
const validWorkspaceId = v4();
|
||||
const noDbWorkspaceId = v4();
|
||||
const fileEntry = 'README.txt';
|
||||
|
||||
const validWorkspacePath = path.join(
|
||||
appDataPath,
|
||||
'workspaces',
|
||||
'local',
|
||||
validWorkspaceId
|
||||
);
|
||||
const noDbWorkspacePath = path.join(
|
||||
appDataPath,
|
||||
'workspaces',
|
||||
'local',
|
||||
noDbWorkspaceId
|
||||
);
|
||||
const nonDirectoryPath = path.join(
|
||||
appDataPath,
|
||||
'workspaces',
|
||||
'local',
|
||||
fileEntry
|
||||
);
|
||||
|
||||
await fs.ensureDir(validWorkspacePath);
|
||||
await fs.ensureFile(path.join(validWorkspacePath, 'storage.db'));
|
||||
await fs.ensureDir(noDbWorkspacePath);
|
||||
await fs.outputFile(nonDirectoryPath, 'not-a-workspace');
|
||||
|
||||
const ids = await listLocalWorkspaceIds();
|
||||
expect(ids).toContain(validWorkspaceId);
|
||||
expect(ids).not.toContain(noDbWorkspaceId);
|
||||
expect(ids).not.toContain(fileEntry);
|
||||
});
|
||||
|
||||
test('trash workspace', async () => {
|
||||
const { trashWorkspace } =
|
||||
await import('@affine/electron/helper/workspace/handlers');
|
||||
|
||||
@@ -48,15 +48,44 @@ import { WorkspaceImpl } from '../../workspace/impls/workspace';
|
||||
import { getWorkspaceProfileWorker } from './out-worker';
|
||||
|
||||
export const LOCAL_WORKSPACE_LOCAL_STORAGE_KEY = 'affine-local-workspace';
|
||||
export const LOCAL_WORKSPACE_GLOBAL_STATE_KEY =
|
||||
'workspace-engine:local-workspace-ids:v1';
|
||||
const LOCAL_WORKSPACE_CHANGED_BROADCAST_CHANNEL_KEY =
|
||||
'affine-local-workspace-changed';
|
||||
|
||||
const logger = new DebugLogger('local-workspace');
|
||||
|
||||
export function getLocalWorkspaceIds(): string[] {
|
||||
type GlobalStateStorageLike = {
|
||||
ready: Promise<void>;
|
||||
get<T>(key: string): T | undefined;
|
||||
set<T>(key: string, value: T): void;
|
||||
};
|
||||
|
||||
function normalizeWorkspaceIds(ids: unknown): string[] {
|
||||
if (!Array.isArray(ids)) {
|
||||
return [];
|
||||
}
|
||||
return ids.filter((id): id is string => typeof id === 'string');
|
||||
}
|
||||
|
||||
function getElectronGlobalStateStorage(): GlobalStateStorageLike | null {
|
||||
if (!BUILD_CONFIG.isElectron) {
|
||||
return null;
|
||||
}
|
||||
const sharedStorage = (
|
||||
globalThis as {
|
||||
__sharedStorage?: { globalState?: GlobalStateStorageLike };
|
||||
}
|
||||
).__sharedStorage;
|
||||
return sharedStorage?.globalState ?? null;
|
||||
}
|
||||
|
||||
function getLegacyLocalWorkspaceIds(): string[] {
|
||||
try {
|
||||
return JSON.parse(
|
||||
localStorage.getItem(LOCAL_WORKSPACE_LOCAL_STORAGE_KEY) ?? '[]'
|
||||
return normalizeWorkspaceIds(
|
||||
JSON.parse(
|
||||
localStorage.getItem(LOCAL_WORKSPACE_LOCAL_STORAGE_KEY) ?? '[]'
|
||||
)
|
||||
);
|
||||
} catch (e) {
|
||||
logger.error('Failed to get local workspace ids', e);
|
||||
@@ -64,21 +93,98 @@ export function getLocalWorkspaceIds(): string[] {
|
||||
}
|
||||
}
|
||||
|
||||
export function getLocalWorkspaceIds(): string[] {
|
||||
const globalState = getElectronGlobalStateStorage();
|
||||
if (globalState) {
|
||||
const value = globalState.get(LOCAL_WORKSPACE_GLOBAL_STATE_KEY);
|
||||
if (value !== undefined) {
|
||||
return normalizeWorkspaceIds(value);
|
||||
}
|
||||
}
|
||||
|
||||
return getLegacyLocalWorkspaceIds();
|
||||
}
|
||||
|
||||
export function setLocalWorkspaceIds(
|
||||
idsOrUpdater: string[] | ((ids: string[]) => string[])
|
||||
) {
|
||||
localStorage.setItem(
|
||||
LOCAL_WORKSPACE_LOCAL_STORAGE_KEY,
|
||||
JSON.stringify(
|
||||
typeof idsOrUpdater === 'function'
|
||||
? idsOrUpdater(getLocalWorkspaceIds())
|
||||
: idsOrUpdater
|
||||
)
|
||||
const next = normalizeWorkspaceIds(
|
||||
typeof idsOrUpdater === 'function'
|
||||
? idsOrUpdater(getLocalWorkspaceIds())
|
||||
: idsOrUpdater
|
||||
);
|
||||
const deduplicated = [...new Set(next)];
|
||||
|
||||
const globalState = getElectronGlobalStateStorage();
|
||||
if (globalState) {
|
||||
globalState.set(LOCAL_WORKSPACE_GLOBAL_STATE_KEY, deduplicated);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
localStorage.setItem(
|
||||
LOCAL_WORKSPACE_LOCAL_STORAGE_KEY,
|
||||
JSON.stringify(deduplicated)
|
||||
);
|
||||
} catch (e) {
|
||||
logger.error('Failed to set local workspace ids', e);
|
||||
}
|
||||
}
|
||||
|
||||
class LocalWorkspaceFlavourProvider implements WorkspaceFlavourProvider {
|
||||
constructor(private readonly framework: FrameworkProvider) {}
|
||||
constructor(private readonly framework: FrameworkProvider) {
|
||||
if (BUILD_CONFIG.isElectron) {
|
||||
void this.ensureWorkspaceIdsMigrated();
|
||||
}
|
||||
}
|
||||
|
||||
private migration: Promise<void> | null = null;
|
||||
|
||||
private ensureWorkspaceIdsMigrated() {
|
||||
if (!BUILD_CONFIG.isElectron) {
|
||||
return;
|
||||
}
|
||||
if (this.migration) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.migration = (async () => {
|
||||
const electronApi = this.framework.get(DesktopApiService);
|
||||
await electronApi.sharedStorage.globalState.ready;
|
||||
|
||||
const persistedIds = normalizeWorkspaceIds(
|
||||
electronApi.sharedStorage.globalState.get(
|
||||
LOCAL_WORKSPACE_GLOBAL_STATE_KEY
|
||||
)
|
||||
);
|
||||
const legacyIds = getLegacyLocalWorkspaceIds();
|
||||
|
||||
let scannedIds: string[] = [];
|
||||
try {
|
||||
scannedIds =
|
||||
await electronApi.handler.workspace.listLocalWorkspaceIds();
|
||||
} catch (e) {
|
||||
logger.error('Failed to scan local workspace ids', e);
|
||||
}
|
||||
|
||||
setLocalWorkspaceIds(currentIds => {
|
||||
return [
|
||||
...new Set([
|
||||
...currentIds,
|
||||
...persistedIds,
|
||||
...legacyIds,
|
||||
...scannedIds,
|
||||
]),
|
||||
];
|
||||
});
|
||||
})()
|
||||
.catch(e => {
|
||||
logger.error('Failed to migrate local workspace ids', e);
|
||||
})
|
||||
.finally(() => {
|
||||
this.notifyChannel.postMessage(null);
|
||||
});
|
||||
}
|
||||
|
||||
readonly flavour = 'local';
|
||||
readonly notifyChannel = new BroadcastChannel(
|
||||
@@ -242,6 +348,9 @@ class LocalWorkspaceFlavourProvider implements WorkspaceFlavourProvider {
|
||||
);
|
||||
isRevalidating$ = new LiveData(false);
|
||||
revalidate(): void {
|
||||
if (BUILD_CONFIG.isElectron) {
|
||||
void this.ensureWorkspaceIdsMigrated();
|
||||
}
|
||||
// notify livedata to re-scan workspaces
|
||||
this.notifyChannel.postMessage(null);
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import { GlobalState } from '../storage';
|
||||
import { WorkspaceFlavoursProvider } from '../workspace';
|
||||
import { CloudWorkspaceFlavoursProvider } from './impls/cloud';
|
||||
import {
|
||||
LOCAL_WORKSPACE_LOCAL_STORAGE_KEY,
|
||||
LocalWorkspaceFlavoursProvider,
|
||||
setLocalWorkspaceIds,
|
||||
} from './impls/local';
|
||||
|
||||
export { base64ToUint8Array, uint8ArrayToBase64 } from './utils/base64';
|
||||
@@ -25,12 +25,5 @@ export function configureBrowserWorkspaceFlavours(framework: Framework) {
|
||||
* Used after copying sqlite database file to appdata folder
|
||||
*/
|
||||
export function _addLocalWorkspace(id: string) {
|
||||
const allWorkspaceIDs: string[] = JSON.parse(
|
||||
localStorage.getItem(LOCAL_WORKSPACE_LOCAL_STORAGE_KEY) ?? '[]'
|
||||
);
|
||||
allWorkspaceIDs.push(id);
|
||||
localStorage.setItem(
|
||||
LOCAL_WORKSPACE_LOCAL_STORAGE_KEY,
|
||||
JSON.stringify(allWorkspaceIDs)
|
||||
);
|
||||
setLocalWorkspaceIds(ids => (ids.includes(id) ? ids : [...ids, id]));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user