feat: 添加历史路径功能

This commit is contained in:
Baobhan Sith
2025-05-23 15:58:26 +08:00
parent d8f0524b7c
commit f8282fe9c0
14 changed files with 787 additions and 42 deletions
+16 -2
View File
@@ -227,13 +227,12 @@ const definedMigrations: Migration[] = [
ANALYZE; -- 重新分析数据库模式
`
},
// --- 未来可以添加更多迁移 ---
{
id: 6,
name: 'Create passkeys table for WebAuthn credentials',
check: async (db: Database): Promise<boolean> => {
const passkeysTableAlreadyExists = await tableExists(db, 'passkeys');
return !passkeysTableAlreadyExists; // Only run if the table does NOT exist
return !passkeysTableAlreadyExists;
},
sql: `
CREATE TABLE IF NOT EXISTS passkeys (
@@ -251,6 +250,21 @@ const definedMigrations: Migration[] = [
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
`
},
{
id: 7,
name: 'Create path_history table',
check: async (db: Database): Promise<boolean> => {
const tableAlreadyExists = await tableExists(db, 'path_history');
return !tableAlreadyExists;
},
sql: `
CREATE TABLE IF NOT EXISTS path_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
path TEXT NOT NULL,
timestamp INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
`
}
];
@@ -72,6 +72,7 @@ export const tableDefinitions: TableDefinition[] = [
// Other utilities
{ name: 'ip_blacklist', sql: schemaSql.createIpBlacklistTableSQL },
{ name: 'command_history', sql: schemaSql.createCommandHistoryTableSQL },
{ name: 'path_history', sql: schemaSql.createPathHistoryTableSQL },
{ name: 'quick_commands', sql: schemaSql.createQuickCommandsTableSQL },
// Appearance related tables (often depend on others or have init logic)
+8
View File
@@ -158,6 +158,14 @@ CREATE TABLE IF NOT EXISTS command_history (
);
`;
export const createPathHistoryTableSQL = `
CREATE TABLE IF NOT EXISTS path_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
path TEXT NOT NULL,
timestamp INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
`;
export const createQuickCommandsTableSQL = `
CREATE TABLE IF NOT EXISTS quick_commands (
id INTEGER PRIMARY KEY AUTOINCREMENT,
+3 -1
View File
@@ -54,6 +54,7 @@ import sshKeysRouter from './ssh_keys/ssh_keys.routes';
import quickCommandTagRoutes from './quick-command-tags/quick-command-tag.routes';
import sshSuspendRouter from './ssh-suspend/ssh-suspend.routes';
import { transfersRoutes } from './transfers/transfers.routes';
import pathHistoryRoutes from './path-history/path-history.routes';
import { initializeWebSocket } from './websocket';
import { ipWhitelistMiddleware } from './auth/ipWhitelist.middleware';
@@ -258,7 +259,8 @@ const startServer = () => {
app.use('/api/v1/ssh-keys', sshKeysRouter);
app.use('/api/v1/quick-command-tags', quickCommandTagRoutes);
app.use('/api/v1/ssh-suspend', sshSuspendRouter);
app.use('/api/v1/transfers', transfersRoutes());
app.use('/api/v1/transfers', transfersRoutes());
app.use('/api/v1/path-history', pathHistoryRoutes);
// 状态检查接口
app.get('/api/v1/status', (req: Request, res: Response) => {
@@ -0,0 +1,73 @@
import { Request, Response } from 'express';
import * as PathHistoryService from '../services/path-history.service';
/**
* 处理添加新路径历史记录的请求
*/
export const addPath = async (req: Request, res: Response): Promise<void> => {
const { path } = req.body;
if (!path || typeof path !== 'string' || path.trim().length === 0) {
res.status(400).json({ message: '路径不能为空' });
return;
}
try {
const newId = await PathHistoryService.addPathHistory(path);
res.status(201).json({ id: newId, message: '路径已添加到历史记录' });
} catch (error: any) {
console.error('添加路径历史记录控制器出错:', error);
res.status(500).json({ message: error.message || '无法添加路径历史记录' });
}
};
/**
* 处理获取所有路径历史记录的请求
*/
export const getAllPaths = async (req: Request, res: Response): Promise<void> => {
try {
const history = await PathHistoryService.getAllPathHistory();
// Repository 返回的是升序(旧->新)
res.status(200).json(history);
} catch (error: any) {
console.error('获取路径历史记录控制器出错:', error);
res.status(500).json({ message: error.message || '无法获取路径历史记录' });
}
};
/**
* 处理根据 ID 删除路径历史记录的请求
*/
export const deletePath = async (req: Request, res: Response): Promise<void> => {
const id = parseInt(req.params.id, 10);
if (isNaN(id)) {
res.status(400).json({ message: '无效的 ID' });
return;
}
try {
const success = await PathHistoryService.deletePathHistoryById(id);
if (success) {
res.status(200).json({ message: '路径历史记录已删除' });
} else {
res.status(404).json({ message: '未找到要删除的路径历史记录' });
}
} catch (error: any) {
console.error('删除路径历史记录控制器出错:', error);
res.status(500).json({ message: error.message || '无法删除路径历史记录' });
}
};
/**
* 处理清空所有路径历史记录的请求
*/
export const clearAllPaths = async (req: Request, res: Response): Promise<void> => {
try {
const count = await PathHistoryService.clearAllPathHistory();
res.status(200).json({ count, message: `已清空 ${count} 条路径历史记录` });
} catch (error: any) {
console.error('清空路径历史记录控制器出错:', error);
res.status(500).json({ message: error.message || '无法清空路径历史记录' });
}
};
@@ -0,0 +1,15 @@
import { Router } from 'express';
import * as PathHistoryController from './path-history.controller';
import { isAuthenticated } from '../auth/auth.middleware'; // 更新认证中间件
const router = Router();
// 应用认证中间件到所有路径历史路由
router.use(isAuthenticated);
router.post('/', PathHistoryController.addPath);
router.get('/', PathHistoryController.getAllPaths);
router.delete('/:id', PathHistoryController.deletePath);
router.delete('/', PathHistoryController.clearAllPaths); // 更新清空路由
export default router;
@@ -0,0 +1,100 @@
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
// 定义路径历史记录的接口
export interface PathHistoryEntry {
id: number;
path: string;
timestamp: number; // Unix 时间戳 (秒)
}
type DbPathHistoryRow = PathHistoryEntry;
/**
* 插入或更新一条路径历史记录。
* 如果路径已存在,则更新其时间戳;否则,插入新记录。
* @param path - 要添加或更新的路径字符串
* @returns 返回插入或更新记录的 ID
*/
export const upsertPath = async (path: string): Promise<number> => {
const now = Math.floor(Date.now() / 1000); // 获取当前时间戳
const db = await getDbInstance();
try {
// 1. 尝试更新现有记录的时间戳
const updateSql = `UPDATE path_history SET timestamp = ? WHERE path = ?`;
const updateResult = await runDb(db, updateSql, [now, path]);
if (updateResult.changes > 0) {
// 更新成功,需要获取被更新记录的 ID
const selectSql = `SELECT id FROM path_history WHERE path = ? ORDER BY timestamp DESC LIMIT 1`;
const row = await getDbRow<{ id: number }>(db, selectSql, [path]);
if (row) {
return row.id;
} else {
// This case should theoretically not happen if update succeeded
throw new Error('更新成功但无法找到记录 ID');
}
} else {
// 2. 没有记录被更新,说明路径不存在,执行插入
const insertSql = `INSERT INTO path_history (path, timestamp) VALUES (?, ?)`;
const insertResult = await runDb(db, insertSql, [path, now]);
// Ensure lastID is valid before returning
if (typeof insertResult.lastID !== 'number' || insertResult.lastID <= 0) {
throw new Error('插入新路径历史记录后未能获取有效的 lastID');
}
return insertResult.lastID;
}
} catch (err: any) {
console.error('Upsert 路径历史记录时出错:', err.message);
throw new Error('无法更新或插入路径历史记录');
}
};
/**
* 获取所有路径历史记录,按时间戳升序排列(最旧的在前)
* @returns 返回包含所有历史记录条目的数组
*/
export const getAllPaths = async (): Promise<PathHistoryEntry[]> => {
const sql = `SELECT id, path, timestamp FROM path_history ORDER BY timestamp ASC`;
try {
const db = await getDbInstance();
const rows = await allDb<DbPathHistoryRow>(db, sql);
return rows;
} catch (err: any) {
console.error('获取路径历史记录时出错:', err.message);
throw new Error('无法获取路径历史记录');
}
};
/**
* 根据 ID 删除指定的路径历史记录
* @param id - 要删除的记录 ID
* @returns 返回是否成功删除 (true/false)
*/
export const deletePathById = async (id: number): Promise<boolean> => {
const sql = `DELETE FROM path_history WHERE id = ?`;
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [id]);
return result.changes > 0;
} catch (err: any) {
console.error('删除路径历史记录时出错:', err.message);
throw new Error('无法删除路径历史记录');
}
};
/**
* 清空所有路径历史记录
* @returns 返回删除的行数
*/
export const clearAllPaths = async (): Promise<number> => {
const sql = `DELETE FROM path_history`;
try {
const db = await getDbInstance();
const result = await runDb(db, sql);
return result.changes;
} catch (err: any) {
console.error('清空路径历史记录时出错:', err.message);
throw new Error('无法清空路径历史记录');
}
};
@@ -0,0 +1,43 @@
import * as PathHistoryRepository from '../repositories/path-history.repository';
import { PathHistoryEntry } from '../repositories/path-history.repository';
/**
* 添加一条路径历史记录
* @param path - 要添加的路径
* @returns 返回添加记录的 ID
*/
export const addPathHistory = async (path: string): Promise<number> => {
// 可以在这里添加额外的业务逻辑,例如校验路径格式、长度限制等
if (!path || path.trim().length === 0) {
throw new Error('路径不能为空');
}
// 调用 upsertPath 来处理插入或更新时间戳
return PathHistoryRepository.upsertPath(path.trim());
};
/**
* 获取所有路径历史记录
* @returns 返回所有历史记录条目数组,按时间戳升序
*/
export const getAllPathHistory = async (): Promise<PathHistoryEntry[]> => {
return PathHistoryRepository.getAllPaths();
};
/**
* 根据 ID 删除一条路径历史记录
* @param id - 要删除的记录 ID
* @returns 返回是否成功删除 (删除行数 > 0)
*/
export const deletePathHistoryById = async (id: number): Promise<boolean> => {
const success = await PathHistoryRepository.deletePathById(id);
return success;
};
/**
* 清空所有路径历史记录
* @returns 返回删除的记录条数
*/
export const clearAllPathHistory = async (): Promise<number> => {
return PathHistoryRepository.clearAllPaths();
};