mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
assets: go 15GB limit
Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
+147
-3
@@ -22,7 +22,10 @@
|
||||
// whenever S3_ENDPOINT isn't reachable from this machine —
|
||||
// see resolvePublicHost() below)
|
||||
|
||||
const { S3Client, DeleteObjectCommand, GetObjectCommand, HeadBucketCommand } = require("@aws-sdk/client-s3");
|
||||
const {
|
||||
S3Client, DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, HeadBucketCommand, PutObjectCommand,
|
||||
CreateMultipartUploadCommand, UploadPartCommand, CompleteMultipartUploadCommand, AbortMultipartUploadCommand,
|
||||
} = require("@aws-sdk/client-s3");
|
||||
const { Upload } = require("@aws-sdk/lib-storage");
|
||||
const { getSignedUrl } = require("@aws-sdk/s3-request-presigner");
|
||||
const crypto = require("crypto");
|
||||
@@ -44,7 +47,14 @@ const credentials = {
|
||||
// WHEN_REQUIRED skips validation unless the operation mandates it (GetObject
|
||||
// never does) — safe here since Garage isn't computing/verifying per AWS spec
|
||||
// anyway.
|
||||
const CHECKSUM_CONFIG = { responseChecksumValidation: "WHEN_REQUIRED" };
|
||||
//
|
||||
// requestChecksumCalculation: "WHEN_REQUIRED" — same fix, request side. SDK
|
||||
// v3's default "WHEN_SUPPORTED" makes presignUpload()'s PutObjectCommand carry
|
||||
// a signed x-amz-checksum-crc32 computed over an empty/phantom body (there's
|
||||
// no real body yet at presign time), so Garage rejects the browser's actual
|
||||
// PUT once real bytes arrive with "InvalidDigest ... expected Crc32([0,0,0,0])".
|
||||
// PutObject doesn't require a checksum, so WHEN_REQUIRED just omits it.
|
||||
const CHECKSUM_CONFIG = { responseChecksumValidation: "WHEN_REQUIRED", requestChecksumCalculation: "WHEN_REQUIRED" };
|
||||
|
||||
// Internal client — uploads, deletes, direct streams from the server itself.
|
||||
// Always targets S3_ENDPOINT. When this backend is co-located with Garage,
|
||||
@@ -271,4 +281,138 @@ async function ping() {
|
||||
await client.send(new HeadBucketCommand({ Bucket: DEFAULT_BUCKET }));
|
||||
}
|
||||
|
||||
module.exports = { uploadFile, deleteFile, getSignedDownloadUrl, getPublicUrl, getObjectStream, ping };
|
||||
// S3's own hard ceiling for a single (non-multipart) PUT — files above this
|
||||
// need real multipart upload instead (see presignUpload() below).
|
||||
const MULTIPART_THRESHOLD = 5 * 1024 ** 3; // 5GB
|
||||
|
||||
// Fixed part size for multipart uploads — comfortably above S3's 5MB-per-part
|
||||
// minimum, and keeps the part count reasonable even at the largest files this
|
||||
// app expects (15GB / 50MB = 300 parts, well under S3's 10,000-part ceiling).
|
||||
// No adaptive sizing needed at this scale.
|
||||
const PART_SIZE = 50 * 1024 ** 2; // 50MB
|
||||
|
||||
// ─── presignUpload ────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Presigned upload URL(s) for browser-direct uploads — the browser PUTs the
|
||||
// file's bytes straight to storage; this backend never buffers them. Signed
|
||||
// against the same public-facing host as getPublicUrl() (browsers can't
|
||||
// reach S3_ENDPOINT when that's an internal-only address), and deliberately
|
||||
// does NOT pin Content-Type on any signed command — the browser's actual
|
||||
// Content-Type header (from the File object) would have to match whatever
|
||||
// was signed exactly, and leaving it unsigned avoids that fragility.
|
||||
//
|
||||
// input: originalname, ownerType — same as uploadFile(), used to build the
|
||||
// same {prefix}/{uuid}.{ext} key convention.
|
||||
// size — total file size in bytes, decides single-PUT vs multipart.
|
||||
// output: { key, uploadUrl } (size <= 5GB)
|
||||
// or { key, multipart: true, uploadId, partSize, parts } (size > 5GB)
|
||||
// parts: [{ partNumber, uploadUrl }, ...], one presigned UploadPart
|
||||
// URL per part, all generated upfront in this one call.
|
||||
//
|
||||
async function presignUpload(originalname, ownerType = "image", size = 0) {
|
||||
const key = buildKey(originalname, ownerType);
|
||||
const client = getPublicClient();
|
||||
|
||||
if (size <= MULTIPART_THRESHOLD) {
|
||||
// Generous expiry — even a file near the 5GB ceiling can take a long
|
||||
// time to PUT on a slow connection, and the signature must still be
|
||||
// valid when the browser actually gets around to sending it.
|
||||
const uploadUrl = await getSignedUrl(
|
||||
client,
|
||||
new PutObjectCommand({ Bucket: DEFAULT_BUCKET, Key: key }),
|
||||
{ expiresIn: 4 * 60 * 60 } // 4h
|
||||
);
|
||||
return { key, uploadUrl };
|
||||
}
|
||||
|
||||
// Above the single-PUT ceiling — the backend initiates the multipart
|
||||
// session itself with real credentials (a lightweight control-plane call,
|
||||
// no file bytes involved); only the individual part uploads, which do
|
||||
// carry real bytes, get presigned for the browser.
|
||||
const { UploadId: uploadId } = await s3.send(new CreateMultipartUploadCommand({
|
||||
Bucket: DEFAULT_BUCKET,
|
||||
Key: key,
|
||||
}));
|
||||
|
||||
const partCount = Math.ceil(size / PART_SIZE);
|
||||
const parts = [];
|
||||
for (let partNumber = 1; partNumber <= partCount; partNumber++) {
|
||||
const uploadUrl = await getSignedUrl(
|
||||
client,
|
||||
new UploadPartCommand({ Bucket: DEFAULT_BUCKET, Key: key, UploadId: uploadId, PartNumber: partNumber }),
|
||||
{ expiresIn: 24 * 60 * 60 } // 24h — a 15GB upload can genuinely take a while on a slow connection
|
||||
);
|
||||
parts.push({ partNumber, uploadUrl });
|
||||
}
|
||||
|
||||
return { key, multipart: true, uploadId, partSize: PART_SIZE, parts };
|
||||
}
|
||||
|
||||
// ─── completeMultipartUpload ──────────────────────────────────────────────────
|
||||
//
|
||||
// Finishes a multipart upload once the browser has PUT every part directly
|
||||
// (see presignUpload() above). Parts must carry the ETag each part's own PUT
|
||||
// response returned — order doesn't matter here, they're sorted by
|
||||
// PartNumber before submitting. Real credentials, not presigned: this is a
|
||||
// small control-plane call, no file bytes involved.
|
||||
//
|
||||
// input: key, uploadId, parts — [{ partNumber, etag }, ...]
|
||||
//
|
||||
async function completeMultipartUpload(key, uploadId, parts) {
|
||||
await s3.send(new CompleteMultipartUploadCommand({
|
||||
Bucket: DEFAULT_BUCKET,
|
||||
Key: key,
|
||||
UploadId: uploadId,
|
||||
MultipartUpload: {
|
||||
Parts: parts
|
||||
.slice()
|
||||
.sort((a, b) => a.partNumber - b.partNumber)
|
||||
.map(({ partNumber, etag }) => ({ PartNumber: partNumber, ETag: etag })),
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
// ─── abortMultipartUpload ──────────────────────────────────────────────────────
|
||||
//
|
||||
// Cancels an in-progress multipart upload (a part permanently failed after
|
||||
// retries, or the browser gave up) so it doesn't linger as orphaned storage
|
||||
// forever. Real credentials, called server-side — this only ever happens on
|
||||
// failure, no need for browser-direct access to it.
|
||||
//
|
||||
async function abortMultipartUpload(key, uploadId) {
|
||||
await s3.send(new AbortMultipartUploadCommand({
|
||||
Bucket: DEFAULT_BUCKET,
|
||||
Key: key,
|
||||
UploadId: uploadId,
|
||||
}));
|
||||
}
|
||||
|
||||
// ─── getFileMetadata ──────────────────────────────────────────────────────────
|
||||
//
|
||||
// Reads back what the browser actually uploaded via presignUpload() — used by
|
||||
// the finalize step in place of multer's req.file (size, mimetype), since no
|
||||
// buffer ever passes through this backend to read those from directly. ETag
|
||||
// doubles as the object's checksum: for a single (non-multipart) unencrypted
|
||||
// PutObjectCommand, S3-compatible ETag is exactly the MD5 of the body.
|
||||
//
|
||||
// input: key
|
||||
// output: { size, mimetype, checksum } — checksum is the ETag with its
|
||||
// surrounding quotes stripped, or null if the object has no ETag.
|
||||
//
|
||||
async function getFileMetadata(key) {
|
||||
const result = await s3.send(new HeadObjectCommand({
|
||||
Bucket: DEFAULT_BUCKET,
|
||||
Key: key,
|
||||
}));
|
||||
return {
|
||||
size: result.ContentLength ?? null,
|
||||
mimetype: result.ContentType ?? null,
|
||||
checksum: result.ETag ? result.ETag.replace(/"/g, "") : null,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
uploadFile, deleteFile, getSignedDownloadUrl, getPublicUrl, getObjectStream,
|
||||
presignUpload, completeMultipartUpload, abortMultipartUpload,
|
||||
getFileMetadata, buildPublicUrl, ping,
|
||||
};
|
||||
Reference in New Issue
Block a user