Skip to content

Commit 9cd4ecc

Browse files
authored
Merge pull request #20 from romot-co/codex/refactor-fileoperations.scandirectory-for-streaming
Stream file reads during directory scan
2 parents 7eb0731 + 8e1beb9 commit 9cd4ecc

2 files changed

Lines changed: 30 additions & 14 deletions

File tree

src/__tests__/fileOperations.test.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import * as fs from 'fs/promises';
2+
import * as fsStream from 'fs';
23
import * as vscode from 'vscode';
3-
import { PathLike } from 'fs';
4+
import { PathLike, ReadStream } from 'fs';
5+
import { Readable } from 'stream';
46
import * as path from 'path';
57
import { FileOperations } from '../fileOperations';
68
import { FileSizeLimitError, ScanError } from '../errors/errors';
79
import { ScanOptions } from '../types/fileTypes';
810

911
jest.mock('fs/promises');
12+
jest.mock('fs');
1013
jest.mock('../utils/fileUtils', () => ({
1114
isBinaryFile: (buf: Buffer | string) => String(buf).includes('BIN')
1215
}));
@@ -23,6 +26,7 @@ jest.mock('../utils/fileUtils', () => ({
2326
});
2427

2528
const mockedFs = jest.mocked(fs);
29+
const mockedFsStream = jest.mocked(fsStream);
2630
const ROOT = '/ws';
2731
const BASE_OPTS: ScanOptions = {
2832
maxFileSize: 1024 * 1024,
@@ -47,6 +51,7 @@ beforeEach(() => {
4751
mockedFs.stat.mockReset();
4852
mockedFs.readdir.mockReset();
4953
mockedFs.readFile.mockReset();
54+
mockedFsStream.createReadStream.mockReset();
5055
jest.clearAllMocks();
5156
});
5257

@@ -68,8 +73,8 @@ describe('FileOperations: basics', () => {
6873
if (s === path.join(ROOT, 'src', 'b')) return [d('c.js')];
6974
return [];
7075
});
71-
// ③ readFile
72-
mockedFs.readFile.mockResolvedValue('DATA');
76+
// ③ createReadStream
77+
mockedFsStream.createReadStream.mockImplementation(() => Readable.from(['DATA']) as unknown as ReadStream);
7378

7479
const res = await fo.scanDirectory('src', BASE_OPTS);
7580

@@ -93,7 +98,7 @@ describe('FileOperations: filters', () => {
9398
it('excludePatterns に一致するファイルを除外する', async () => {
9499
mockedFs.stat.mockImplementation((p) => smartStat(p));
95100
mockedFs.readdir.mockResolvedValue([d('skip.txt'), d('keep.txt')] as any[]);
96-
mockedFs.readFile.mockResolvedValue('C');
101+
mockedFsStream.createReadStream.mockImplementation(() => Readable.from(['C']) as unknown as ReadStream);
97102

98103
const res = await fo.scanDirectory('.', {
99104
...BASE_OPTS,
@@ -110,7 +115,7 @@ describe('FileOperations: filters', () => {
110115
return smartStat(p, tooLarge ? 2_000_000 : 100);
111116
});
112117
mockedFs.readdir.mockResolvedValue([d('small.txt'), d('large.txt')] as any[]);
113-
mockedFs.readFile.mockResolvedValue('C');
118+
mockedFsStream.createReadStream.mockImplementation(() => Readable.from(['C']) as unknown as ReadStream);
114119

115120
const res = await fo.scanDirectory('.', BASE_OPTS);
116121
expect(res.files.map(f => f.relativePath)).toEqual(['small.txt']);
@@ -119,9 +124,11 @@ describe('FileOperations: filters', () => {
119124
it('バイナリファイルは除外される', async () => {
120125
mockedFs.stat.mockImplementation((p) => smartStat(p));
121126
mockedFs.readdir.mockResolvedValue([d('text.txt'), d('binary.dat')] as any[]);
122-
mockedFs.readFile.mockImplementation((async (p: PathLike) =>
123-
path.basename(String(p)).startsWith('binary') ? Buffer.from('BIN') : Buffer.from('TXT')
124-
) as any);
127+
mockedFsStream.createReadStream.mockImplementation((p: PathLike) => {
128+
const base = path.basename(String(p));
129+
const data = base.startsWith('binary') ? 'BIN' : 'TXT';
130+
return Readable.from([data]) as unknown as ReadStream;
131+
});
125132

126133
const res = await fo.scanDirectory('.', BASE_OPTS);
127134
expect(res.files.map(f => f.relativePath)).toEqual(['text.txt']);

src/fileOperations.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as vscode from 'vscode';
22
import * as path from 'path';
33
import * as fs from 'fs/promises';
4+
import { createReadStream } from 'fs';
45
import { FileInfo, DirectoryInfo, ScanOptions } from './types/fileTypes';
56
import { minimatch } from 'minimatch';
67
import { DirectoryNotFoundError, FileSizeLimitError, ScanError } from './errors/errors';
@@ -129,6 +130,16 @@ export class FileOperations {
129130
this.currentSelectedPath = path;
130131
}
131132

133+
private async readFileContentStream(filePath: string): Promise<string> {
134+
return new Promise((resolve, reject) => {
135+
let data = '';
136+
const stream = createReadStream(filePath, { encoding: 'utf-8' });
137+
stream.on('data', chunk => { data += chunk; });
138+
stream.on('end', () => resolve(data));
139+
stream.on('error', err => reject(err));
140+
});
141+
}
142+
132143
async scanDirectory(targetPath: string, options: ScanOptions): Promise<DirectoryInfo> {
133144
try {
134145
// .gitignoreパターンを読み込む(初回のみ)
@@ -156,8 +167,8 @@ export class FileOperations {
156167
}
157168

158169
// バイナリファイルのチェック
159-
const buffer = await fs.readFile(absolutePath);
160-
if (isBinaryFile(buffer)) {
170+
const content = await this.readFileContentStream(absolutePath);
171+
if (isBinaryFile(content)) {
161172
this.logger.info(`バイナリファイルをスキップ: ${relativePath}`);
162173
return {
163174
uri: vscode.Uri.file(path.dirname(absolutePath)),
@@ -167,7 +178,6 @@ export class FileOperations {
167178
};
168179
}
169180

170-
const content = buffer.toString('utf-8');
171181
const language = this.detectLanguage(path.basename(absolutePath));
172182
let imports: string[] | undefined;
173183
if (options.includeDependencies) {
@@ -231,13 +241,12 @@ export class FileOperations {
231241
}
232242

233243
// バイナリファイルのチェック
234-
const buffer = await fs.readFile(entryPath);
235-
if (isBinaryFile(buffer)) {
244+
const content = await this.readFileContentStream(entryPath);
245+
if (isBinaryFile(content)) {
236246
this.logger.info(`バイナリファイルをスキップ: ${entryRelativePath}`);
237247
continue;
238248
}
239249

240-
const content = buffer.toString('utf-8');
241250
const language = this.detectLanguage(entryNameString);
242251
let imports: string[] | undefined;
243252
if (options.includeDependencies) {

0 commit comments

Comments
 (0)