Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions config/defaults.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ cloud_storage:
- gif
- mp3
- mp4
temp_photo:
# max single file size: 5M
single_file_size: 5242880
# max files per day per user: 60
total_files: 60
prefix_path: cloud-storage/temp-photo
allow_suffix:
- png
- jpg
- jpeg

user:
avatar:
Expand Down
8 changes: 8 additions & 0 deletions config/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ cloud_storage:
- gif
- mp3
- mp4
temp_photo:
single_file_size: 5242880
total_files: 60
prefix_path: cloud-storage/temp-photo
allow_suffix:
- png
- jpg
- jpeg

user:
avatar:
Expand Down
6 changes: 6 additions & 0 deletions src/constants/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,12 @@ export const CloudStorage = {
totalSize: config.cloud_storage.total_size,
prefixPath: config.cloud_storage.prefix_path,
allowFileSuffix: config.cloud_storage.allow_file_suffix,
tempPhoto: {
singleFileSize: config.cloud_storage.temp_photo.single_file_size,
totalFiles: config.cloud_storage.temp_photo.total_files,
prefixPath: config.cloud_storage.temp_photo.prefix_path,
allowSuffix: config.cloud_storage.temp_photo.allow_suffix,
},
};

export const StorageService = {
Expand Down
9 changes: 9 additions & 0 deletions src/plugins/Ajv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ const avatarSuffix: FormatDefinition<string> = {
},
};

const tempPhotoSuffix: FormatDefinition<string> = {
validate: fileName => {
const suffix = path.extname(fileName).slice(1).toLowerCase();

return CloudStorage.tempPhoto.allowSuffix.includes(suffix);
},
};

const oauthLogoSuffix: FormatDefinition<string> = {
validate: fileName => {
const suffix = path.extname(fileName).slice(1).toLowerCase();
Expand Down Expand Up @@ -98,6 +106,7 @@ export const ajvSelfPlugin = (ajv: Ajv): void => {
ajv.addFormat("uuid-v4", uuidV4);
ajv.addFormat("file-suffix", fileSuffix);
ajv.addFormat("avatar-suffix", avatarSuffix);
ajv.addFormat("temp-photo-suffix", tempPhotoSuffix);
ajv.addFormat("oauth-logo-suffix", oauthLogoSuffix);
ajv.addFormat("url", url);
ajv.addFormat("https", https);
Expand Down
6 changes: 6 additions & 0 deletions src/utils/ParseConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ type Config = {
total_size: number;
prefix_path: string;
allow_file_suffix: string[];
temp_photo: {
single_file_size: number;
total_files: number;
prefix_path: string;
allow_suffix: string[];
};
};
user: {
avatar: {
Expand Down
2 changes: 2 additions & 0 deletions src/utils/Redis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export const RedisKey = {
agoraRTMUserToken: (userUUID: string): string => `agora:rtm:userUUID:${userUUID}`,
cloudStorageFileInfo: (userUUID: string, fileUUID: string): string =>
`cloudStorage:${userUUID}:${fileUUID}`,
cloudStorageTempPhotoInfo: (userUUID: string, fileUUID: string): string =>
`cloudStorage:tempPhoto:${userUUID}:${fileUUID}`,
userAvatarFileInfo: (userUUID: string, fileUUID: string): string =>
`user:avatar:${userUUID}:${fileUUID}`,
roomInviteCode: (inviteCode: string): string => `room:invite:${inviteCode}`,
Expand Down
2 changes: 2 additions & 0 deletions src/v2/controllers/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { developerOAuthRouters } from "./developer/routes";
import { applicationRouters } from "./application/routes";
import { roomRouters } from "./room/routes";
import { oauthRouters } from "./auth2/routes";
import { tempPhotoRouters } from "./temp-photo/routes";

export const v2Routes = [
userRouters,
Expand All @@ -12,4 +13,5 @@ export const v2Routes = [
applicationRouters,
roomRouters,
oauthRouters,
tempPhotoRouters,
];
13 changes: 13 additions & 0 deletions src/v2/controllers/temp-photo/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Server } from "../../../utils/registryRoutersV2";
import { tempPhotoUploadStart, tempPhotoUploadStartSchema } from "./upload/start";
import { tempPhotoUploadFinish, tempPhotoUploadFinishSchema } from "./upload/finish";

export const tempPhotoRouters = (server: Server): void => {
server.post("temp-photo/upload/start", tempPhotoUploadStart, {
schema: tempPhotoUploadStartSchema,
});

server.post("temp-photo/upload/finish", tempPhotoUploadFinish, {
schema: tempPhotoUploadFinishSchema,
});
};
33 changes: 33 additions & 0 deletions src/v2/controllers/temp-photo/upload/finish.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Type } from "@sinclair/typebox";
import { FastifyRequestTypebox, Response } from "../../../../types/Server";
import { successJSON } from "../../internal/utils/response-json";
import { CloudStorageUploadService } from "../../../services/cloud-storage/upload";

export const tempPhotoUploadFinishSchema = {
body: Type.Object(
{
fileUUID: Type.String({
format: "uuid-v4",
}),
},
{
additionalProperties: false,
},
),
};

export const tempPhotoUploadFinish = async (
req: FastifyRequestTypebox<typeof tempPhotoUploadFinishSchema>,
): Promise<Response> => {
const cloudStorageUploadSVC = new CloudStorageUploadService(
req.ids,
req.DBTransaction,
req.userUUID,
);

await cloudStorageUploadSVC.tempPhotoFinish({
fileUUID: req.body.fileUUID,
});

return successJSON({});
};
46 changes: 46 additions & 0 deletions src/v2/controllers/temp-photo/upload/start.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Static, Type } from "@sinclair/typebox";
import { CloudStorage } from "../../../../constants/Config";
import { FastifyRequestTypebox, Response } from "../../../../types/Server";
import { successJSON } from "../../internal/utils/response-json";
import { CloudStorageUploadService } from "../../../services/cloud-storage/upload";
import { uploadStartReturnSchema } from "../../../services/cloud-storage/upload.schema";
import { useOnceService } from "../../../service-locator";

export const tempPhotoUploadStartSchema = {
body: Type.Object(
{
fileName: Type.String({
minLength: 3,
maxLength: 128,
format: "temp-photo-suffix",
}),
fileSize: Type.Integer({
maximum: CloudStorage.tempPhoto.singleFileSize,
minimum: 1,
}),
},
{
additionalProperties: false,
},
),
};

export const tempPhotoUploadStart = async (
req: FastifyRequestTypebox<typeof tempPhotoUploadStartSchema>,
): Promise<Response<Static<typeof uploadStartReturnSchema>>> => {
const complianceText = useOnceService("complianceText", req.ids);
await complianceText.assertTextNormal(req.body.fileName);

const cloudStorageUploadSVC = new CloudStorageUploadService(
req.ids,
req.DBTransaction,
req.userUUID,
);

const result = await cloudStorageUploadSVC.tempPhotoStart({
fileName: req.body.fileName,
fileSize: req.body.fileSize,
});

return successJSON(result);
};
94 changes: 94 additions & 0 deletions src/v2/services/cloud-storage/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import {
CloudStorageUploadStartReturn,
GetFileInfoByRedisReturn,
InsertFileInfo,
TempPhotoUploadFinishConfig,
TempPhotoUploadStartConfig,
TempPhotoUploadStartReturn,
} from "./upload.type";
import { FError } from "../../../error/ControllerError";
import { ErrorCode } from "../../../ErrorCode";
Expand Down Expand Up @@ -251,4 +254,95 @@ export class CloudStorageUploadService {
}
: {};
}

// Codes below are for temp photo uploading.
public static generateTempPhotoOSSFilePath = (fileName: string, fileUUID: string): string => {
const datePath = format("yyyy-MM/dd")(Date.now());
const prefix = CloudStorage.tempPhoto.prefixPath;
// e.g: PREFIX/temp-photo/2021-10/19/UUID/UUID.jpg
return `${prefix}/${datePath}/${fileUUID}/${fileUUID}${path.extname(fileName)}`;
};

public async assertTempPhotoConcurrentLimit(): Promise<void> {
const uploadingFiles = await RedisService.scan(
RedisKey.cloudStorageTempPhotoInfo(this.userUUID, "*"),
CloudStorage.tempPhoto.totalFiles + 1,
);

if (uploadingFiles.length >= CloudStorage.tempPhoto.totalFiles) {
throw new FError(ErrorCode.UploadConcurrentLimit);
}
}

public async getTempPhotoFileInfoByRedis(
fileUUID: string,
): Promise<TempPhotoUploadStartConfig> {
const fileInfo = await RedisService.hmget(
RedisKey.cloudStorageTempPhotoInfo(this.userUUID, fileUUID),
["fileName", "fileSize"],
);

const fileName = fileInfo[0];
const fileSize = Number(fileInfo[1] || undefined);

if (!fileName || Number.isNaN(fileSize)) {
this.logger.info("not found temp photo in redis", {
CloudStorageUpload: {
fileNameIsEmpty: !fileName,
fileSizeIsNaN: Number.isNaN(fileSize),
},
});
throw new FError(ErrorCode.FileNotFound);
}

return {
fileName,
fileSize,
};
}

public async tempPhotoStart(
config: TempPhotoUploadStartConfig,
): Promise<TempPhotoUploadStartReturn> {
const { fileName, fileSize } = config;
await this.assertTempPhotoConcurrentLimit();

const fileUUID = v4();
const oneDay = 60 * 60 * 24;
await RedisService.hmset(
RedisKey.cloudStorageTempPhotoInfo(this.userUUID, fileUUID),
{
fileName,
fileSize: String(fileSize),
},
oneDay,
);

const ossFilePath = CloudStorageUploadService.generateTempPhotoOSSFilePath(
fileName,
fileUUID,
);
const { policy, signature } = this.oss.policyTemplate(fileName, ossFilePath, fileSize);

return {
fileUUID,
ossFilePath,
ossDomain: this.oss.domain,
policy,
signature,
};
}

public async tempPhotoFinish(config: TempPhotoUploadFinishConfig): Promise<void> {
const { fileUUID } = config;

const { fileName } = await this.getTempPhotoFileInfoByRedis(fileUUID);

const ossFilePath = CloudStorageUploadService.generateTempPhotoOSSFilePath(
fileName,
fileUUID,
);

await this.oss.assertExists(ossFilePath);
}
}
17 changes: 17 additions & 0 deletions src/v2/services/cloud-storage/upload.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,20 @@ export interface GetFileInfoByRedisReturn {
targetDirectoryPath: string;
fileResourceType: FileResourceType;
}

export interface TempPhotoUploadStartConfig {
fileName: string;
fileSize: number;
}

export interface TempPhotoUploadStartReturn {
fileUUID: string;
ossDomain: string;
ossFilePath: string;
policy: string;
signature: string;
}

export interface TempPhotoUploadFinishConfig {
fileUUID: string;
}