init
This commit is contained in:
150
server/src/modules/gitlab-storage/gitlab-storage.service.ts
Normal file
150
server/src/modules/gitlab-storage/gitlab-storage.service.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user