@mtorange/mcp-local-file-search
Version:
Local file indexing and search MCP server with BM25 algorithm and multilingual support
212 lines (182 loc) • 6.01 kB
JavaScript
const fs = require('fs-extra');
const path = require('path');
const mammoth = require('mammoth');
const pdfParse = require('pdf-parse');
const XLSX = require('xlsx');
const JSZip = require('jszip');
const xml2js = require('xml2js');
class FileParser {
constructor(debugFile = null) {
this.debugFile = debugFile;
}
// 디버그 로그 출력
debug(message) {
if (this.debugFile) {
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] ${message}\n`;
fs.appendFileSync(this.debugFile, logMessage);
}
}
// 파일 확장자 가져오기
getFileExtension(filePath) {
return path.extname(filePath).toLowerCase();
}
// 파일 파싱
async parseFile(filePath) {
try {
const ext = this.getFileExtension(filePath);
const stats = await fs.stat(filePath);
this.debug(`파일 파싱 시작: ${filePath} (${ext})`);
let content = '';
switch (ext) {
case '.txt':
case '.md':
case '.json':
case '.js':
case '.ts':
case '.html':
case '.css':
case '.xml':
case '.csv':
content = await this.parseTextFile(filePath);
break;
case '.doc':
case '.docx':
content = await this.parseWordFile(filePath);
break;
case '.pdf':
content = await this.parsePdfFile(filePath);
break;
case '.xls':
case '.xlsx':
content = await this.parseExcelFile(filePath);
break;
case '.pptx':
content = await this.parsePowerPointFile(filePath);
break;
default:
// 지원하지 않는 파일 형식은 텍스트로 시도
try {
content = await this.parseTextFile(filePath);
} catch (error) {
this.debug(`지원하지 않는 파일 형식: ${filePath}`);
return null;
}
}
this.debug(`파일 파싱 완료: ${filePath} (${content.length}자)`);
return {
file: filePath,
content: content,
extension: ext,
size: stats.size,
mtime: stats.mtime
};
} catch (error) {
this.debug(`파일 파싱 오류: ${filePath} - ${error.message}`);
return null;
}
}
// 텍스트 파일 파싱
async parseTextFile(filePath) {
const content = await fs.readFile(filePath, 'utf8');
return content;
}
// Word 파일 파싱
async parseWordFile(filePath) {
const buffer = await fs.readFile(filePath);
const result = await mammoth.extractRawText({ buffer });
return result.value;
}
// PDF 파일 파싱
async parsePdfFile(filePath) {
const buffer = await fs.readFile(filePath);
const result = await pdfParse(buffer);
return result.text;
}
// Excel 파일 파싱
async parseExcelFile(filePath) {
const workbook = XLSX.readFile(filePath);
let content = '';
workbook.SheetNames.forEach(sheetName => {
const sheet = workbook.Sheets[sheetName];
const sheetData = XLSX.utils.sheet_to_csv(sheet);
content += `Sheet: ${sheetName}\n${sheetData}\n\n`;
});
return content;
}
// PowerPoint 파일 파싱 (개선된 버전)
async parsePowerPointFile(filePath) {
try {
this.debug(`PowerPoint 파일 파싱 시작: ${filePath}`);
// PPTX 파일 읽기
const data = await fs.readFile(filePath);
const zip = await JSZip.loadAsync(data);
let allText = '';
// 슬라이드 파일들을 찾아서 처리
const slideFiles = Object.keys(zip.files).filter(name =>
name.startsWith('ppt/slides/slide') && name.endsWith('.xml')
);
this.debug(`PowerPoint 슬라이드 파일 ${slideFiles.length}개 발견`);
for (const slideFile of slideFiles) {
try {
const xmlContent = await zip.files[slideFile].async('text');
const parser = new xml2js.Parser();
const result = await parser.parseStringPromise(xmlContent);
// XML에서 텍스트 추출
const slideText = this.extractTextFromXml(result);
if (slideText) {
allText += slideText + '\n\n';
}
} catch (error) {
this.debug(`슬라이드 파일 ${slideFile} 파싱 오류: ${error.message}`);
}
}
this.debug(`PowerPoint 파일 파싱 완료: ${filePath}, 추출된 텍스트 길이: ${allText.length}`);
return allText;
} catch (error) {
this.debug(`PowerPoint 파일 파싱 오류: ${filePath} - ${error.message}`);
return '';
}
}
// XML에서 텍스트 추출 헬퍼 함수
extractTextFromXml(xmlObj) {
let texts = [];
const traverse = (obj) => {
if (obj && typeof obj === 'object') {
// 텍스트 노드 찾기
if (obj['a:t'] && Array.isArray(obj['a:t'])) {
obj['a:t'].forEach(textNode => {
if (typeof textNode === 'string') {
texts.push(textNode);
} else if (textNode._) {
texts.push(textNode._);
}
});
}
// 재귀적으로 모든 프로퍼티 탐색
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
traverse(obj[key]);
}
}
} else if (Array.isArray(obj)) {
obj.forEach(item => traverse(item));
}
};
traverse(xmlObj);
return texts.join(' ').trim();
}
// 지원하는 파일 확장자 목록
getSupportedExtensions() {
return [
'.txt', '.md', '.json', '.js', '.ts', '.html', '.css', '.xml', '.csv',
'.doc', '.docx', '.pdf', '.xls', '.xlsx', '.pptx'
];
}
// 파일이 지원되는지 확인
isSupported(filePath) {
const ext = this.getFileExtension(filePath);
return this.getSupportedExtensions().includes(ext);
}
}
module.exports = FileParser;