83 lines
2.3 KiB
TypeScript
83 lines
2.3 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import { mkdir } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
|
|
const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp"];
|
|
const MAX_SIZE = 5 * 1024 * 1024; // 5MB
|
|
const FETCH_TIMEOUT = 10_000; // 10 seconds
|
|
|
|
interface FetchImageResult {
|
|
filename: string;
|
|
sourceUrl: string;
|
|
}
|
|
|
|
export async function fetchImageFromUrl(
|
|
url: string,
|
|
uploadsDir = "uploads",
|
|
): Promise<FetchImageResult> {
|
|
// Validate URL format
|
|
let parsedUrl: URL;
|
|
try {
|
|
parsedUrl = new URL(url);
|
|
} catch {
|
|
throw new Error("Invalid URL format");
|
|
}
|
|
|
|
if (!["http:", "https:"].includes(parsedUrl.protocol)) {
|
|
throw new Error("URL must use HTTP or HTTPS");
|
|
}
|
|
|
|
// Fetch with timeout
|
|
const controller = new AbortController();
|
|
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
|
|
|
|
let response: Response;
|
|
try {
|
|
response = await fetch(url, { signal: controller.signal });
|
|
} catch (err) {
|
|
if (err instanceof DOMException && err.name === "AbortError") {
|
|
throw new Error("Request timed out");
|
|
}
|
|
throw new Error(`Failed to fetch image: ${(err as Error).message}`);
|
|
} finally {
|
|
clearTimeout(timeout);
|
|
}
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}: Failed to fetch image`);
|
|
}
|
|
|
|
// Validate content type
|
|
const contentType = response.headers
|
|
.get("content-type")
|
|
?.split(";")[0]
|
|
.trim();
|
|
if (!contentType || !ALLOWED_TYPES.includes(contentType)) {
|
|
throw new Error(
|
|
`Invalid content type: ${contentType ?? "unknown"}. Accepted: jpeg, png, webp`,
|
|
);
|
|
}
|
|
|
|
// Check content length if available
|
|
const contentLength = response.headers.get("content-length");
|
|
if (contentLength && Number.parseInt(contentLength, 10) > MAX_SIZE) {
|
|
throw new Error("File too large. Maximum size is 5MB");
|
|
}
|
|
|
|
// Read body and check actual size
|
|
const buffer = await response.arrayBuffer();
|
|
if (buffer.byteLength > MAX_SIZE) {
|
|
throw new Error("File too large. Maximum size is 5MB");
|
|
}
|
|
|
|
// Determine extension
|
|
const ext = contentType === "image/jpeg" ? "jpg" : contentType.split("/")[1];
|
|
const filename = `${Date.now()}-${randomUUID()}.${ext}`;
|
|
|
|
// Ensure directory exists and write
|
|
await mkdir(uploadsDir, { recursive: true });
|
|
await Bun.write(join(uploadsDir, filename), buffer);
|
|
|
|
return { filename, sourceUrl: url };
|
|
}
|