mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
fix(import): keep dry-run database access read-only
Use non-migrating session enumeration during import analysis so previews cannot mutate legacy schemas or raise data-directory compatibility gates.
This commit is contained in:
1 parent
53406f4c6d
commit
82710e2d5a
3 files changed
+83
-2
No files matched your search
@@ -3,6 +3,7 @@ import fs from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import test from 'node:test'
|
||||
import Database from 'better-sqlite3'
|
||||
import type { PathProvider } from '@openchatlab/core'
|
||||
import {
|
||||
DatabaseManager,
|
||||
@@ -161,6 +162,61 @@ test('analyzeAutoImport previews a new session without writing a database', asyn
|
||||
assert.deepEqual(manager.listSessionIds(), [])
|
||||
})
|
||||
|
||||
test('analyzeAutoImport does not migrate legacy session databases', async (t) => {
|
||||
const root = makeTempDir()
|
||||
t.after(() => fs.rmSync(root, { recursive: true, force: true }))
|
||||
const dbDir = path.join(root, 'data', 'databases')
|
||||
fs.mkdirSync(dbDir, { recursive: true })
|
||||
const dbPath = path.join(dbDir, 'legacy.db')
|
||||
const legacyDb = new Database(dbPath, { nativeBinding })
|
||||
legacyDb.exec(`
|
||||
CREATE TABLE meta (
|
||||
name TEXT NOT NULL,
|
||||
platform TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
imported_at INTEGER NOT NULL,
|
||||
schema_version INTEGER DEFAULT 4
|
||||
);
|
||||
INSERT INTO meta (name, platform, type, imported_at, schema_version)
|
||||
VALUES ('Legacy Chat', 'qq', 'group', 1000, 4);
|
||||
|
||||
CREATE TABLE member (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
platform_id TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
nickname TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE message (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sender_id INTEGER NOT NULL,
|
||||
ts INTEGER NOT NULL,
|
||||
type INTEGER NOT NULL,
|
||||
content TEXT
|
||||
);
|
||||
`)
|
||||
legacyDb.close()
|
||||
|
||||
const manager = new DatabaseManager(createPathProvider(root), {
|
||||
nativeBinding,
|
||||
runtime: { version: '0.25.1', kind: 'cli' },
|
||||
})
|
||||
const result = await analyzeAutoImport(manager, writeTempChatFile(root), { nativeBinding })
|
||||
|
||||
assert.equal(result.success, true)
|
||||
const unchangedDb = new Database(dbPath, { readonly: true, nativeBinding })
|
||||
const schemaVersion = unchangedDb.prepare('SELECT schema_version FROM meta').pluck().get()
|
||||
const memberColumns = unchangedDb.pragma('table_info(member)') as Array<{ name: string }>
|
||||
unchangedDb.close()
|
||||
|
||||
assert.equal(schemaVersion, 4)
|
||||
assert.equal(
|
||||
memberColumns.some((column) => column.name === 'account_name'),
|
||||
false
|
||||
)
|
||||
assert.equal(readDataDirCompatibilityMeta(path.join(root, 'data')), null)
|
||||
})
|
||||
|
||||
test('autoImport reports the same exact-message deduplication on create and incremental paths', async (t) => {
|
||||
const root = makeTempDir()
|
||||
t.after(() => fs.rmSync(root, { recursive: true, force: true }))
|
||||
|
||||
@@ -238,14 +238,16 @@ export async function analyzeAutoImport(
|
||||
if (formatId) formatOptions.formatId = formatId
|
||||
if (chatIndex !== undefined) formatOptions.chatIndex = chatIndex
|
||||
const progressAdapter = createProgressAdapter(onProgress)
|
||||
let readonlySessionIds: string[] | undefined
|
||||
const listSessionIdsReadonly = (): string[] => (readonlySessionIds ??= dbManager.listSessionIdsReadonly())
|
||||
|
||||
return sharedAnalyzeAutoImportFile(
|
||||
filePath,
|
||||
{
|
||||
listSessionIds: () => dbManager.listSessionIds(),
|
||||
listSessionIds: listSessionIdsReadonly,
|
||||
openReadonly: (candidateSessionId) => dbManager.openRawSessionDatabase(candidateSessionId, { readonly: true }),
|
||||
onProgress: progressAdapter,
|
||||
sessionExists: (candidateSessionId) => dbManager.listSessionIds().includes(candidateSessionId),
|
||||
sessionExists: (candidateSessionId) => listSessionIdsReadonly().includes(candidateSessionId),
|
||||
analyzeCreateSession: (sourcePath, sourceFormatOptions) =>
|
||||
sharedAnalyzeNewImport(sourcePath, progressAdapter, {
|
||||
formatId: typeof sourceFormatOptions?.formatId === 'string' ? sourceFormatOptions.formatId : undefined,
|
||||
|
||||
@@ -198,6 +198,29 @@ export class DatabaseManager {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 只读列举聊天会话 ID,不触发数据库迁移或兼容门禁提升。
|
||||
*/
|
||||
listSessionIdsReadonly(): string[] {
|
||||
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()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getUserDataDir(): string {
|
||||
return this.pathProvider.getUserDataDir()
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user