// 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) */ 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 []; } } /** * 格式化版本号显示 (0_42 -> 0.42) */ export function formatVersion(version: string): string { return version.replace('_', '.'); } /** * 格式化文件大小 */ export function formatFileSize(bytes: number): string { if (bytes < 1024) return bytes + ' B'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB'; if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(2) + ' MB'; return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB'; } /** * 格式化日期时间 */ export function formatDateTime(date: Date): string { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); return `${year}-${month}-${day} ${hours}:${minutes}`; }