109 lines
3.4 KiB
TypeScript
109 lines
3.4 KiB
TypeScript
import { AwsClient } from 'https://esm.sh/aws4fetch@1.0.17'
|
|
|
|
const CUSTOM_MEDIA_DOMAIN = (Deno.env.get("CUSTOM_MEDIA_DOMAIN") ?? "https://img.sojorn.net").trim();
|
|
const CUSTOM_VIDEO_DOMAIN = (Deno.env.get("CUSTOM_VIDEO_DOMAIN") ?? "https://quips.sojorn.net").trim();
|
|
|
|
const DEFAULT_BUCKET_NAME = "sojorn-media";
|
|
const RESOLVED_BUCKET = (Deno.env.get("R2_BUCKET_NAME") ?? DEFAULT_BUCKET_NAME).trim();
|
|
|
|
function normalizeKey(key: string): string {
|
|
let normalized = key.replace(/^\/+/, "");
|
|
if (RESOLVED_BUCKET && normalized.startsWith(`${RESOLVED_BUCKET}/`)) {
|
|
normalized = normalized.slice(RESOLVED_BUCKET.length + 1);
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
function extractObjectKey(input: string): string {
|
|
const trimmed = input.trim();
|
|
if (!trimmed) {
|
|
throw new Error("Missing file key");
|
|
}
|
|
|
|
try {
|
|
const url = new URL(trimmed);
|
|
const key = decodeURIComponent(url.pathname);
|
|
return normalizeKey(key);
|
|
} catch {
|
|
return normalizeKey(trimmed);
|
|
}
|
|
}
|
|
|
|
export function transformLegacyMediaUrl(input: string): string | null {
|
|
const trimmed = input.trim();
|
|
if (!trimmed) return null;
|
|
|
|
try {
|
|
const url = new URL(trimmed);
|
|
|
|
// Handle legacy media.sojorn.net URLs
|
|
if (url.hostname === 'media.sojorn.net') {
|
|
const key = decodeURIComponent(url.pathname);
|
|
return key;
|
|
}
|
|
|
|
return null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Deprecated: no-op signer retained for compatibility
|
|
export async function signR2Url(fileKey: string, expiresIn: number = 3600): Promise<string> {
|
|
return await trySignR2Url(fileKey, undefined, expiresIn) ?? fileKey;
|
|
}
|
|
|
|
export async function trySignR2Url(fileKey: string, bucket?: string, expiresIn: number = 3600): Promise<string | null> {
|
|
try {
|
|
const key = normalizeKey(extractObjectKey(fileKey));
|
|
|
|
// Check if we have credentials to sign. If not, fallback to public URL.
|
|
const ACCOUNT_ID = Deno.env.get('R2_ACCOUNT_ID');
|
|
const ACCESS_KEY = Deno.env.get('R2_ACCESS_KEY');
|
|
const SECRET_KEY = Deno.env.get('R2_SECRET_KEY');
|
|
|
|
const isVideo = key.toLowerCase().endsWith('.mp4') ||
|
|
key.toLowerCase().endsWith('.mov') ||
|
|
key.toLowerCase().endsWith('.webm') ||
|
|
bucket === 'sojorn-videos';
|
|
|
|
if (!ACCOUNT_ID || !ACCESS_KEY || !SECRET_KEY) {
|
|
console.warn("Missing R2 credentials for signing. Falling back to public domain.");
|
|
const domain = isVideo ? CUSTOM_VIDEO_DOMAIN : CUSTOM_MEDIA_DOMAIN;
|
|
if (domain && domain.startsWith("http")) {
|
|
return `${domain.replace(/\/+$/, "")}/${key}`;
|
|
}
|
|
return fileKey;
|
|
}
|
|
|
|
const r2 = new AwsClient({
|
|
accessKeyId: ACCESS_KEY,
|
|
secretAccessKey: SECRET_KEY,
|
|
region: 'auto',
|
|
service: 's3',
|
|
});
|
|
|
|
const targetBucket = bucket || (isVideo ? 'sojorn-videos' : 'sojorn-media');
|
|
|
|
// We sign against the actual R2 endpoint to ensure auth works,
|
|
// but the SignedMediaImage can handle redirect/proxying if needed.
|
|
const url = new URL(`https://${ACCOUNT_ID}.r2.cloudflarestorage.com/${targetBucket}/${key}`);
|
|
|
|
// Add expiration
|
|
url.searchParams.set('X-Amz-Expires', expiresIn.toString());
|
|
|
|
const signedRequest = await r2.sign(url, {
|
|
method: "GET",
|
|
aws: { signQuery: true, allHeaders: false },
|
|
});
|
|
|
|
return signedRequest.url;
|
|
} catch (error) {
|
|
console.error("R2 signing failed", {
|
|
fileKey,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
});
|
|
return null;
|
|
}
|
|
}
|