Compare commits

..

6 Commits

Author SHA1 Message Date
075ce07550 Feat: add package list
All checks were successful
Build and push image / release-image (push) Successful in 3m34s
2025-07-29 01:12:50 +08:00
143e6747cc Chore: more verbose and bump version
All checks were successful
Build and push image / release-image (push) Successful in 1m27s
2025-07-27 21:47:35 +08:00
4d7049a30a Fix: e2e test
All checks were successful
Build and push image / release-image (push) Successful in 1m38s
2025-07-10 18:25:38 +08:00
4e3ac71c78 Chore: lint
All checks were successful
Build and push image / release-image (push) Successful in 1m32s
2025-07-10 18:02:49 +08:00
6ab755d91a Fix: range request header on response
All checks were successful
Build and push image / release-image (push) Successful in 1m34s
2025-07-10 17:12:33 +08:00
8b66e589d7 Fix: add not found response
All checks were successful
Build and push image / release-image (push) Successful in 1m39s
2025-07-10 16:26:16 +08:00
5 changed files with 97 additions and 21 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "archrepo", "name": "archrepo",
"version": "0.0.1", "version": "2.0.2",
"description": "", "description": "",
"author": "", "author": "",
"private": true, "private": true,

View File

@@ -1,4 +1,5 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { ConfigService } from '@nestjs/config';
import { AppController } from './app.controller'; import { AppController } from './app.controller';
import { AppService } from './app.service'; import { AppService } from './app.service';
@@ -8,7 +9,7 @@ describe('AppController', () => {
beforeEach(async () => { beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({ const app: TestingModule = await Test.createTestingModule({
controllers: [AppController], controllers: [AppController],
providers: [AppService], providers: [ConfigService, AppService],
}).compile(); }).compile();
appController = app.get<AppController>(AppController); appController = app.get<AppController>(AppController);

View File

@@ -1,15 +1,25 @@
import { Response } from 'express';
import { Readable } from 'stream';
import { import {
Controller, Controller,
Get, Get,
Header, Header,
Res,
Param, Param,
Query,
Headers, Headers,
HttpException, HttpException,
HttpStatus, HttpStatus,
StreamableFile, StreamableFile,
} from '@nestjs/common'; } from '@nestjs/common';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { GetObjectCommandInput } from '@aws-sdk/client-s3'; import { ApiOkResponse, ApiNotFoundResponse } from '@nestjs/swagger';
import {
GetObjectCommandInput,
ListObjectsV2CommandInput,
} from '@aws-sdk/client-s3';
import { AppService } from './app.service'; import { AppService } from './app.service';
@Controller() @Controller()
@@ -19,15 +29,19 @@ export class AppController {
private readonly appService: AppService, private readonly appService: AppService,
) {} ) {}
@ApiOkResponse()
@Get('healthz') @Get('healthz')
getHealthz(): string { getHealthz(): string {
return this.appService.getHealthz(); return this.appService.getHealthz();
} }
@Get(`:repo/os/:arch/:pkg`) @ApiOkResponse()
@ApiNotFoundResponse()
@Get(':repo/os/:arch/:pkg')
@Header('Accept-Ranges', 'bytes') @Header('Accept-Ranges', 'bytes')
async getRepo( async getPackage(
@Headers('Range') range: string, @Headers('Range') range: string,
@Res({ passthrough: true }) res: Response,
@Param('repo') repo: string, @Param('repo') repo: string,
@Param('arch') arch: string, @Param('arch') arch: string,
@Param('pkg') pkg: string, @Param('pkg') pkg: string,
@@ -40,12 +54,54 @@ export class AppController {
HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND,
); );
const stream = await this.appService.getObject({ const output = await this.appService.getObject({
Bucket: this.configService.get<string>('MINIO_BUCKET'), Bucket: this.configService.get<string>('MINIO_BUCKET'),
Key: pkg, Key: pkg,
Range: range, Range: range,
} as GetObjectCommandInput); } as GetObjectCommandInput);
if (!output || !output.Body) {
console.log(`No such file '${pkg}'`);
throw new HttpException(`No such file '${pkg}'`, HttpStatus.NOT_FOUND);
}
return new StreamableFile(stream); if (range !== undefined) {
res.set('Content-Length', `${output.ContentLength}`);
res.set('Content-Range', `${output.ContentRange}`);
}
return new StreamableFile(output.Body as Readable);
}
@ApiOkResponse()
@ApiNotFoundResponse()
@Get(':repo/os/:arch')
async getPackageList(
@Res({ passthrough: true }) res: Response,
@Param('repo') repo: string,
@Param('arch') arch: string,
@Query('format') format: string,
): Promise<string> {
if (repo !== this.configService.get<string>('REPO_NAME'))
throw new HttpException(`Repo '${repo}' not exist`, HttpStatus.NOT_FOUND)
if (arch !== this.configService.get<string>('ARCH_NAME'))
throw new HttpException(
`Architecture '${arch}' not exist`,
HttpStatus.NOT_FOUND,
);
const output = await this.appService.getObjectList({
Bucket: this.configService.get<string>('MINIO_BUCKET')
} as ListObjectsV2CommandInput);
if (!output) {
console.log('Cannot list files');
throw new HttpException(`Cannot list files`, HttpStatus.NOT_FOUND);
}
if (format === 'json')
return JSON.stringify(output.Contents?.reduce((acc, cur) => {
acc.push(cur.Key ?? '');
return acc;
}, Array<string>()), null, 2);
return output.Contents?.reduce(
(acc, cur) => `${acc}\n${cur.Key ?? ''}`, '') ?? '';
} }
} }

View File

@@ -1,11 +1,15 @@
import { Readable } from 'stream'; import { Injectable } from '@nestjs/common';
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { import {
S3Client, S3Client,
S3ClientConfig, S3ClientConfig,
GetObjectCommand, GetObjectCommand,
GetObjectCommandInput, GetObjectCommandInput,
GetObjectCommandOutput,
ListObjectsV2Command,
ListObjectsV2CommandInput,
ListObjectsV2CommandOutput,
NoSuchKey,
} from '@aws-sdk/client-s3'; } from '@aws-sdk/client-s3';
@Injectable() @Injectable()
@@ -28,14 +32,28 @@ export class AppService {
return 'OK'; return 'OK';
} }
async getObject(getObjectConfig: GetObjectCommandInput): Promise<Readable> { async getObject(
const command = new GetObjectCommand(getObjectConfig); config: GetObjectCommandInput,
const res = await this.s3Client.send(command); ): Promise<GetObjectCommandOutput | null> {
if (!res.Body) const command = new GetObjectCommand(config);
throw new HttpException( try {
's3 get object failed', return await this.s3Client.send(command);
HttpStatus.INTERNAL_SERVER_ERROR, } catch (err: unknown) {
); if (err instanceof NoSuchKey) return null;
return res.Body as Readable; throw err;
}
}
async getObjectList(
config: ListObjectsV2CommandInput,
): Promise<ListObjectsV2CommandOutput | null> {
const command = new ListObjectsV2Command(config);
try {
return await this.s3Client.send(command);
} catch (err: unknown) {
if (err instanceof Error)
return null;
throw err;
}
} }
} }

View File

@@ -1,5 +1,6 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common'; import { INestApplication } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import * as request from 'supertest'; import * as request from 'supertest';
import { App } from 'supertest/types'; import { App } from 'supertest/types';
import { AppModule } from './../src/app.module'; import { AppModule } from './../src/app.module';
@@ -16,10 +17,10 @@ describe('AppController (e2e)', () => {
await app.init(); await app.init();
}); });
it('/ (GET)', () => { it('/healthz (GET)', () => {
return request(app.getHttpServer()) return request(app.getHttpServer())
.get('/') .get('/healthz')
.expect(200) .expect(200)
.expect('Hello World!'); .expect('OK');
}); });
}); });