// lib/fs-utils.ts import fs from 'fs/promises'; import path from 'path'; import { RESOURCE_PATH } from './constants'; /** * Validate that a path doesn't escape the base directory (prevents path traversal) */ export function validatePath(basePath: string, ...segments: string[]): string { const fullPath = path.resolve(basePath, ...segments); const normalizedBase = path.resolve(basePath); if (!fullPath.startsWith(normalizedBase)) { throw new Error('Invalid path: path traversal detected'); } return fullPath; } /** * 获取所有项目目录 */ export async function getProjects(): Promise { const dirs = await fs.readdir(RESOURCE_PATH); const projectDirs = await Promise.all( dirs.map(async (dir) => { const dirPath = validatePath(RESOURCE_PATH, dir); try { const stats = await fs.stat(dirPath); return stats.isDirectory() ? dir : null; } catch { return null; } }) ); return projectDirs.filter((dir): dir is string => dir !== null); } /** * 获取指定项目的所有版本目录 */ export async function getVersions(project: string): Promise { const projectPath = validatePath(RESOURCE_PATH, project); const dirs = await fs.readdir(projectPath); const versionDirs = await Promise.all( dirs.map(async (dir) => { const dirPath = validatePath(projectPath, dir); try { const stats = await fs.stat(dirPath); return stats.isDirectory() ? dir : null; } catch { return null; } }) ); return versionDirs.filter((dir): dir is string => dir !== null).sort(); } /** * 获取指定项目版本的 APK 文件列表 */ export async function getApks(project: string, version: string, commitId?: string): Promise<{ name: string; commit: string | null; size: number; modifiedAt: Date; }[]> { const apksPath = validatePath(RESOURCE_PATH, project, version, 'apks'); try { const files = await fs.readdir(apksPath); const apkFiles = files.filter(file => file.endsWith('.apk')); const apks = await Promise.all( apkFiles.map(async (file) => { const filePath = validatePath(apksPath, file); const stats = await fs.stat(filePath); // 提取 commit id (假设文件名格式为 xxx-{commit}-xxx.apk) const commitMatch = file.match(/([a-f0-9]{7,})/i); const commit = commitMatch ? commitMatch[1] : null; return { name: file, commit, size: stats.size, modifiedAt: stats.mtime, }; }) ); // 如果指定了 commit id,进行过滤 if (commitId) { return apks.filter(apk => apk.commit && apk.commit.toLowerCase().includes(commitId.toLowerCase()) ); } return apks.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime()); } catch (error) { // 如果 apks 目录不存在,返回空数组 return []; } }