Files
ftdl/lib/fs-utils.ts
tech 8227f36cbe refactor: organize tests and extract formatting utilities
- Move test files to lib/__tests__/ directory
- Extract formatting utilities (formatVersion, formatFileSize, formatDateTime)
  from fs-utils.ts to new utils.ts module
- Add Jest test configuration and test scripts
- Update component imports to use new utils module
- Add CLAUDE.md documentation for project structure

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-04 02:33:30 +08:00

104 lines
2.9 KiB
TypeScript
Raw Permalink 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)
*/
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<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 [];
}
}