151 lines
4.9 KiB
TypeScript
151 lines
4.9 KiB
TypeScript
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<ArtifactDownloadStream> {
|
|
const gitlabApiBase = this.configService.get<string>('GITLAB_API_BASE');
|
|
const gitlabToken = this.configService.get<string>('GITLAB_TOKEN');
|
|
|
|
if (gitlabApiBase && gitlabToken && artifact.gitlabProjectId > 0) {
|
|
return this.downloadFromGitlab(artifact, gitlabApiBase, gitlabToken);
|
|
}
|
|
|
|
return this.readFromLocalStorage(artifact);
|
|
}
|
|
|
|
async uploadArtifact(input: ArtifactUploadInput): Promise<ArtifactUploadResult> {
|
|
const gitlabApiBase = this.configService.get<string>('GITLAB_API_BASE');
|
|
const gitlabToken = this.configService.get<string>('GITLAB_TOKEN');
|
|
const projectId = Number(this.configService.get<string>('GITLAB_PROJECT_ID', '0'));
|
|
const packagePrefix = this.configService.get<string>('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<ArtifactDownloadStream> {
|
|
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,
|
|
};
|
|
}
|
|
}
|