Browse Source

fix: resolve K-1 PDF upload ENOENT and controller route prefix issues

- Register MulterModule with disk storage in K1ImportModule so FileInterceptor
  writes uploaded files to disk (sets file.filename and file.path)
- Make UploadService.createDocument robust for both disk and memory storage
  by generating filename and writing buffer to disk when file.filename is unset
- Fix reprocess method to join relative filePath with uploadDir for absolute path
- Remove duplicate 'api/v1' prefix from K1ImportController and CellMappingController
  (NestJS global prefix + URI versioning already adds /api/v1/)
pull/6701/head
Robert Patch 2 months ago
parent
commit
220417bc02
  1. 2
      apps/api/src/app/cell-mapping/cell-mapping.controller.ts
  2. 2
      apps/api/src/app/k1-import/k1-import.controller.ts
  3. 37
      apps/api/src/app/k1-import/k1-import.module.ts
  4. 8
      apps/api/src/app/k1-import/k1-import.service.ts
  5. 17
      apps/api/src/app/upload/upload.service.ts

2
apps/api/src/app/cell-mapping/cell-mapping.controller.ts

@ -14,7 +14,7 @@ import { AuthGuard } from '@nestjs/passport';
import { CellMappingService } from './cell-mapping.service'; import { CellMappingService } from './cell-mapping.service';
@Controller('api/v1/cell-mapping') @Controller('cell-mapping')
export class CellMappingController { export class CellMappingController {
public constructor( public constructor(
private readonly cellMappingService: CellMappingService private readonly cellMappingService: CellMappingService

2
apps/api/src/app/k1-import/k1-import.controller.ts

@ -26,7 +26,7 @@ import { ConfirmK1Dto } from './dto/confirm-k1.dto';
import { VerifyK1Dto } from './dto/verify-k1.dto'; import { VerifyK1Dto } from './dto/verify-k1.dto';
import { K1ImportService } from './k1-import.service'; import { K1ImportService } from './k1-import.service';
@Controller('api/v1/k1-import') @Controller('k1-import')
export class K1ImportController { export class K1ImportController {
public constructor( public constructor(
private readonly k1ImportService: K1ImportService, private readonly k1ImportService: K1ImportService,

37
apps/api/src/app/k1-import/k1-import.module.ts

@ -2,6 +2,11 @@ import { ConfigurationModule } from '@ghostfolio/api/services/configuration/conf
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { MulterModule } from '@nestjs/platform-express';
import { diskStorage } from 'multer';
import { existsSync, mkdirSync } from 'node:fs';
import { join } from 'node:path';
import { v4 as uuidv4 } from 'uuid';
import { CellMappingModule } from '../cell-mapping/cell-mapping.module'; import { CellMappingModule } from '../cell-mapping/cell-mapping.module';
import { UploadModule } from '../upload/upload.module'; import { UploadModule } from '../upload/upload.module';
@ -15,10 +20,40 @@ import { AzureExtractor } from './extractors/azure-extractor';
import { PdfParseExtractor } from './extractors/pdf-parse-extractor'; import { PdfParseExtractor } from './extractors/pdf-parse-extractor';
import { TesseractExtractor } from './extractors/tesseract-extractor'; import { TesseractExtractor } from './extractors/tesseract-extractor';
const uploadDir = process.env.UPLOAD_DIR || join(process.cwd(), 'uploads');
@Module({ @Module({
controllers: [K1ImportController], controllers: [K1ImportController],
exports: [K1ImportService], exports: [K1ImportService],
imports: [CellMappingModule, ConfigurationModule, PrismaModule, UploadModule], imports: [
CellMappingModule,
ConfigurationModule,
MulterModule.register({
limits: {
fileSize: 25 * 1024 * 1024 // 25 MB
},
storage: diskStorage({
destination: (_req, _file, cb) => {
const now = new Date();
const yearDir = now.getFullYear().toString();
const monthDir = (now.getMonth() + 1).toString().padStart(2, '0');
const subDir = join(uploadDir, yearDir, monthDir);
if (!existsSync(subDir)) {
mkdirSync(subDir, { recursive: true });
}
cb(null, subDir);
},
filename: (_req, file, cb) => {
const ext = file.originalname.split('.').pop();
cb(null, `${uuidv4()}.${ext}`);
}
})
}),
PrismaModule,
UploadModule
],
providers: [ providers: [
AzureExtractor, AzureExtractor,
K1AggregationService, K1AggregationService,

8
apps/api/src/app/k1-import/k1-import.service.ts

@ -476,16 +476,18 @@ export class K1ImportService {
// Read file from disk and run extraction asynchronously // Read file from disk and run extraction asynchronously
const fs = await import('fs/promises'); const fs = await import('fs/promises');
const filePath = (document as any).url || (document as any).filePath; const relativePath = (document as any).filePath;
if (!filePath) { if (!relativePath) {
throw new HttpException( throw new HttpException(
'Cannot determine file path for stored document', 'Cannot determine file path for stored document',
StatusCodes.INTERNAL_SERVER_ERROR StatusCodes.INTERNAL_SERVER_ERROR
); );
} }
const fileBuffer = await fs.readFile(filePath); const uploadDir = this.uploadService.getUploadDir();
const fullPath = join(uploadDir, relativePath);
const fileBuffer = await fs.readFile(fullPath);
const file = { const file = {
buffer: fileBuffer, buffer: fileBuffer,
originalname: originalSession.fileName, originalname: originalSession.fileName,

17
apps/api/src/app/upload/upload.service.ts

@ -4,8 +4,9 @@ import { HttpException, Injectable } from '@nestjs/common';
import { DocumentType } from '@prisma/client'; import { DocumentType } from '@prisma/client';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { createReadStream, existsSync } from 'node:fs'; import { createReadStream, existsSync } from 'node:fs';
import { mkdir } from 'node:fs/promises'; import { mkdir, writeFile } from 'node:fs/promises';
import { join } from 'node:path'; import { join } from 'node:path';
import { v4 as uuidv4 } from 'uuid';
@Injectable() @Injectable()
export class UploadService { export class UploadService {
@ -51,7 +52,19 @@ export class UploadService {
await mkdir(subDir, { recursive: true }); await mkdir(subDir, { recursive: true });
} }
const relativePath = `${yearDir}/${monthDir}/${file.filename}`; // Support both disk storage (file.filename set by multer) and memory storage (file.buffer)
let filename = file.filename;
if (!filename) {
const ext = (file.originalname || 'file').split('.').pop();
filename = `${uuidv4()}.${ext}`;
if (file.buffer) {
await writeFile(join(subDir, filename), file.buffer);
}
}
const relativePath = `/${yearDir}/${monthDir}/${filename}`;
return this.prismaService.document.create({ return this.prismaService.document.create({
data: { data: {

Loading…
Cancel
Save