Skip to content

Commit 6aabf80

Browse files
author
Rajat Saxena
committed
tested: ~380mb video upload; removed boilerplate code from tus feature; x-medialit-signature and x-medialit-apikey headers introduced
1 parent c1ea56a commit 6aabf80

File tree

15 files changed

+424
-563
lines changed

15 files changed

+424
-563
lines changed

apps/api/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
"aws-sdk": "^2.1692.0",
4444
"cors": "^2.8.5",
4545
"dotenv": "^16.4.7",
46-
"express": "^4.18.2",
46+
"express": "^5.1.0",
4747
"express-fileupload": "^1.3.1",
4848
"joi": "^17.6.0",
4949
"mongoose": "^8.0.1",

apps/api/src/apikey/middleware.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
import {
2-
BAD_REQUEST,
3-
SUBSCRIPTION_NOT_VALID,
4-
UNAUTHORISED,
5-
} from "../config/strings";
1+
import { BAD_REQUEST, UNAUTHORISED } from "../config/strings";
62
import { getApiKeyUsingKeyId } from "./queries";
73
import { getUser } from "../user/queries";
84
import { Apikey } from "@medialit/models";
@@ -12,7 +8,7 @@ export default async function apikey(
128
res: any,
139
next: (...args: any[]) => void,
1410
) {
15-
const reqKey = req.body.apikey;
11+
const reqKey = req.body.apikey || req.headers["x-medialit-apikey"];
1612

1713
if (!reqKey) {
1814
console.log("API key missing in request");
@@ -24,13 +20,6 @@ export default async function apikey(
2420
return res.status(401).json({ error: UNAUTHORISED });
2521
}
2622

27-
// const isSubscriptionValid = await validateSubscription(
28-
// apiKey!.userId.toString(),
29-
// );
30-
// if (!isSubscriptionValid) {
31-
// return res.status(403).json({ error: SUBSCRIPTION_NOT_VALID });
32-
// }
33-
3423
req.user = await getUser(apiKey!.userId.toString());
3524
req.apikey = apiKey.key;
3625

apps/api/src/config/constants.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,19 @@ export const tempFileDirForUploads = process.env.TEMP_FILE_DIR_FOR_UPLOADS;
66
export const maxFileUploadSizeSubscribed = process.env
77
.MAX_UPLOAD_SIZE_SUBSCRIBED
88
? +process.env.MAX_UPLOAD_SIZE_SUBSCRIBED
9-
: 2147483648;
9+
: 2147483648; // 2GB
1010
export const maxFileUploadSizeNotSubscribed = process.env
1111
.MAX_UPLOAD_SIZE_NOT_SUBSCRIBED
1212
? +process.env.MAX_UPLOAD_SIZE_NOT_SUBSCRIBED
13-
: 52428800;
13+
: 52428800; // 50MB
1414
export const maxStorageAllowedSubscribed = process.env
1515
.MAX_STORAGE_ALLOWED_SUBSCRIBED
1616
? +process.env.MAX_STORAGE_ALLOWED_SUBSCRIBED
17-
: 107374182400;
17+
: 107374182400; // 100GB
1818
export const maxStorageAllowedNotSubscribed = process.env
1919
.MAX_STORAGE_ALLOWED_NOT_SUBSCRIBED
2020
? +process.env.MAX_STORAGE_ALLOWED_NOT_SUBSCRIBED
21-
: 1073741824;
21+
: 1073741824; // 1GB
2222
export const PRESIGNED_URL_VALIDITY_MINUTES = 5;
2323
export const PRESIGNED_URL_LENGTH = 100;
2424
export const MEDIA_ID_LENGTH = 40;

apps/api/src/config/strings.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ export const FILE_IS_REQUIRED = "File is required";
66
export const FILE_SIZE_EXCEEDED = "File size exceeded";
77
export const NOT_FOUND = "Not found";
88
export const PRESIGNED_URL_INVALID = "The link is invalid";
9+
export const NOT_ENOUGH_STORAGE =
10+
"Not enough storage space in your account to upload this file";

apps/api/src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ app.use("/settings/media", mediaSettingsRoutes(passport));
3333
app.use("/media/presigned", presignedUrlRoutes);
3434
app.use("/media", tusRoutes);
3535
app.use("/media", mediaRoutes);
36-
// Mount TUS routes under /media so paths like /media/create/tus match correctly
3736

3837
const port = process.env.PORT || 80;
3938

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { maxStorageAllowedNotSubscribed } from "../config/constants";
22
import { maxStorageAllowedSubscribed } from "../config/constants";
3-
import { Constants, getSubscriptionStatus } from "@medialit/models";
3+
import { getSubscriptionStatus, User } from "@medialit/models";
44
import mediaQueries from "./queries";
5+
import { NOT_ENOUGH_STORAGE } from "../config/strings";
56

67
export default async function storageValidation(
78
req: any,
@@ -14,21 +15,25 @@ export default async function storageValidation(
1415
});
1516
}
1617

18+
if (!(await hasEnoughStorage((req.files.file as any).size, req.user))) {
19+
return res.status(403).json({
20+
error: NOT_ENOUGH_STORAGE,
21+
});
22+
}
23+
24+
next();
25+
}
26+
27+
export async function hasEnoughStorage(
28+
size: number,
29+
user: User,
30+
): Promise<boolean> {
1731
const totalSpaceOccupied = await mediaQueries.getTotalSpace({
18-
userId: (req as any).user.id,
32+
userId: user.id,
1933
});
20-
const maxStorageAllowed = getSubscriptionStatus(req.user)
34+
const maxStorageAllowed = getSubscriptionStatus(user)
2135
? maxStorageAllowedSubscribed
2236
: maxStorageAllowedNotSubscribed;
2337

24-
if (
25-
totalSpaceOccupied + (req.files?.file as any).size >
26-
maxStorageAllowed
27-
) {
28-
return res.status(400).json({
29-
error: "You do not have enough storage space in your account to upload this file",
30-
});
31-
}
32-
33-
next();
38+
return totalSpaceOccupied + size <= maxStorageAllowed;
3439
}

apps/api/src/presigning/middleware.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ import { PRESIGNED_URL_INVALID } from "../config/strings";
33
import * as preSignedUrlService from "./service";
44

55
export default async function presigned(
6-
req: Request & { user: any; apikey: string },
6+
req: Request & { user?: any; apikey?: string },
77
res: Response,
88
next: (...args: any[]) => void,
99
) {
10-
const { signature } = req.query;
10+
const signature =
11+
req.query.signature || req.headers["x-medialit-signature"];
1112

1213
const response = await preSignedUrlService.getUserAndGroupFromPresignedUrl(
1314
signature as string,

apps/api/src/presigning/service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,5 +90,5 @@ export async function generateSignedUrlForTus({
9090
);
9191
});
9292

93-
return `${protocol}://${host}/media/create/tus?signature=${presignedUrl?.signature}`;
93+
return `${protocol}://${host}/media/create/resumable?signature=${presignedUrl?.signature}`;
9494
}

apps/api/src/tus/finalize.ts

Lines changed: 59 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -24,44 +24,9 @@ import { createMedia } from "../media/queries";
2424
import getTags from "../media/utils/get-tags";
2525
import { getTusUpload, markTusUploadComplete } from "./queries";
2626
import * as presignedUrlService from "../presigning/service";
27-
28-
const generateAndUploadThumbnail = async ({
29-
workingDirectory,
30-
key,
31-
mimetype,
32-
originalFilePath,
33-
tags,
34-
}: {
35-
workingDirectory: string;
36-
key: string;
37-
mimetype: string;
38-
originalFilePath: string;
39-
tags: string;
40-
}): Promise<boolean> => {
41-
const thumbPath = `${workingDirectory}/thumb.webp`;
42-
43-
let isThumbGenerated = false;
44-
if (imagePatternForThumbnailGeneration.test(mimetype)) {
45-
await thumbnail.forImage(originalFilePath, thumbPath);
46-
isThumbGenerated = true;
47-
}
48-
if (videoPattern.test(mimetype)) {
49-
await thumbnail.forVideo(originalFilePath, thumbPath);
50-
isThumbGenerated = true;
51-
}
52-
53-
if (isThumbGenerated) {
54-
await putObject({
55-
Key: key,
56-
Body: createReadStream(thumbPath),
57-
ContentType: "image/webp",
58-
ACL: USE_CLOUDFRONT ? "private" : "public-read",
59-
Tagging: tags,
60-
});
61-
}
62-
63-
return isThumbGenerated;
64-
};
27+
import { getUser } from "../user/queries";
28+
import { hasEnoughStorage } from "../media/storage-middleware";
29+
import { NOT_ENOUGH_STORAGE } from "../config/strings";
6530

6631
export default async function finalizeUpload(uploadId: string) {
6732
logger.info({ uploadId }, "Finalizing tus upload");
@@ -79,6 +44,11 @@ export default async function finalizeUpload(uploadId: string) {
7944
const { userId, apikey, metadata, uploadLength, tempFilePath, signature } =
8045
tusUpload;
8146

47+
const user = await getUser(userId);
48+
if (!(await hasEnoughStorage(uploadLength, user!))) {
49+
throw new Error(NOT_ENOUGH_STORAGE);
50+
}
51+
8252
// Read the completed file from tus data store
8353
const tusFilePath = path.join(
8454
`${tempFileDirForUploads}/tus-uploads`,
@@ -95,41 +65,43 @@ export default async function finalizeUpload(uploadId: string) {
9565
const webpOutputQuality = mediaSettings?.webpOutputQuality || 0;
9666

9767
// Generate unique media ID
98-
const fileName = generateFileName(metadata.filename);
68+
const fileName = generateFileName(metadata.fileName);
9969
const temporaryFolderForWork = `${tempFileDirForUploads}/${fileName.name}`;
10070
if (!foldersExist([temporaryFolderForWork])) {
10171
createFolders([temporaryFolderForWork]);
10272
}
10373

104-
let fileExtension = path.extname(metadata.filename).replace(".", "");
105-
let mimeType = metadata.mimetype;
74+
let fileExtension = path.extname(metadata.fileName).replace(".", "");
75+
let mimeType = metadata.mimeType;
10676
if (useWebP && imagePattern.test(mimeType)) {
10777
fileExtension = "webp";
10878
mimeType = "image/webp";
10979
}
11080

11181
const mainFilePath = `${temporaryFolderForWork}/main.${fileExtension}`;
11282

113-
// Copy file from tus store to working directory
83+
console.log(mainFilePath, tusFilePath);
84+
85+
//Copy file from tus store to working directory
11486
const tusFileContent = readFileSync(tusFilePath);
11587
require("fs").writeFileSync(mainFilePath, tusFileContent);
11688

11789
// Apply WebP conversion if needed
118-
if (useWebP && imagePattern.test(metadata.mimetype)) {
90+
if (useWebP && imagePattern.test(metadata.mimeType)) {
11991
await imageUtils.convertToWebp(mainFilePath, webpOutputQuality);
12092
}
12193

12294
const uploadParams: UploadParams = {
12395
Key: generateKey({
12496
mediaId: fileName.name,
125-
access: metadata.access === "public" ? "public" : "private",
97+
access: metadata.accessControl === "public" ? "public" : "private",
12698
filename: `main.${fileExtension}`,
12799
}),
128100
Body: createReadStream(mainFilePath),
129101
ContentType: mimeType,
130102
ACL: USE_CLOUDFRONT
131103
? "private"
132-
: metadata.access === "public"
104+
: metadata.accessControl === "public"
133105
? "public-read"
134106
: "private",
135107
};
@@ -142,7 +114,7 @@ export default async function finalizeUpload(uploadId: string) {
142114
try {
143115
isThumbGenerated = await generateAndUploadThumbnail({
144116
workingDirectory: temporaryFolderForWork,
145-
mimetype: metadata.mimetype,
117+
mimetype: metadata.mimeType,
146118
originalFilePath: mainFilePath,
147119
key: generateKey({
148120
mediaId: fileName.name,
@@ -162,12 +134,13 @@ export default async function finalizeUpload(uploadId: string) {
162134
mediaId: fileName.name,
163135
userId: new mongoose.Types.ObjectId(userId),
164136
apikey,
165-
originalFileName: metadata.filename,
137+
originalFileName: metadata.fileName,
166138
mimeType,
167139
size: uploadLength,
168140
thumbnailGenerated: isThumbGenerated,
169141
caption: metadata.caption,
170-
accessControl: metadata.access === "public" ? "public-read" : "private",
142+
accessControl:
143+
metadata.accessControl === "public" ? "public-read" : "private",
171144
group: metadata.group,
172145
};
173146
const media = await createMedia(mediaObject);
@@ -201,3 +174,41 @@ export default async function finalizeUpload(uploadId: string) {
201174

202175
return media.mediaId;
203176
}
177+
178+
const generateAndUploadThumbnail = async ({
179+
workingDirectory,
180+
key,
181+
mimetype,
182+
originalFilePath,
183+
tags,
184+
}: {
185+
workingDirectory: string;
186+
key: string;
187+
mimetype: string;
188+
originalFilePath: string;
189+
tags: string;
190+
}): Promise<boolean> => {
191+
const thumbPath = `${workingDirectory}/thumb.webp`;
192+
193+
let isThumbGenerated = false;
194+
if (imagePatternForThumbnailGeneration.test(mimetype)) {
195+
await thumbnail.forImage(originalFilePath, thumbPath);
196+
isThumbGenerated = true;
197+
}
198+
if (videoPattern.test(mimetype)) {
199+
await thumbnail.forVideo(originalFilePath, thumbPath);
200+
isThumbGenerated = true;
201+
}
202+
203+
if (isThumbGenerated) {
204+
await putObject({
205+
Key: key,
206+
Body: createReadStream(thumbPath),
207+
ContentType: "image/webp",
208+
ACL: USE_CLOUDFRONT ? "private" : "public-read",
209+
Tagging: tags,
210+
});
211+
}
212+
213+
return isThumbGenerated;
214+
};

apps/api/src/tus/queries.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,6 @@ export async function createTusUpload(
2525
isComplete: false,
2626
expiresAt,
2727
};
28-
console.log(
29-
"Creating TusUpload with ID:",
30-
data.uploadId,
31-
data,
32-
tusUploadData,
33-
);
3428
const tusUpload = await TusUploadModel.create(tusUploadData);
3529

3630
return tusUpload;

0 commit comments

Comments
 (0)