feat: 添加路径收藏功能

This commit is contained in:
Baobhan Sith
2025-05-23 18:47:04 +08:00
parent f8282fe9c0
commit ae60e0c398
15 changed files with 1269 additions and 7 deletions
@@ -265,6 +265,24 @@ const definedMigrations: Migration[] = [
timestamp INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
`
},
{
id: 8,
name: 'Create favorite_paths table',
check: async (db: Database): Promise<boolean> => {
const tableAlreadyExists = await tableExists(db, 'favorite_paths');
return !tableAlreadyExists; // Only run if the table does NOT exist
},
sql: `
CREATE TABLE IF NOT EXISTS favorite_paths (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NULL,
path TEXT NOT NULL,
last_used_at INTEGER NULL;
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
`
}
];
@@ -74,6 +74,7 @@ export const tableDefinitions: TableDefinition[] = [
{ name: 'command_history', sql: schemaSql.createCommandHistoryTableSQL },
{ name: 'path_history', sql: schemaSql.createPathHistoryTableSQL },
{ name: 'quick_commands', sql: schemaSql.createQuickCommandsTableSQL },
{ name: 'favorite_paths', sql: schemaSql.createFavoritePathsTableSQL }, // Added Favorite Paths table
// Appearance related tables (often depend on others or have init logic)
{
+10
View File
@@ -239,4 +239,14 @@ CREATE TABLE IF NOT EXISTS appearance_settings (
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
`;
export const createFavoritePathsTableSQL = `
CREATE TABLE IF NOT EXISTS favorite_paths (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NULL,
path TEXT NOT NULL,
last_used_at INTEGER NULL,
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
`;
@@ -0,0 +1,227 @@
import { Request, Response } from 'express';
import * as FavoritePathsService from '../services/favorite-paths.service';
import { FavoritePathSortBy } from '../services/favorite-paths.service';
/**
* 处理添加新收藏路径的请求
*/
export const createFavoritePath = async (req: Request, res: Response): Promise<void> => {
const { name, path } = req.body;
if (!path || typeof path !== 'string' || path.trim().length === 0) {
res.status(400).json({ message: '路径内容不能为空' });
return;
}
if (name !== null && typeof name !== 'string') {
res.status(400).json({ message: '名称必须是字符串或 null' });
return;
}
try {
const newId = await FavoritePathsService.addFavoritePath(name, path);
const newFavoritePath = await FavoritePathsService.getFavoritePathById(newId);
if (newFavoritePath) {
res.status(201).json({ message: '收藏路径已添加', favoritePath: newFavoritePath });
} else {
console.error(`[Controller] 添加收藏路径后未能找到 ID: ${newId}`);
res.status(201).json({ message: '收藏路径已添加,但无法检索新记录', id: newId });
}
} catch (error: any) {
console.error('[Controller] 添加收藏路径失败:', error.message);
res.status(500).json({ message: error.message || '无法添加收藏路径' });
}
};
/**
* 处理获取所有收藏路径的请求 (支持排序)
*/
export const getAllFavoritePaths = async (req: Request, res: Response): Promise<void> => {
const sortBy = req.query.sortBy as FavoritePathSortBy | undefined;
const validSortByOptions: FavoritePathSortBy[] = ['name', 'last_used_at'];
const validSortBy: FavoritePathSortBy = sortBy && validSortByOptions.includes(sortBy) ? sortBy : 'name';
try {
const favoritePaths = await FavoritePathsService.getAllFavoritePaths(validSortBy);
res.status(200).json(favoritePaths);
} catch (error: any) {
console.error('获取收藏路径控制器出错:', error);
res.status(500).json({ message: error.message || '无法获取收藏路径' });
}
};
/**
* 处理根据 ID 获取单个收藏路径的请求
*/
export const getFavoritePathById = 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 favoritePath = await FavoritePathsService.getFavoritePathById(id);
if (favoritePath) {
res.status(200).json(favoritePath);
} else {
res.status(404).json({ message: '未找到指定的收藏路径' });
}
} catch (error: any) {
console.error('获取单个收藏路径控制器出错:', error);
res.status(500).json({ message: error.message || '无法获取收藏路径' });
}
};
/**
* 处理更新收藏路径的请求
*/
export const updateFavoritePath = async (req: Request, res: Response): Promise<void> => {
const id = parseInt(req.params.id, 10);
const { name, path } = req.body;
if (isNaN(id)) {
res.status(400).json({ message: '无效的 ID' });
return;
}
if (!path || typeof path !== 'string' || path.trim().length === 0) {
res.status(400).json({ message: '路径内容不能为空' });
return;
}
if (name !== null && typeof name !== 'string') {
res.status(400).json({ message: '名称必须是字符串或 null' });
return;
}
try {
const success = await FavoritePathsService.updateFavoritePath(id, name, path);
if (success) {
const updatedFavoritePath = await FavoritePathsService.getFavoritePathById(id);
if (updatedFavoritePath) {
res.status(200).json({ message: '收藏路径已更新', favoritePath: updatedFavoritePath });
} else {
console.error(`[Controller] 更新收藏路径后未能找到 ID: ${id}`);
res.status(200).json({ message: '收藏路径已更新,但无法检索更新后的记录' });
}
} else {
const pathExists = await FavoritePathsService.getFavoritePathById(id);
if (!pathExists) {
res.status(404).json({ message: '未找到要更新的收藏路径' });
} else {
console.error(`[Controller] 更新收藏路径 ${id} 失败,但路径存在。`);
res.status(500).json({ message: '更新收藏路径时发生未知错误' });
}
}
} catch (error: any) {
console.error('更新收藏路径控制器出错:', error);
res.status(500).json({ message: error.message || '无法更新收藏路径' });
}
};
/**
* 处理删除收藏路径的请求
*/
export const deleteFavoritePath = 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 FavoritePathsService.deleteFavoritePath(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 incrementUsage = 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 lastUsedSuccess = await FavoritePathsService.updateFavoritePathLastUsed(id);
if (lastUsedSuccess) {
const updatedPath = await FavoritePathsService.getFavoritePathById(id);
if (updatedPath) {
res.status(200).json({ message: '上次使用时间已更新', favoritePath: updatedPath });
} else {
// 这种情况理论上不应该发生,因为 updateFavoritePathLastUsed 内部应该处理了路径不存在的情况
// 收藏路径 (ID: ${id}) 上次使用时间已更新,但无法检索该路径。
console.error(`[Controller] 收藏路径 (ID: ${id}) 上次使用时间已更新,但无法检索该路径。`);
res.status(404).json({ message: '上次使用时间已更新,但无法检索更新后的收藏路径' });
}
} else {
// 更新失败,检查路径是否存在以提供更具体的错误信息
const pathExists = await FavoritePathsService.getFavoritePathById(id);
if (!pathExists) {
res.status(404).json({ message: '未找到要更新上次使用时间的收藏路径' });
} else {
// 路径存在,但更新操作失败
console.warn(`[Controller] 尝试更新收藏路径 (ID: ${id}) 的上次使用时间失败,但路径存在。`);
res.status(500).json({ message: '更新上次使用时间失败' });
}
}
} catch (error: any) {
console.error('更新收藏路径上次使用时间控制器出错:', error);
res.status(500).json({ message: error.message || '无法更新上次使用时间' });
}
};
/**
* 处理更新收藏路径上次使用时间戳的请求 (PUT)
*/
export const updateLastUsedTimestamp = 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 FavoritePathsService.updateFavoritePathLastUsed(id);
if (success) {
const updatedPath = await FavoritePathsService.getFavoritePathById(id);
if (updatedPath) {
res.status(200).json({ message: '上次使用时间戳已更新', favoritePath: updatedPath });
} else {
// 这种情况理论上不应该发生,因为 updateFavoritePathLastUsed 内部应该处理了路径不存在的情况
console.error(`[Controller] 收藏路径 (ID: ${id}) 上次使用时间戳已更新,但无法检索该路径。`);
res.status(404).json({ message: '上次使用时间戳已更新,但无法检索更新后的收藏路径' });
}
} else {
// 更新失败,检查路径是否存在以提供更具体的错误信息
const pathExists = await FavoritePathsService.getFavoritePathById(id);
if (!pathExists) {
res.status(404).json({ message: '未找到要更新上次使用时间戳的收藏路径' });
} else {
// 路径存在,但更新操作失败
console.warn(`[Controller] 尝试更新收藏路径 (ID: ${id}) 的上次使用时间戳失败,但路径存在。`);
res.status(500).json({ message: '更新上次使用时间戳失败' });
}
}
} catch (error: any) {
console.error('更新收藏路径上次使用时间戳控制器出错:', error);
res.status(500).json({ message: error.message || '无法更新上次使用时间戳' });
}
};
@@ -0,0 +1,18 @@
import { Router } from 'express';
import * as FavoritePathsController from './favorite-paths.controller';
import { isAuthenticated } from '../auth/auth.middleware';
const router = Router();
// 应用认证中间件,确保所有路径收藏相关的API都需要用户认证
router.use(isAuthenticated);
// 定义路由
router.post('/', FavoritePathsController.createFavoritePath); // POST /api/favorite-paths
router.get('/', FavoritePathsController.getAllFavoritePaths); // GET /api/favorite-paths?sortBy=name|usage_count
router.get('/:id', FavoritePathsController.getFavoritePathById); // GET /api/favorite-paths/:id
router.put('/:id', FavoritePathsController.updateFavoritePath); // PUT /api/favorite-paths/:id
router.delete('/:id', FavoritePathsController.deleteFavoritePath); // DELETE /api/favorite-paths/:id
router.put('/:id/update-last-used', FavoritePathsController.updateLastUsedTimestamp); // PUT /api/favorite-paths/:id/update-last-used
export default router;
+2
View File
@@ -55,6 +55,7 @@ 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 favoritePathsRouter from './favorite-paths/favorite-paths.routes';
import { initializeWebSocket } from './websocket';
import { ipWhitelistMiddleware } from './auth/ipWhitelist.middleware';
@@ -261,6 +262,7 @@ const startServer = () => {
app.use('/api/v1/ssh-suspend', sshSuspendRouter);
app.use('/api/v1/transfers', transfersRoutes());
app.use('/api/v1/path-history', pathHistoryRoutes);
app.use('/api/v1/favorite-paths', favoritePathsRouter);
// 状态检查接口
app.get('/api/v1/status', (req: Request, res: Response) => {
@@ -0,0 +1,123 @@
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
// 定义收藏路径接口
export interface FavoritePath {
id: number;
name: string | null; // 名称可选
path: string;
last_used_at?: number | null; // 上次使用时间,允许为空
created_at: number; // Unix 时间戳 (秒)
updated_at: number; // Unix 时间戳 (秒)
}
/**
* 添加一条新的收藏路径
* @param name - 路径名称 (可选)
* @param path - 路径内容
* @returns 返回插入记录的 ID
*/
export const addFavoritePath = async (name: string | null, path: string): Promise<number> => {
const sql = `INSERT INTO favorite_paths (name, path, created_at, updated_at) VALUES (?, ?, strftime('%s', 'now'), strftime('%s', 'now'))`;
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [name, path]);
if (typeof result.lastID !== 'number' || result.lastID <= 0) {
throw new Error('添加收藏路径后未能获取有效的 lastID');
}
return result.lastID;
} catch (err: any) {
console.error('添加收藏路径时出错:', err.message);
throw new Error('无法添加收藏路径');
}
};
/**
* 更新指定的收藏路径
* @param id - 要更新的记录 ID
* @param name - 新的路径名称 (可选)
* @param path - 新的路径内容
* @returns 返回是否成功更新 (true/false)
*/
export const updateFavoritePath = async (id: number, name: string | null, path: string): Promise<boolean> => {
const sql = `UPDATE favorite_paths SET name = ?, path = ?, updated_at = strftime('%s', 'now') WHERE id = ?`;
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [name, path, id]);
return result.changes > 0;
} catch (err: any) {
console.error('更新收藏路径时出错:', err.message);
throw new Error('无法更新收藏路径');
}
};
/**
* 根据 ID 删除指定的收藏路径
* @param id - 要删除的记录 ID
* @returns 返回是否成功删除 (true/false)
*/
export const deleteFavoritePath = async (id: number): Promise<boolean> => {
const sql = `DELETE FROM favorite_paths 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('无法删除收藏路径');
}
};
/**
* 获取所有收藏路径
* @param sortBy - 排序字段 ('name' 或 'usage_count')
* @returns 返回包含所有收藏路径条目的数组
*/
export const getAllFavoritePaths = async (sortBy: 'name' | 'last_used_at' = 'name'): Promise<FavoritePath[]> => {
let orderByClause = 'ORDER BY name ASC'; // 默认按名称升序
if (sortBy === 'last_used_at') {
orderByClause = 'ORDER BY last_used_at DESC, name ASC'; // 按上次使用时间降序,同时间的按名称升序
}
const sql = `SELECT id, name, path, last_used_at, created_at, updated_at FROM favorite_paths ${orderByClause}`;
try {
const db = await getDbInstance();
const rows = await allDb<FavoritePath>(db, sql);
return rows;
} catch (err: any) {
console.error('获取收藏路径时出错:', err.message);
throw new Error('无法获取收藏路径');
}
};
/**
* 更新指定收藏路径的上次使用时间
* @param id - 要更新的记录 ID
* @returns 返回是否成功更新 (true/false)
*/
export const updateFavoritePathLastUsedAt = async (id: number): Promise<boolean> => {
const sql = `UPDATE favorite_paths SET last_used_at = strftime('%s', 'now'), updated_at = strftime('%s', 'now') 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('无法更新收藏路径上次使用时间');
}
};
/**
* 根据 ID 查找收藏路径
* @param id - 要查找的记录 ID
* @returns 返回找到的收藏路径条目,如果未找到则返回 undefined
*/
export const findFavoritePathById = async (id: number): Promise<FavoritePath | undefined> => {
const sql = `SELECT id, name, path, last_used_at, created_at, updated_at FROM favorite_paths WHERE id = ?`;
try {
const db = await getDbInstance();
const row = await getDbRow<FavoritePath>(db, sql, [id]);
return row;
} catch (err: any) {
console.error('查找收藏路径时出错:', err.message);
throw new Error('无法查找收藏路径');
}
};
@@ -0,0 +1,75 @@
import * as FavoritePathsRepository from '../repositories/favorite-paths.repository';
import { FavoritePath } from '../repositories/favorite-paths.repository';
// 定义排序类型
export type FavoritePathSortBy = 'name' | 'last_used_at';
/**
* 添加收藏路径
* @param name - 路径名称 (可选)
* @param path - 路径内容
* @returns 返回添加记录的 ID
*/
export const addFavoritePath = async (name: string | null, path: string): Promise<number> => {
if (!path || path.trim().length === 0) {
throw new Error('路径内容不能为空');
}
// 如果 name 是空字符串,则视为 null
const finalName = name && name.trim().length > 0 ? name.trim() : null;
const favoritePathId = await FavoritePathsRepository.addFavoritePath(finalName, path.trim());
return favoritePathId;
};
/**
* 更新收藏路径
* @param id - 要更新的记录 ID
* @param name - 新的路径名称 (可选)
* @param path - 新的路径内容
* @returns 返回是否成功更新 (更新行数 > 0)
*/
export const updateFavoritePath = async (id: number, name: string | null, path: string): Promise<boolean> => {
if (!path || path.trim().length === 0) {
throw new Error('路径内容不能为空');
}
const finalName = name && name.trim().length > 0 ? name.trim() : null;
const pathUpdated = await FavoritePathsRepository.updateFavoritePath(id, finalName, path.trim());
return pathUpdated;
};
/**
* 删除收藏路径
* @param id - 要删除的记录 ID
* @returns 返回是否成功删除 (删除行数 > 0)
*/
export const deleteFavoritePath = async (id: number): Promise<boolean> => {
const changes = await FavoritePathsRepository.deleteFavoritePath(id);
return changes;
};
/**
* 获取所有收藏路径,并按指定方式排序
* @param sortBy - 排序字段 ('name' 或 'usage_count')
* @returns 返回排序后的收藏路径数组
*/
export const getAllFavoritePaths = async (sortBy: FavoritePathSortBy = 'name'): Promise<FavoritePath[]> => {
return FavoritePathsRepository.getAllFavoritePaths(sortBy);
};
/**
* 更新收藏路径的上次使用时间
* @param id - 收藏路径的ID
* @returns Promise<boolean> - 操作是否成功
*/
export const updateFavoritePathLastUsed = async (id: number): Promise<boolean> => {
// 未来可能在这里添加额外的业务逻辑或验证
return FavoritePathsRepository.updateFavoritePathLastUsedAt(id);
};
/**
* 根据 ID 获取单个收藏路径
* @param id - 记录 ID
* @returns 返回找到的收藏路径,或 undefined
*/
export const getFavoritePathById = async (id: number): Promise<FavoritePath | undefined> => {
return FavoritePathsRepository.findFavoritePathById(id);
};