fix(import): ignore AppleDouble database sidecars

This commit is contained in:
digua committed 2026-08-01 17:00:09 +08:00
1 parent 8eae64df8e
commit 1e339875e1
4 files changed
+48 -41

No files matched your search

@@ -20,6 +20,7 @@ import {
computeAndSetOverviewCache,
createImportPerfLogger,
deleteSessionCache,
listDatabaseCandidateIds,
} from '@openchatlab/node-runtime'
import type {
AutoImportBatchItemResult,
@@ -131,13 +132,7 @@ export async function autoImport(
return sharedAutoImportFile(
filePath,
{
listSessionIds: () =>
fs.existsSync(getDbDir())
? fs
.readdirSync(getDbDir())
.filter((name) => name.endsWith('.db'))
.map((name) => name.slice(0, -3))
: [],
listSessionIds: () => listDatabaseCandidateIds(getDbDir()),
openReadonly: (sessionId) => new BetterSqliteAdapter(openRawDatabase(getDbPath(sessionId), { readonly: true })),
onProgress: (progress) => sendProgress(requestId, progress),
sessionExists: (sessionId) => fs.existsSync(getDbPath(sessionId)),
@@ -183,13 +178,7 @@ export async function autoImportBatch(
},
})),
{
listSessionIds: () =>
fs.existsSync(getDbDir())
? fs
.readdirSync(getDbDir())
.filter((name) => name.endsWith('.db'))
.map((name) => name.slice(0, -3))
: [],
listSessionIds: () => listDatabaseCandidateIds(getDbDir()),
openReadonly: (sessionId) => new BetterSqliteAdapter(openRawDatabase(getDbPath(sessionId), { readonly: true })),
sessionExists: (sessionId) => fs.existsSync(getDbPath(sessionId)),
createSession: (sourcePath, sourceFormatOptions, sessionId, itemProgress) =>
@@ -7,7 +7,7 @@ import Database from 'better-sqlite3'
import type { PathProvider } from '@openchatlab/core'
import { CHAT_DB_SCHEMA, CURRENT_SCHEMA_VERSION, getSessionInfo } from '@openchatlab/core'
import { DataDirCompatibilityError, readDataDirCompatibilityMeta } from './data-dir-compat'
import { DatabaseManager } from './database-manager'
import { DatabaseManager, listDatabaseCandidateIds } from './database-manager'
const nativeBinding = path.resolve('apps/cli/native/better_sqlite3.node')
@@ -1031,3 +1031,20 @@ test('listSessionIds ignores non-ChatLab sqlite databases without migrating them
assert.deepEqual(manager.listSessionIds(), [])
manager.closeAll()
})
test('database candidate enumeration ignores AppleDouble sidecars but keeps damaged databases visible', () => {
const root = makeTempDir()
const dbDir = path.join(root, 'data', 'databases')
fs.mkdirSync(dbDir, { recursive: true })
fs.writeFileSync(path.join(dbDir, '._chat_existing.db'), Buffer.from([0x00, 0x05, 0x16, 0x07]))
const manager = new DatabaseManager(createPathProvider(root), { nativeBinding, allowMissingRuntimeForTests: true })
assert.deepEqual(listDatabaseCandidateIds(dbDir), [])
assert.deepEqual(manager.listSessionIds(), [])
assert.deepEqual(manager.listSessionIdsReadonly(), [])
fs.writeFileSync(path.join(dbDir, 'damaged.db'), 'not a sqlite database')
assert.deepEqual(listDatabaseCandidateIds(dbDir), ['damaged'])
assert.throws(() => manager.listSessionIds(), /file is not a database/)
assert.throws(() => manager.listSessionIdsReadonly(), /file is not a database/)
})
+26 -25
View File
@@ -52,6 +52,19 @@ interface OpenRawSessionDatabaseOptions {
initializeChatTables?: boolean
}
/**
* List database candidates that require further inspection.
* macOS AppleDouble `._*` files contain metadata rather than SQLite data.
*/
export function listDatabaseCandidateIds(databaseDir: string): string[] {
if (!fs.existsSync(databaseDir)) return []
return fs
.readdirSync(databaseDir)
.filter((fileName) => fileName.endsWith('.db') && !fileName.startsWith('._'))
.map((fileName) => fileName.slice(0, -3))
}
export class DatabaseManager {
private cache = new Map<string, DatabaseAdapter>()
private nativeBinding?: string
@@ -185,17 +198,11 @@ export class DatabaseManager {
this.assertCompatible()
const dbDir = this.pathProvider.getDatabaseDir()
if (!fs.existsSync(dbDir)) return []
return fs
.readdirSync(dbDir)
.filter((f) => f.endsWith('.db'))
.map((f) => f.replace('.db', ''))
.filter((id) => {
const db = this.open(id)
if (!db) return false
return isChatSessionDb(db)
})
return listDatabaseCandidateIds(dbDir).filter((id) => {
const db = this.open(id)
if (!db) return false
return isChatSessionDb(db)
})
}
/**
@@ -205,20 +212,14 @@ export class DatabaseManager {
this.assertCompatible()
const dbDir = this.pathProvider.getDatabaseDir()
if (!fs.existsSync(dbDir)) return []
return fs
.readdirSync(dbDir)
.filter((fileName) => fileName.endsWith('.db'))
.map((fileName) => fileName.replace('.db', ''))
.filter((sessionId) => {
const db = this.openRawSessionDatabase(sessionId, { readonly: true })
try {
return isChatSessionDb(db)
} finally {
db.close()
}
})
return listDatabaseCandidateIds(dbDir).filter((sessionId) => {
const db = this.openRawSessionDatabase(sessionId, { readonly: true })
try {
return isChatSessionDb(db)
} finally {
db.close()
}
})
}
getUserDataDir(): string {
+1 -1
View File
@@ -120,7 +120,7 @@ export {
getSystemLogsDir,
hasPendingElectronDataWarning,
} from './node-path-provider'
export { DatabaseManager } from './database-manager'
export { DatabaseManager, listDatabaseCandidateIds } from './database-manager'
export { createJiebaNlpProvider } from './jieba-nlp-provider'
export {
applyPendingNodeDataDirMigration,