Files
ftdl/lib/fs-utils.ts
tech 2a883c23f9 fix: address race conditions and add path validation
- Fixed race condition in getProjects() by using Promise.all with proper async/await
- Fixed race condition in getVersions() by using Promise.all with proper async/await
- Fixed race condition in getApks() by ensuring Promise.all wraps the async map
- Added validatePath() helper function to prevent path traversal attacks
- Applied path validation to all file system operations

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-04 01:54:07 +08:00

132 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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<string[]> {
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<string[]> {
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}`;
}