fix(sync): delete imported session files completely

This commit is contained in:
digua committed 2026-06-13 13:16:52 +08:00
1 parent 848a44310c
commit 82a6be4fcb
7 files changed
+124 -13

No files matched your search

+54
View File
@@ -0,0 +1,54 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import test from 'node:test'
import Fastify from 'fastify'
import { DataSourceManager } from '@openchatlab/sync'
import { registerAutomationRoutes } from './routes'
function makeTempDir(): string {
const baseDir = fs.existsSync('/private/tmp') ? '/private/tmp' : os.tmpdir()
return fs.mkdtempSync(path.join(baseDir, 'chatlab-sync-routes-'))
}
test('DELETE import session deletes imported local session when deleteData=true', async () => {
const settingsDir = makeTempDir()
const dsManager = new DataSourceManager(settingsDir)
const ds = dsManager.add({
baseUrl: 'http://example.com',
token: 'token',
intervalMinutes: 60,
})
const [session] = dsManager.addSessions(ds.id, [{ name: 'First chat', remoteSessionId: 'remote-1' }])
dsManager.updateSession(ds.id, session.id, { targetSessionId: 'local-1' })
const deletedSessionIds: string[] = []
const app = Fastify()
registerAutomationRoutes(app, {
dsManager,
pullEngine: {
triggerPull: async () => ({ success: true, newMessageCount: 0 }),
triggerPullAll: async () => ({ success: true, newMessageCount: 0 }),
getProgress: () => [],
} as any,
dbManager: {
deleteSessionDatabaseFiles: (sessionId: string) => {
deletedSessionIds.push(sessionId)
},
} as any,
serverInfo: { port: 5200, host: '127.0.0.1', token: 'api-token' },
})
await app.ready()
const resp = await app.inject({
method: 'DELETE',
url: `/_web/automation/data-sources/${ds.id}/sessions/${session.id}?deleteData=true`,
})
await app.close()
assert.equal(resp.statusCode, 200)
assert.deepEqual(deletedSessionIds, ['local-1'])
assert.equal(dsManager.get(ds.id)?.sessions.length, 0)
})
+1 -4
View File
@@ -91,10 +91,7 @@ export function registerAutomationRoutes(server: FastifyInstance, ctx: SyncRoute
if (!removed) return reply.code(404).send({ error: 'Session not found' })
reloadTimer(request.params.id)
if (request.query.deleteData === 'true' && removed.targetSessionId) {
dbManager.close(removed.targetSessionId)
const dbPath = dbManager.getDbPath(removed.targetSessionId)
const fs = await import('node:fs')
if (fs.existsSync(dbPath)) fs.unlinkSync(dbPath)
dbManager.deleteSessionDatabaseFiles(removed.targetSessionId)
}
return { success: true }
})
+11 -1
View File
@@ -18,6 +18,7 @@ import {
getSchemaVersion,
} from '@openchatlab/core'
import { openBetterSqliteDatabase } from './better-sqlite3-adapter'
import { deleteSessionCache } from './cache/session-cache'
import { assertDataDirCompatible, type RuntimeIdentity } from './data-dir-compat'
import {
CHAT_DB_COMPATIBILITY_RAISES,
@@ -225,10 +226,15 @@ export class DatabaseManager {
return adapter
}
deleteSessionDatabaseFiles(sessionId: string): void {
/**
* 删除一个会话在本地留下的完整文件集合。
* SQLite WAL 模式会产生 sidecar 文件,前端查询还会写 JSON 缓存;只删主库会让列表和后续同步读到脏状态。
*/
deleteSessionDatabaseFiles(sessionId: string): boolean {
this.close(sessionId)
const dbPath = this.getDbPath(sessionId)
const existed = ['', '-wal', '-shm'].some((suffix) => fs.existsSync(dbPath + suffix))
for (const suffix of ['', '-wal', '-shm']) {
try {
const filePath = dbPath + suffix
@@ -237,6 +243,10 @@ export class DatabaseManager {
/* ignore cleanup failures */
}
}
const cacheDir = this.pathProvider.getCacheDir()
deleteSessionCache(sessionId, cacheDir)
deleteSessionCache(sessionId, path.join(cacheDir, 'query'))
return existed
}
raiseCurrentChatDbCompatibilityGate(): void {
@@ -0,0 +1,55 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import test from 'node:test'
import type { PathProvider } from '@openchatlab/core'
import { DatabaseManager } from '../../database-manager'
import { createDatabaseManagerAdapter } from './database-manager-adapter'
function makeTempDir(): string {
const baseDir = fs.existsSync('/private/tmp') ? '/private/tmp' : os.tmpdir()
return fs.mkdtempSync(path.join(baseDir, 'chatlab-db-adapter-'))
}
function createPathProvider(root: string): PathProvider {
return {
getSystemDir: () => root,
getUserDataDir: () => path.join(root, 'data'),
getDatabaseDir: () => path.join(root, 'data', 'databases'),
getAiDataDir: () => path.join(root, 'ai'),
getSettingsDir: () => path.join(root, 'settings'),
getCacheDir: () => path.join(root, 'cache'),
getTempDir: () => path.join(root, 'temp'),
getLogsDir: () => path.join(root, 'logs'),
getDownloadsDir: () => path.join(root, 'downloads'),
}
}
test('DatabaseManager adapter deletes session database sidecar files and caches', () => {
const root = makeTempDir()
const pathProvider = createPathProvider(root)
const manager = new DatabaseManager(pathProvider, { allowMissingRuntimeForTests: true })
const adapter = createDatabaseManagerAdapter(manager)
const sessionId = 'synced-session'
const dbPath = manager.getDbPath(sessionId)
fs.mkdirSync(path.dirname(dbPath), { recursive: true })
for (const suffix of ['', '-wal', '-shm']) {
fs.writeFileSync(dbPath + suffix, 'stale-data', 'utf-8')
}
const cachePath = path.join(pathProvider.getCacheDir(), `${sessionId}.cache.json`)
const queryCachePath = path.join(pathProvider.getCacheDir(), 'query', `${sessionId}.cache.json`)
fs.mkdirSync(path.dirname(cachePath), { recursive: true })
fs.mkdirSync(path.dirname(queryCachePath), { recursive: true })
fs.writeFileSync(cachePath, '{}', 'utf-8')
fs.writeFileSync(queryCachePath, '{}', 'utf-8')
assert.equal(adapter.deleteSessionFile(sessionId), true)
assert.equal(fs.existsSync(dbPath), false)
assert.equal(fs.existsSync(dbPath + '-wal'), false)
assert.equal(fs.existsSync(dbPath + '-shm'), false)
assert.equal(fs.existsSync(cachePath), false)
assert.equal(fs.existsSync(queryCachePath), false)
})
@@ -5,7 +5,6 @@
* interface expected by session/member services.
*/
import * as fs from 'fs'
import type { DatabaseAdapter } from '@openchatlab/core'
import type { DatabaseManager } from '../../database-manager'
import type { SessionRuntimeAdapter } from './types'
@@ -19,11 +18,7 @@ export function createDatabaseManagerAdapter(dbManager: DatabaseManager): Sessio
getDbPath: (sessionId) => dbManager.getDbPath(sessionId),
deleteSessionFile(sessionId: string): boolean {
dbManager.close(sessionId)
const dbPath = dbManager.getDbPath(sessionId)
if (!fs.existsSync(dbPath)) return false
fs.unlinkSync(dbPath)
return true
return dbManager.deleteSessionDatabaseFiles(sessionId)
},
ensureReadonly(sessionId: string): DatabaseAdapter {
@@ -14,7 +14,7 @@ export interface SessionRuntimeAdapter {
closeSession(sessionId: string): void
getDbPath(sessionId: string): string
/** Delete the session database file. Returns false if not found. */
/** Delete the session database files and cache. Returns false if not found. */
deleteSessionFile(sessionId: string): boolean
/** Open readonly, throw 404 if not found. */
@@ -126,7 +126,7 @@ export function updateSessionOwnerId(adapter: SessionRuntimeAdapter, sessionId:
}
/**
* Delete a session (close DB + remove file).
* Delete a session (close DB + remove files/cache).
* Returns true if deleted, false if file not found.
*/
export function deleteSession(adapter: SessionRuntimeAdapter, sessionId: string): boolean {