mirror of
https://github.com/hex248/sprint.git
synced 2026-02-08 02:33:01 +00:00
gif avatar support
all resizing is done server-side with sharp
This commit is contained in:
@@ -32,6 +32,7 @@
|
||||
"dotenv": "^17.2.3",
|
||||
"drizzle-orm": "^0.45.0",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"pg": "^8.16.3"
|
||||
"pg": "^8.16.3",
|
||||
"sharp": "^0.34.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { BunRequest } from "bun";
|
||||
import sharp from "sharp";
|
||||
import { s3Client, s3Endpoint, s3PublicUrl } from "../../s3";
|
||||
|
||||
const MAX_FILE_SIZE = 5 * 1024 * 1024;
|
||||
const ALLOWED_TYPES = ["image/png", "image/jpeg", "image/webp", "image/gif"];
|
||||
const ALLOWED_EXTENSIONS = ["png", "jpg", "jpeg", "webp", "gif"];
|
||||
const TARGET_SIZE = 256;
|
||||
|
||||
export default async function uploadAvatar(req: BunRequest) {
|
||||
if (req.method !== "POST") {
|
||||
@@ -28,19 +29,38 @@ export default async function uploadAvatar(req: BunRequest) {
|
||||
});
|
||||
}
|
||||
|
||||
const fileExtension = file.name.split(".").pop()?.toLowerCase();
|
||||
if (!fileExtension || !ALLOWED_EXTENSIONS.includes(fileExtension)) {
|
||||
return new Response("invalid file extension", { status: 400 });
|
||||
const isGIF = file.type === "image/gif";
|
||||
const outputExtension = isGIF ? "gif" : "png";
|
||||
const outputMimeType = isGIF ? "image/gif" : "image/png";
|
||||
|
||||
let resizedBuffer: Buffer;
|
||||
try {
|
||||
const inputBuffer = Buffer.from(await file.arrayBuffer());
|
||||
|
||||
if (isGIF) {
|
||||
resizedBuffer = await sharp(inputBuffer, { animated: true })
|
||||
.resize(TARGET_SIZE, TARGET_SIZE, { fit: "cover" })
|
||||
.gif()
|
||||
.toBuffer();
|
||||
} else {
|
||||
resizedBuffer = await sharp(inputBuffer)
|
||||
.resize(TARGET_SIZE, TARGET_SIZE, { fit: "cover" })
|
||||
.png()
|
||||
.toBuffer();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("failed to resize image:", error);
|
||||
return new Response("failed to process image", { status: 500 });
|
||||
}
|
||||
|
||||
const uuid = randomUUID();
|
||||
const key = `avatars/${uuid}.${fileExtension}`;
|
||||
const key = `avatars/${uuid}.${outputExtension}`;
|
||||
const publicUrlBase = s3PublicUrl || s3Endpoint;
|
||||
const publicUrl = `${publicUrlBase}/${key}`;
|
||||
|
||||
try {
|
||||
const s3File = s3Client.file(key);
|
||||
await s3File.write(file, { type: file.type });
|
||||
await s3File.write(resizedBuffer, { type: outputMimeType });
|
||||
} catch (error) {
|
||||
console.error("failed to upload to S3:", error);
|
||||
return new Response("failed to upload image", { status: 500 });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getAuthHeaders, getServerURL, resizeImageToSquare } from "@/lib/utils";
|
||||
import { getAuthHeaders, getServerURL } from "@/lib/utils";
|
||||
import type { ServerQueryInput } from "..";
|
||||
|
||||
export async function uploadAvatar({
|
||||
@@ -21,17 +21,8 @@ export async function uploadAvatar({
|
||||
return;
|
||||
}
|
||||
|
||||
let resizedFile: File;
|
||||
try {
|
||||
const blob = await resizeImageToSquare(file, 256);
|
||||
resizedFile = new File([blob], "avatar.png", { type: "image/png" });
|
||||
} catch (_error) {
|
||||
onError?.("Failed to resize image");
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("file", resizedFile);
|
||||
formData.append("file", file);
|
||||
|
||||
const res = await fetch(`${getServerURL()}/user/upload-avatar`, {
|
||||
method: "POST",
|
||||
|
||||
@@ -31,45 +31,3 @@ export function getServerURL() {
|
||||
}
|
||||
return serverURL;
|
||||
}
|
||||
|
||||
export async function resizeImageToSquare(file: File, targetSize: number = 256): Promise<Blob> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
if (!ctx) {
|
||||
reject(new Error("Could not get canvas context"));
|
||||
return;
|
||||
}
|
||||
|
||||
img.onload = () => {
|
||||
canvas.width = targetSize;
|
||||
canvas.height = targetSize;
|
||||
|
||||
const minDimension = Math.min(img.width, img.height);
|
||||
const startX = (img.width - minDimension) / 2;
|
||||
const startY = (img.height - minDimension) / 2;
|
||||
|
||||
ctx.drawImage(img, startX, startY, minDimension, minDimension, 0, 0, targetSize, targetSize);
|
||||
|
||||
canvas.toBlob(
|
||||
(blob) => {
|
||||
if (blob) {
|
||||
resolve(blob);
|
||||
} else {
|
||||
reject(new Error("Failed to create blob"));
|
||||
}
|
||||
},
|
||||
"image/png",
|
||||
0.9,
|
||||
);
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
reject(new Error("Failed to load image"));
|
||||
};
|
||||
|
||||
img.src = URL.createObjectURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user