diff --git a/packages/http-routes/src/routes/web/cache.test.ts b/packages/http-routes/src/routes/web/cache.test.ts index fb8f7160..11cd0e08 100644 --- a/packages/http-routes/src/routes/web/cache.test.ts +++ b/packages/http-routes/src/routes/web/cache.test.ts @@ -1,11 +1,14 @@ import { describe, it } from 'node:test' import assert from 'node:assert/strict' +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' import Fastify from 'fastify' import type { PathProvider } from '@openchatlab/core' import type { HttpRouteContext } from '../../context' import { registerCacheRoutes } from './cache' -function createPathProvider(): PathProvider { +function createPathProvider(overrides: Partial = {}): PathProvider { return { getSystemDir: () => '/tmp/chatlab-test', getUserDataDir: () => '/tmp/chatlab-test/data', @@ -16,9 +19,15 @@ function createPathProvider(): PathProvider { getTempDir: () => '/tmp/chatlab-test/temp', getLogsDir: () => '/tmp/chatlab-test/logs', getDownloadsDir: () => '/tmp/chatlab-test/downloads', + ...overrides, } } +function makeTempDir(): string { + const baseDir = fs.existsSync('/private/tmp') ? '/private/tmp' : os.tmpdir() + return fs.mkdtempSync(path.join(baseDir, 'chatlab-cache-routes-')) +} + describe('registerCacheRoutes data directory routes', () => { it('returns data directory capability and pending migration', async () => { const app = Fastify() @@ -45,6 +54,7 @@ describe('registerCacheRoutes data directory routes', () => { defaultPath: '/tmp/chatlab-test/default-data', isCustom: true, canSetDataDir: true, + hasLegacyDataAtDefaultDir: false, pendingMigration: { from: '/tmp/chatlab-test/data', to: '/tmp/chatlab-test/new-data', @@ -55,6 +65,42 @@ describe('registerCacheRoutes data directory routes', () => { await app.close() }) + it('reports legacy data at default directory only when default databases contain db files', async () => { + const root = makeTempDir() + const currentDir = path.join(root, 'custom-data') + const defaultDir = path.join(root, 'default-data') + fs.mkdirSync(path.join(defaultDir, 'databases'), { recursive: true }) + fs.writeFileSync(path.join(defaultDir, '.chatlab'), 'ChatLab Data Directory') + + const appWithoutDb = Fastify() + registerCacheRoutes(appWithoutDb, { + pathProvider: createPathProvider({ getUserDataDir: () => currentDir }), + defaultUserDataDir: defaultDir, + isCustomDataDir: true, + } as unknown as HttpRouteContext) + await appWithoutDb.ready() + + const emptyResponse = await appWithoutDb.inject({ method: 'GET', url: '/_web/cache/data-dir' }) + assert.equal(emptyResponse.statusCode, 200) + assert.equal(emptyResponse.json().hasLegacyDataAtDefaultDir, false) + await appWithoutDb.close() + + fs.writeFileSync(path.join(defaultDir, 'databases', 'legacy.db'), 'sqlite') + + const appWithDb = Fastify() + registerCacheRoutes(appWithDb, { + pathProvider: createPathProvider({ getUserDataDir: () => currentDir }), + defaultUserDataDir: defaultDir, + isCustomDataDir: true, + } as unknown as HttpRouteContext) + await appWithDb.ready() + + const response = await appWithDb.inject({ method: 'GET', url: '/_web/cache/data-dir' }) + assert.equal(response.statusCode, 200) + assert.equal(response.json().hasLegacyDataAtDefaultDir, true) + await appWithDb.close() + }) + it('delegates data directory changes to context callback', async () => { const app = Fastify() const calls: Array<{ path: string | null; migrate?: boolean }> = [] diff --git a/packages/http-routes/src/routes/web/cache.ts b/packages/http-routes/src/routes/web/cache.ts index 24c62dcc..8d2443f0 100644 --- a/packages/http-routes/src/routes/web/cache.ts +++ b/packages/http-routes/src/routes/web/cache.ts @@ -43,6 +43,17 @@ async function getFileCount(dirPath: string): Promise { return count } +function hasChatLabDatabases(dirPath: string): boolean { + try { + const markerPath = path.join(dirPath, '.chatlab') + const dbDir = path.join(dirPath, 'databases') + if (!fs.existsSync(markerPath) || !fs.existsSync(dbDir)) return false + return fs.readdirSync(dbDir).some((file) => file.endsWith('.db')) + } catch { + return false + } +} + export function registerCacheRoutes(server: FastifyInstance, ctx: HttpRouteContext): void { const pp = ctx.pathProvider const downloadsDir = ctx.downloadsDir ?? pp.getDownloadsDir() @@ -128,6 +139,10 @@ export function registerCacheRoutes(server: FastifyInstance, ctx: HttpRouteConte defaultPath: ctx.defaultUserDataDir, isCustom: ctx.isCustomDataDir ?? false, canSetDataDir: ctx.canSetDataDir ?? Boolean(ctx.setDataDir), + hasLegacyDataAtDefaultDir: + Boolean(ctx.defaultUserDataDir) && + path.resolve(pp.getUserDataDir()) !== path.resolve(ctx.defaultUserDataDir ?? '') && + hasChatLabDatabases(ctx.defaultUserDataDir ?? ''), pendingMigration: pending ? { from: pending.from, diff --git a/src/components/common/Settings/DataStorage/StorageManageSection.vue b/src/components/common/Settings/DataStorage/StorageManageSection.vue index f4988ae4..2f87abeb 100644 --- a/src/components/common/Settings/DataStorage/StorageManageSection.vue +++ b/src/components/common/Settings/DataStorage/StorageManageSection.vue @@ -20,6 +20,7 @@ const clearingId = ref(null) const dataDir = ref('') const dataDirInput = ref('') const defaultDataDir = ref('') +const hasLegacyDataAtDefaultDir = ref(false) const isCustomDataDir = ref(false) const canSetDataDir = ref(false) const isUpdatingDataDir = ref(false) @@ -72,6 +73,7 @@ const canMigrateToDefault = computed(() => { dataDir.value && defaultDataDir.value && dataDir.value !== defaultDataDir.value && + hasLegacyDataAtDefaultDir.value && !isMigrateHintIgnored.value ) }) @@ -95,6 +97,7 @@ async function loadDataDir() { dataDir.value = info.path dataDirInput.value = info.pendingMigration?.to || info.path defaultDataDir.value = info.defaultPath || '' + hasLegacyDataAtDefaultDir.value = Boolean(info.hasLegacyDataAtDefaultDir) isCustomDataDir.value = info.isCustom canSetDataDir.value = IS_ELECTRON || Boolean(info.canSetDataDir) updateMigrateHintIgnored() diff --git a/src/services/cache/types.ts b/src/services/cache/types.ts index c0d52544..a008c5d6 100644 --- a/src/services/cache/types.ts +++ b/src/services/cache/types.ts @@ -21,6 +21,7 @@ export interface DataDirInfo { defaultPath?: string isCustom: boolean canSetDataDir?: boolean + hasLegacyDataAtDefaultDir?: boolean pendingMigration?: { from: string to: string