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:
1 parent
f3c49a9e49
commit
128ccf951c
6 files changed
+41
-31
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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
@@ -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');
|
||||
|
||||
|
||||
Reference in new issue
Block a user