Skip to content

Commit 2ca919a

Browse files
committed
🚧 Start migration from file module to storage module with bun integration
1 parent fcf2cb9 commit 2ca919a

File tree

11 files changed

+273
-107
lines changed

11 files changed

+273
-107
lines changed

bun.lock

Lines changed: 88 additions & 88 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,19 @@
1414
"dependencies": {
1515
"@fastify/helmet": "^13.0.1",
1616
"@fastify/static": "^8.1.1",
17-
"@nestjs/common": "^11.0.13",
17+
"@nestjs/common": "^11.0.15",
1818
"@nestjs/config": "^4.0.2",
1919
"@nestjs/cli": "^11.0.6",
20-
"@nestjs/core": "^11.0.13",
20+
"@nestjs/core": "^11.0.15",
2121
"@nestjs/jwt": "^11.0.0",
2222
"@nestjs/passport": "^11.0.5",
23-
"@nestjs/platform-fastify": "^11.0.13",
24-
"@nestjs/platform-socket.io": "^11.0.13",
23+
"@nestjs/platform-fastify": "^11.0.15",
24+
"@nestjs/platform-socket.io": "^11.0.15",
2525
"@nestjs/schedule": "^5.0.1",
2626
"@nestjs/swagger": "^11.1.1",
2727
"@nestjs/throttler": "^6.4.0",
28-
"@nestjs/websockets": "^11.0.13",
29-
"@prisma/client": "6.5.0",
28+
"@nestjs/websockets": "^11.0.15",
29+
"@prisma/client": "6.6.0",
3030
"axios": "^1.8.4",
3131
"class-transformer": "^0.5.1",
3232
"class-validator": "^0.14.1",
@@ -35,23 +35,23 @@
3535
"jszip": "^3.10.1",
3636
"minio": "^8.0.5",
3737
"passport-jwt": "^4.0.1",
38-
"sharp": "^0.33.5",
38+
"sharp": "^0.34.1",
3939
"socket.io": "^4.8.1",
4040
"swagger-themes": "^1.4.3",
4141
"uuid": "^11.1.0"
4242
},
4343
"devDependencies": {
44-
"@nestjs/schematics": "^11.0.3",
44+
"@nestjs/schematics": "^11.0.5",
4545
"@stylistic/eslint-plugin": "^4.2.0",
46-
"@types/bun": "^1.2.8",
46+
"@types/bun": "^1.2.9",
4747
"@types/jsdom": "^21.1.7",
4848
"@types/node": "^22.14.0",
4949
"@types/passport-jwt": "^4.0.1",
50-
"@typescript-eslint/parser": "^8.29.0",
51-
"eslint": "^9.23.0",
52-
"prisma": "^6.5.0",
50+
"@typescript-eslint/parser": "^8.29.1",
51+
"eslint": "^9.24.0",
52+
"prisma": "^6.6.0",
5353
"source-map-support": "^0.5.21",
54-
"typescript": "^5.8.2"
54+
"typescript": "^5.8.3"
5555
},
5656
"prisma": {
5757
"seed": "bun prisma/seed.ts"

prisma/schema.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
generator client {
88
provider = "prisma-client-js"
9+
output = "../node_modules/.prisma/client"
910
}
1011

1112
datasource db {

src/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {WebsocketModule} from "./modules/websocket/websocket.module";
1313
import {CustomValidationPipe} from "./common/pipes/custom-validation.pipe";
1414
import {MiscModule} from "./modules/misc/misc.module";
1515
import {UsersModule} from "./modules/users/users.module";
16+
import {StorageModule} from "./modules/storage/storage.module";
1617

1718
@Module({
1819
imports: [
@@ -31,6 +32,7 @@ import {UsersModule} from "./modules/users/users.module";
3132
ImageModule,
3233
WebsocketModule,
3334
UsersModule,
35+
StorageModule,
3436
],
3537
controllers: [],
3638
providers: [

src/modules/file/file.service.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {Injectable, NotFoundException} from "@nestjs/common";
1+
import {Injectable, Logger, NotFoundException} from "@nestjs/common";
22
import {ConfigService} from "@nestjs/config";
33
import {PrismaService} from "../misc/prisma.service";
44
import {MiscService} from "../misc/misc.service";
@@ -12,6 +12,7 @@ import {BucketItem} from "minio";
1212
export class FileService{
1313
private readonly saver: Saver;
1414
private readonly secondarySaver: Saver;
15+
private readonly logger: Logger = new Logger(FileService.name);
1516

1617
constructor(
1718
private readonly configService: ConfigService,
@@ -27,7 +28,11 @@ export class FileService{
2728
this.saver = this.getFileSaver();
2829
}
2930

31+
/**
32+
* @deprecated
33+
*/
3034
getS3Saver(){
35+
this.logger.warn("Deprecated: FileService.getS3Saver");
3136
return new S3Saver(
3237
this.configService.get("S3_ENDPOINT"),
3338
this.configService.get("S3_PORT"),
@@ -39,19 +44,33 @@ export class FileService{
3944
);
4045
}
4146

47+
/**
48+
* @deprecated
49+
*/
4250
getFileSaver(){
51+
this.logger.warn("Deprecated: FileService.getFileSaver");
4352
return new FileSaver("images");
4453
}
4554

55+
/**
56+
* @deprecated
57+
* @param data
58+
*/
4659
async saveImage(data: Buffer): Promise<string>{
60+
this.logger.warn("Deprecated: FileService.saveImage");
4761
const sum = this.cipherService.getSum(data);
4862
await this.saver.saveFile(data, sum);
4963
if(this.configService.get("FILESYSTEM") === "both")
5064
await this.secondarySaver.saveFile(data, sum);
5165
return sum;
5266
}
5367

68+
/**
69+
* @deprecated
70+
* @param sum
71+
*/
5472
async loadImage(sum: string): Promise<Buffer>{
73+
this.logger.warn("Deprecated: FileService.loadImage");
5574
try{
5675
const stream = await this.saver.getFile(sum);
5776
return await new Promise<Buffer>((resolve, reject) => {
@@ -65,13 +84,22 @@ export class FileService{
6584
}
6685
}
6786

87+
/**
88+
* @deprecated
89+
* @param sum
90+
*/
6891
async removeImage(sum: string): Promise<void>{
92+
this.logger.warn("Deprecated: FileService.removeImage");
6993
await this.saver.removeFile(sum);
7094
if(this.configService.get("FILESYSTEM") === "both")
7195
await this.secondarySaver.removeFile(sum);
7296
}
7397

98+
/**
99+
* @deprecated
100+
*/
74101
async checkIntegrity(){
102+
this.logger.warn("Deprecated: FileService.checkIntegrity");
75103
if(this.configService.get("FILESYSTEM") === "both"){
76104
await this.checkS3Integrity();
77105
await this.checkLocalIntegrity();
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import {StorageService} from "./storage.service";
2+
import {MiscModule} from "../misc/misc.module";
3+
import {Module} from "@nestjs/common";
4+
5+
@Module({
6+
providers: [StorageService],
7+
exports: [StorageService],
8+
imports: [MiscModule],
9+
})
10+
export class StorageModule{}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import {Injectable, Logger} from "@nestjs/common";
2+
import {BunFile, Glob, S3Client, S3File, S3ListObjectsResponse} from "bun";
3+
import {MiscService} from "../misc/misc.service";
4+
5+
@Injectable()
6+
export class StorageService{
7+
private readonly s3Client?: S3Client;
8+
private readonly logger: Logger = new Logger(StorageService.name);
9+
10+
constructor(
11+
private readonly miscService: MiscService,
12+
){
13+
if(process.env.S3_ENDPOINT)
14+
this.s3Client = new S3Client({
15+
endpoint: process.env.S3_ENDPOINT,
16+
bucket: process.env.S3_BUCKET_NAME,
17+
region: process.env.S3_REGION,
18+
accessKeyId: process.env.S3_ACCESS_KEY,
19+
secretAccessKey: process.env.S3_SECRET_KEY,
20+
});
21+
}
22+
23+
private getFileName(sum: string): string{
24+
if(this.s3Client)
25+
return `${sum.substring(0, 2)}/${sum}.webp`;
26+
return `.storage/${sum.substring(0, 2)}/${sum}.webp`;
27+
}
28+
29+
async uploadBuffer(data: Buffer): Promise<string>{
30+
const sum: string = this.miscService.getSum(data);
31+
const fileName: string = this.getFileName(sum);
32+
this.logger.verbose(`Uploading file ${fileName}`);
33+
let file: BunFile | S3File;
34+
if(this.s3Client)
35+
file = this.s3Client.file(fileName);
36+
else
37+
file = Bun.file(fileName);
38+
await file.write(data);
39+
return sum;
40+
}
41+
42+
async downloadBuffer(sum: string): Promise<Buffer>{
43+
const fileName: string = this.getFileName(sum);
44+
this.logger.verbose(`Downloading file ${fileName}`);
45+
if(this.s3Client)
46+
return Buffer.from(await this.s3Client.file(fileName).arrayBuffer());
47+
else
48+
return Buffer.from(await Bun.file(fileName).arrayBuffer());
49+
}
50+
51+
async deleteFile(sum: string): Promise<void>{
52+
const fileName: string = this.getFileName(sum);
53+
this.logger.verbose(`Deleting file ${fileName}`);
54+
if(this.s3Client)
55+
await this.s3Client.delete(fileName);
56+
else
57+
await Bun.file(fileName).delete();
58+
}
59+
60+
async presign(sum: string, expiresIn: number = 60 * 60): Promise<string>{
61+
const fileName: string = this.getFileName(sum);
62+
this.logger.verbose(`Presigning file ${fileName}`);
63+
if(!this.s3Client)
64+
throw new Error("S3 client not initialized");
65+
return this.s3Client.presign(fileName, {
66+
expiresIn,
67+
});
68+
}
69+
70+
async listFiles(take: number = Infinity, skip: number = 0): Promise<(BunFile | S3File)[]>{
71+
const files: (BunFile | S3File)[] = [];
72+
if(this.s3Client){
73+
let continuationToken: string | undefined;
74+
let remainingItems: number = take;
75+
let totalSkipped: number = 0;
76+
do{
77+
const s3Files: S3ListObjectsResponse = await this.s3Client.list({
78+
maxKeys: Math.min(remainingItems + Math.max(0, skip - totalSkipped), 1000),
79+
continuationToken,
80+
});
81+
let itemsToProcess = s3Files.contents || [];
82+
if(totalSkipped < skip){
83+
const skipInThisBatch: number = Math.min(skip - totalSkipped, itemsToProcess.length);
84+
itemsToProcess = itemsToProcess.slice(skipInThisBatch);
85+
totalSkipped += skipInThisBatch;
86+
}
87+
const itemsToTake: number = Math.min(remainingItems, itemsToProcess.length);
88+
for(const file of itemsToProcess.slice(0, itemsToTake))
89+
files.push(this.s3Client.file(file.key));
90+
remainingItems -= itemsToTake;
91+
continuationToken = s3Files.nextContinuationToken;
92+
}while(remainingItems > 0 && continuationToken);
93+
return files;
94+
}
95+
const glob = new Glob("**/*");
96+
for(const filePath of glob.scanSync("./.storage"))
97+
files.push(Bun.file(".storage/" + filePath));
98+
return files.slice(skip, take === Infinity ? undefined : take + skip);
99+
}
100+
}

src/modules/webtoon/image/image.controller.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
1-
import {BadRequestException, Controller, Get, Header, Param} from "@nestjs/common";
1+
import {BadRequestException, Controller, Get, Header, HttpCode, Param, Res} from "@nestjs/common";
22
import {ApiResponse, ApiTags} from "@nestjs/swagger";
33
import {WebtoonDatabaseService} from "../webtoon/webtoon-database.service";
44
import {HttpStatusCode} from "axios";
55
import {ImageSumDto} from "./models/dto/image-sum.dto";
66
import {Throttle} from "@nestjs/throttler";
7+
import {StorageService} from "../../storage/storage.service";
8+
import {type FastifyReply} from "fastify";
79

810
@Controller("image")
911
@ApiTags("Image")
1012
@Throttle({default: {limit: 400, ttl: 60000}})
1113
export class ImageController{
1214
constructor(
1315
private readonly webtoonDatabaseService: WebtoonDatabaseService,
16+
private readonly storageService: StorageService,
1417
){}
1518

1619
@Get(":sum")
@@ -25,4 +28,19 @@ export class ImageController{
2528
throw new BadRequestException("Invalid sha256 sum");
2629
return this.webtoonDatabaseService.loadImage(imageSumDto.sum);
2730
}
31+
32+
@Get("v2/:sum")
33+
@Header("Content-Type", "image/webp")
34+
@HttpCode(HttpStatusCode.Found)
35+
@Header("Cache-Control", "public, max-age=604800000")
36+
@ApiResponse({status: HttpStatusCode.Found, description: "Get image"})
37+
@ApiResponse({status: HttpStatusCode.NotFound, description: "Not found"})
38+
@ApiResponse({status: HttpStatusCode.BadRequest, description: "Invalid sha256 sum"})
39+
async getPresignedImage(@Param() imageSumDto: ImageSumDto, @Res() res: FastifyReply){
40+
const regex = new RegExp("^[a-f0-9]{64}$");
41+
if(!regex.test(imageSumDto.sum))
42+
throw new BadRequestException("Invalid sha256 sum");
43+
const presignedUrl: string = await this.storageService.presign(imageSumDto.sum, 60 * 60 * 24 * 7);
44+
res.redirect(presignedUrl);
45+
}
2846
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import {Module} from "@nestjs/common";
22
import {ImageController} from "./image.controller";
33
import {WebtoonModule} from "../webtoon/webtoon.module";
4+
import {StorageModule} from "../../storage/storage.module";
45

56
@Module({
67
controllers: [ImageController],
7-
imports: [WebtoonModule],
8+
imports: [WebtoonModule, StorageModule],
89
})
910
export class ImageModule{}

src/modules/webtoon/webtoon/webtoon-database.service.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import MigrationInfosResponse from "../migration/models/responses/migration-info
1414
import {FileService} from "../../file/file.service";
1515
import {ConfigService} from "@nestjs/config";
1616
import {Images} from "@prisma/client";
17+
import {StorageService} from "../../storage/storage.service";
1718

1819
@Injectable()
1920
export class WebtoonDatabaseService{
@@ -25,6 +26,7 @@ export class WebtoonDatabaseService{
2526
private readonly prismaService: PrismaService,
2627
private readonly fileService: FileService,
2728
private readonly configService: ConfigService,
29+
private readonly storageService: StorageService,
2830
){}
2931

3032
async saveEpisode(webtoon: CachedWebtoonModel, episode: EpisodeModel, episodeData: EpisodeDataModel, index: number, force: boolean = false): Promise<void>{
@@ -485,15 +487,17 @@ export class WebtoonDatabaseService{
485487
async saveImage(image?: Buffer): Promise<string | undefined>{
486488
if(!image)
487489
return undefined;
488-
return await this.fileService.saveImage(image);
490+
// return await this.fileService.saveImage(image);
491+
return await this.storageService.uploadBuffer(image);
489492
}
490493

491494
async loadImage(imageSum: string): Promise<Buffer>{
492-
return await this.fileService.loadImage(imageSum);
495+
// return await this.fileService.loadImage(imageSum);
496+
return await this.storageService.downloadBuffer(imageSum);
493497
}
494498

495499
async removeImage(imageSum: string): Promise<void>{
496-
await this.fileService.removeImage(imageSum);
500+
await this.storageService.deleteFile(imageSum);
497501
}
498502

499503
async getRandomThumbnails(){

0 commit comments

Comments
 (0)