import { HttpStatus, Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import type { ToolArtifact } from '@prisma/client'; import { createReadStream, existsSync } from 'fs'; import { mkdir, writeFile } from 'fs/promises'; import { dirname, resolve } from 'path'; import { Readable } from 'stream'; import { ERROR_CODES } from '../../common/constants/error-codes'; import { AppException } from '../../common/exceptions/app.exception'; export interface ArtifactDownloadStream { stream: NodeJS.ReadableStream; fileName: string; mimeType?: string; fileSize: number; } export interface ArtifactUploadInput { toolId: string; version: string; fileName: string; mimeType?: string; buffer: Buffer; } export interface ArtifactUploadResult { gitlabProjectId: number; gitlabPackageName: string; gitlabPackageVersion: string; gitlabFilePath: string; } @Injectable() export class GitlabStorageService { constructor(private readonly configService: ConfigService) {} async getArtifactStream(artifact: ToolArtifact): Promise { const gitlabApiBase = this.configService.get('GITLAB_API_BASE'); const gitlabToken = this.configService.get('GITLAB_TOKEN'); if (gitlabApiBase && gitlabToken && artifact.gitlabProjectId > 0) { return this.downloadFromGitlab(artifact, gitlabApiBase, gitlabToken); } return this.readFromLocalStorage(artifact); } async uploadArtifact(input: ArtifactUploadInput): Promise { const gitlabApiBase = this.configService.get('GITLAB_API_BASE'); const gitlabToken = this.configService.get('GITLAB_TOKEN'); const projectId = Number(this.configService.get('GITLAB_PROJECT_ID', '0')); const packagePrefix = this.configService.get('GITLAB_PACKAGE_NAME_PREFIX', 'toolsshow'); const packageName = `${packagePrefix}/${input.toolId}`; if (gitlabApiBase && gitlabToken && projectId > 0) { const url = `${gitlabApiBase}/projects/${encodeURIComponent( String(projectId), )}/packages/generic/${encodeURIComponent(packageName)}/${encodeURIComponent( input.version, )}/${encodeURIComponent(input.fileName)}`; const response = await fetch(url, { method: 'PUT', headers: { 'PRIVATE-TOKEN': gitlabToken, 'Content-Type': input.mimeType ?? 'application/octet-stream', }, body: input.buffer as unknown as BodyInit, }); if (!response.ok) { throw new AppException( ERROR_CODES.GITLAB_UPLOAD_FAILED, 'failed to upload artifact to GitLab', HttpStatus.BAD_GATEWAY, ); } return { gitlabProjectId: projectId, gitlabPackageName: packageName, gitlabPackageVersion: input.version, gitlabFilePath: `${packageName}/${input.version}/${input.fileName}`, }; } const localRelativePath = `storage/uploads/${input.toolId}/${input.version}/${input.fileName}`; const localAbsolutePath = resolve(process.cwd(), localRelativePath); await mkdir(dirname(localAbsolutePath), { recursive: true }); await writeFile(localAbsolutePath, input.buffer); return { gitlabProjectId: 0, gitlabPackageName: packageName, gitlabPackageVersion: input.version, gitlabFilePath: localRelativePath.replace(/\\/g, '/'), }; } private async downloadFromGitlab( artifact: ToolArtifact, gitlabApiBase: string, gitlabToken: string, ): Promise { const url = `${gitlabApiBase}/projects/${encodeURIComponent( String(artifact.gitlabProjectId), )}/packages/generic/${encodeURIComponent(artifact.gitlabPackageName)}/${encodeURIComponent( artifact.gitlabPackageVersion, )}/${encodeURIComponent(artifact.fileName)}`; const response = await fetch(url, { headers: { 'PRIVATE-TOKEN': gitlabToken, }, }); if (!response.ok || !response.body) { throw new AppException( ERROR_CODES.GITLAB_DOWNLOAD_FAILED, 'failed to download artifact from GitLab', HttpStatus.BAD_GATEWAY, ); } return { stream: Readable.fromWeb(response.body as any), fileName: artifact.fileName, mimeType: artifact.mimeType ?? undefined, fileSize: artifact.fileSizeBytes, }; } private readFromLocalStorage(artifact: ToolArtifact): ArtifactDownloadStream { const filePath = resolve(process.cwd(), artifact.gitlabFilePath); if (!existsSync(filePath)) { throw new AppException( ERROR_CODES.GITLAB_DOWNLOAD_FAILED, `artifact file not found: ${artifact.gitlabFilePath}`, HttpStatus.NOT_FOUND, ); } return { stream: createReadStream(filePath), fileName: artifact.fileName, mimeType: artifact.mimeType ?? undefined, fileSize: artifact.fileSizeBytes, }; } }