mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
fix(sync): delete imported session files completely
This commit is contained in:
1 parent
848a44310c
commit
82a6be4fcb
7 files changed
+124
-13
No files matched your search
@@ -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)
|
||||
})
|
||||
@@ -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 }
|
||||
})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user