perf: convert sync file I/O to async in API routes (P0-2)

Replace all readFileSync/existsSync/readdirSync with fs.promises
equivalents to unblock the Node event loop under concurrency.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
7418andClaude Opus 4.6 committed 2026-02-13 01:24:21 +08:00
1 parent f3c49a9e49
commit 128ccf951c
6 files changed
+41 -31

No files matched your search

+8 -6
View File
@@ -1,16 +1,16 @@
import { NextRequest, NextResponse } from 'next/server';
import fs from 'fs';
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
import type { ErrorResponse } from '@/types';
function getWindowsDrives(): string[] {
async function getWindowsDrives(): Promise<string[]> {
if (process.platform !== 'win32') return [];
const drives: string[] = [];
for (let i = 65; i <= 90; i++) {
const drive = String.fromCharCode(i) + ':\\';
try {
fs.accessSync(drive);
await fs.access(drive);
drives.push(drive);
} catch {
// drive not available
@@ -26,7 +26,9 @@ export async function GET(request: NextRequest) {
const resolvedDir = path.resolve(dir);
if (!fs.existsSync(resolvedDir)) {
try {
await fs.access(resolvedDir);
} catch {
return NextResponse.json<ErrorResponse>(
{ error: 'Directory does not exist' },
{ status: 404 }
@@ -34,7 +36,7 @@ export async function GET(request: NextRequest) {
}
try {
const entries = fs.readdirSync(resolvedDir, { withFileTypes: true });
const entries = await fs.readdir(resolvedDir, { withFileTypes: true });
const directories = entries
.filter((e) => e.isDirectory() && !e.name.startsWith('.'))
.map((e) => ({
@@ -43,7 +45,7 @@ export async function GET(request: NextRequest) {
}))
.sort((a, b) => a.name.localeCompare(b.name));
const drives = getWindowsDrives();
const drives = await getWindowsDrives();
return NextResponse.json({
current: resolvedDir,
+3 -3
View File
@@ -1,4 +1,6 @@
import { NextRequest, NextResponse } from 'next/server';
import path from 'path';
import os from 'os';
import { readFilePreview, isPathSafe } from '@/lib/files';
import type { FilePreviewResponse, ErrorResponse } from '@/types';
@@ -14,8 +16,6 @@ export async function GET(request: NextRequest) {
);
}
const path = require('path');
const os = require('os');
const resolvedPath = path.resolve(filePath);
const homeDir = os.homedir();
@@ -52,7 +52,7 @@ export async function GET(request: NextRequest) {
}
try {
const preview = readFilePreview(resolvedPath, Math.min(maxLines, 1000));
const preview = await readFilePreview(resolvedPath, Math.min(maxLines, 1000));
return NextResponse.json<FilePreviewResponse>({ preview });
} catch (error) {
return NextResponse.json<ErrorResponse>(
+6 -4
View File
@@ -1,5 +1,5 @@
import { NextRequest } from 'next/server';
import fs from 'fs';
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
import { isPathSafe } from '@/lib/files';
@@ -104,14 +104,16 @@ export async function GET(request: NextRequest) {
});
}
if (!fs.existsSync(resolved)) {
try {
await fs.access(resolved);
} catch {
return new Response(JSON.stringify({ error: 'File not found' }), {
status: 404,
headers: { 'Content-Type': 'application/json' },
});
}
const stat = fs.statSync(resolved);
const stat = await fs.stat(resolved);
if (!stat.isFile()) {
return new Response(JSON.stringify({ error: 'Not a file' }), {
status: 400,
@@ -119,7 +121,7 @@ export async function GET(request: NextRequest) {
});
}
const buffer = fs.readFileSync(resolved);
const buffer = await fs.readFile(resolved);
const ext = path.extname(resolved).toLowerCase();
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
+3 -3
View File
@@ -1,4 +1,6 @@
import { NextRequest, NextResponse } from 'next/server';
import path from 'path';
import os from 'os';
import { scanDirectory, isPathSafe } from '@/lib/files';
import type { FileTreeResponse, ErrorResponse } from '@/types';
@@ -14,8 +16,6 @@ export async function GET(request: NextRequest) {
);
}
const path = require('path');
const os = require('os');
const resolvedDir = path.resolve(dir);
const homeDir = os.homedir();
@@ -52,7 +52,7 @@ export async function GET(request: NextRequest) {
}
try {
const tree = scanDirectory(resolvedDir, Math.min(depth, 5));
const tree = await scanDirectory(resolvedDir, Math.min(depth, 5));
return NextResponse.json<FileTreeResponse>({ tree, root: resolvedDir });
} catch (error) {
return NextResponse.json<ErrorResponse>(
+5 -3
View File
@@ -1,5 +1,5 @@
import { NextRequest } from 'next/server';
import fs from 'fs';
import fs from 'fs/promises';
import path from 'path';
export const runtime = 'nodejs';
@@ -42,14 +42,16 @@ export async function GET(request: NextRequest) {
});
}
if (!fs.existsSync(resolved)) {
try {
await fs.access(resolved);
} catch {
return new Response(JSON.stringify({ error: 'File not found' }), {
status: 404,
headers: { 'Content-Type': 'application/json' },
});
}
const buffer = fs.readFileSync(resolved);
const buffer = await fs.readFile(resolved);
const ext = path.extname(resolved).toLowerCase();
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
+16 -12
View File
@@ -1,4 +1,4 @@
import fs from 'fs';
import fs from 'fs/promises';
import path from 'path';
import type { FileTreeNode, FilePreview } from '@/types';
@@ -74,22 +74,24 @@ export function isPathSafe(basePath: string, targetPath: string): boolean {
return resolvedTarget.startsWith(resolvedBase + path.sep) || resolvedTarget === resolvedBase;
}
export function scanDirectory(dir: string, depth: number = 3): FileTreeNode[] {
export async function scanDirectory(dir: string, depth: number = 3): Promise<FileTreeNode[]> {
const resolvedDir = path.resolve(dir);
if (!fs.existsSync(resolvedDir)) {
try {
await fs.access(resolvedDir);
} catch {
return [];
}
return scanDirectoryRecursive(resolvedDir, depth);
}
function scanDirectoryRecursive(dir: string, depth: number): FileTreeNode[] {
async function scanDirectoryRecursive(dir: string, depth: number): Promise<FileTreeNode[]> {
if (depth <= 0) return [];
let entries: fs.Dirent[];
let entries: import('fs').Dirent[];
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
entries = await fs.readdir(dir, { withFileTypes: true });
} catch {
return [];
}
@@ -114,7 +116,7 @@ function scanDirectoryRecursive(dir: string, depth: number): FileTreeNode[] {
if (entry.isDirectory()) {
if (IGNORED_DIRS.has(entry.name)) continue;
const children = scanDirectoryRecursive(fullPath, depth - 1);
const children = await scanDirectoryRecursive(fullPath, depth - 1);
nodes.push({
name: entry.name,
path: fullPath,
@@ -125,7 +127,7 @@ function scanDirectoryRecursive(dir: string, depth: number): FileTreeNode[] {
const ext = path.extname(entry.name).replace(/^\./, '');
let size: number | undefined;
try {
const stat = fs.statSync(fullPath);
const stat = await fs.stat(fullPath);
size = stat.size;
} catch {
// Skip files we can't stat
@@ -144,20 +146,22 @@ function scanDirectoryRecursive(dir: string, depth: number): FileTreeNode[] {
return nodes;
}
export function readFilePreview(filePath: string, maxLines: number = 200): FilePreview {
export async function readFilePreview(filePath: string, maxLines: number = 200): Promise<FilePreview> {
const resolvedPath = path.resolve(filePath);
if (!fs.existsSync(resolvedPath)) {
try {
await fs.access(resolvedPath);
} catch {
throw new Error(`File not found: ${filePath}`);
}
const stat = fs.statSync(resolvedPath);
const stat = await fs.stat(resolvedPath);
if (!stat.isFile()) {
throw new Error(`Not a file: ${filePath}`);
}
// Read the file content, limiting to maxLines
const content = fs.readFileSync(resolvedPath, 'utf-8');
const content = await fs.readFile(resolvedPath, 'utf-8');
const lines = content.split('\n');
const truncated = lines.slice(0, maxLines).join('\n');