0a9588abb7
- Add pagination support to findAll (page, limit query params) - Add findByTemplateId method to service - Add GET /by-template/:templateId endpoint to controller - Service already includes CRUD for QuestionBank and QuestionBankItem
247 lines
7.0 KiB
TypeScript
247 lines
7.0 KiB
TypeScript
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<string, string>();
|
|
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<string, string>,
|
|
rootGroupId: string,
|
|
): Promise<string> {
|
|
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<string, string> = {
|
|
'.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';
|
|
}
|
|
}
|