import { Injectable, Logger, BadRequestException } from '@nestjs/common'; import { KnowledgeBaseService } from '../knowledge-base/knowledge-base.service'; import { KnowledgeGroupService } from '../knowledge-group/knowledge-group.service'; import * as fs from 'fs'; import * as path from 'path'; @Injectable() export class UploadService { private readonly logger = new Logger(UploadService.name); constructor( private kbService: KnowledgeBaseService, private groupService: KnowledgeGroupService, ) {} async processUploadedFile(file: Express.Multer.File) { // Add more business logic here. Example: // - Save file info to database // - Call other services to process file (Tika text extraction, ES indexing etc.) // - Validate file format or analyze content // Currently only return basic file info return { filename: file.filename, originalname: file.originalname, size: file.size, mimetype: file.mimetype, path: file.path, // After Multer saves file, full path is in file.path }; } async importLocalFolder( sourcePath: string, userId: string, tenantId: string, config: any, ) { if (!fs.existsSync(sourcePath)) { throw new BadRequestException(`Directory not found: ${sourcePath}`); } const stat = fs.statSync(sourcePath); if (!stat.isDirectory()) { throw new BadRequestException(`Path is not a directory: ${sourcePath}`); } // Determine root group for hierarchy or single group let rootGroupId: string | null = null; if (config.groupIds && config.groupIds.length > 0) { rootGroupId = config.groupIds[0]; } this.logger.log( `Starting local folder import: ${sourcePath} for user ${userId}, tenant ${tenantId}`, ); // Trigger scanning and processing asynchronously to not block the request this.executeLocalImport( sourcePath, userId, tenantId, config, rootGroupId, ).catch((err) => { this.logger.error(`Local folder import failed for ${sourcePath}`, err); }); return { sourcePath, status: 'PROCESSING', }; } private async executeLocalImport( sourcePath: string, userId: string, tenantId: string, config: any, rootGroupId: string | null, ) { const files = this.scanDir(sourcePath); this.logger.log(`Found ${files.length} files in ${sourcePath}`); const dirToGroupId = new Map(); if (rootGroupId) { dirToGroupId.set('.', rootGroupId); } else { // Create a root group based on folder name if none provided const rootName = path.basename(sourcePath); const rootGroup = await this.groupService.create(userId, tenantId, { name: rootName, description: `Imported from local path: ${sourcePath}`, }); rootGroupId = rootGroup.id; dirToGroupId.set('.', rootGroupId); } const uploadBaseDir = process.env.UPLOAD_FILE_PATH || './uploads'; for (const filePath of files) { try { const relativeDir = path.relative(sourcePath, path.dirname(filePath)); const normalizedDir = relativeDir || '.'; let targetGroupId = rootGroupId; if (config.useHierarchy) { targetGroupId = await this.ensureHierarchy( userId, tenantId, normalizedDir, dirToGroupId, rootGroupId, ); } const filename = path.basename(filePath); const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9); const storedFilename = `local-${uniqueSuffix}-${filename}`; // Ensure tenant directory exists const tenantDir = path.join(uploadBaseDir, tenantId); if (!fs.existsSync(tenantDir)) { fs.mkdirSync(tenantDir, { recursive: true }); } const targetPath = path.join(tenantDir, storedFilename); fs.copyFileSync(filePath, targetPath); const stats = fs.statSync(targetPath); const fileInfo = { filename: storedFilename, originalname: filename, path: targetPath, size: stats.size, mimetype: this.getMimeType(filename), }; await this.kbService.createAndIndex(fileInfo, userId, tenantId, { ...config, groupIds: [targetGroupId], }); } catch (err) { this.logger.error(`Failed to process local file: ${filePath}`, err); } } this.logger.log(`Local folder import completed: ${sourcePath}`); } private async ensureHierarchy( userId: string, tenantId: string, relativeDir: string, dirToGroupId: Map, rootGroupId: string, ): Promise { if (dirToGroupId.has(relativeDir)) { return dirToGroupId.get(relativeDir)!; } const segments = relativeDir.split(path.sep); let currentPath = ''; let parentId = rootGroupId; for (const segment of segments) { if (!segment || segment === '.') continue; currentPath = currentPath ? path.join(currentPath, segment) : segment; if (dirToGroupId.has(currentPath)) { parentId = dirToGroupId.get(currentPath)!; continue; } const group = await this.groupService.findOrCreate( userId, tenantId, segment, parentId, `Sub-folder from local import: ${currentPath}`, ); dirToGroupId.set(currentPath, group.id); parentId = group.id; } return parentId; } private scanDir(directory: string): string[] { let results: string[] = []; if (!fs.existsSync(directory)) return results; const items = fs.readdirSync(directory); for (const item of items) { const fullPath = path.join(directory, item); const stat = fs.statSync(fullPath); if (stat.isDirectory()) { results = results.concat(this.scanDir(fullPath)); } else { // Only include supported document and code extensions const ext = path.extname(item).toLowerCase().slice(1); if ( [ 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'rtf', 'csv', 'txt', 'md', 'html', 'json', 'xml', 'js', 'ts', 'py', 'java', 'sql', ].includes(ext) ) { results.push(fullPath); } } } return results; } private getMimeType(filename: string): string { const ext = path.extname(filename).toLowerCase(); const mimeMap: Record = { '.pdf': 'application/pdf', '.doc': 'application/msword', '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', '.md': 'text/markdown', '.txt': 'text/plain', '.json': 'application/json', '.html': 'text/html', '.csv': 'text/csv', }; return mimeMap[ext] || 'application/octet-stream'; } }