feat: add S3 object copying and enhanced media URL rewriting for Ghost posts

This commit is contained in:
Ender 2025-10-24 14:03:46 +02:00
parent 5a00636063
commit cd799a2024
2 changed files with 63 additions and 7 deletions

View File

@ -1,7 +1,7 @@
import express from 'express';
import jwt from 'jsonwebtoken';
import { fetch } from 'undici';
import { getPresignedUrl, getPublicUrlForKey } from './storage/s3';
import { getPresignedUrl, getPublicUrlForKey, copyObject } from './storage/s3';
const router = express.Router();
@ -58,6 +58,8 @@ router.post('/post', async (req, res) => {
return res.status(400).json({ error: 'title and html are required' });
}
const replacePairs: Array<{ from: string; to: string }> = [];
async function rewriteInternalMediaUrl(src: string, bucketFallback: string): Promise<string> {
try {
if (!src) return src;
@ -66,14 +68,31 @@ router.post('/post', async (req, res) => {
if (!isInternal) return src;
// parse query
const base = 'http://local';
const u = new URL(src, base);
const decoded = src.replace(/&amp;/g, '&');
const u = new URL(decoded, base);
const key = u.searchParams.get('key') || '';
const bucket = u.searchParams.get('bucket') || bucketFallback;
if (!key || !bucket) return src;
// Prefer a public base if provided, else presigned URL
const publicUrl = getPublicUrlForKey(key);
if (publicUrl) return publicUrl;
return await getPresignedUrl({ bucket, key, expiresInSeconds: 60 * 60 * 24 * 7 });
// Prefer a public base if provided (and optionally copy), else presigned URL
let out: string;
const publicBase = process.env.PUBLIC_MEDIA_BASE_URL;
if (publicBase) {
const destBucket = process.env.PUBLIC_MEDIA_BUCKET || '';
if (destBucket) {
try {
await copyObject({ srcBucket: bucket, srcKey: key, destBucket, destKey: key });
} catch (e) {
console.warn('[Ghost] Copy to public bucket failed, will still use public base URL', e);
}
}
out = getPublicUrlForKey(key) as string;
} else {
out = await getPresignedUrl({ bucket, key, expiresInSeconds: 60 * 60 * 24 * 7 });
}
if (out !== src) {
replacePairs.push({ from: src, to: out });
}
return out;
} catch {
return src;
}
@ -98,10 +117,33 @@ router.post('/post', async (req, res) => {
return out;
}
// Inspect incoming HTML for <img> srcs before rewriting
try {
const re = /<img\b[^>]*src=["']([^"']+)["'][^>]*>/gi;
const found = [...(html || '').matchAll(re)].map(m => m[1]);
console.log('[Ghost] Incoming feature_image (raw):', feature_image || '(none)');
console.log('[Ghost] Incoming <img> count:', found.length);
console.log('[Ghost] Incoming <img> srcs (first 5):', found.slice(0, 5));
console.log('[Ghost] HTML contains /api/media/obj:', (html || '').includes('/api/media/obj'));
} catch {}
const bucketDefault = process.env.S3_BUCKET || '';
const rewrittenFeatureImage = await rewriteInternalMediaUrl(feature_image || '', bucketDefault);
const rewrittenHtml = await rewriteHtmlImagesToPublic(html, bucketDefault);
// Debug logs to trace rewriting
try {
const base = process.env.PUBLIC_MEDIA_BASE_URL || '(none)';
console.log('[Ghost] PUBLIC_MEDIA_BASE_URL:', base);
console.log('[Ghost] Replacements count:', replacePairs.length);
if (rewrittenFeatureImage && rewrittenFeatureImage !== (feature_image || '')) {
console.log('[Ghost] feature_image rewritten');
}
if (replacePairs.length > 0) {
console.log('[Ghost] Sample replacements:', replacePairs.slice(0, 3));
}
} catch {}
const payload = {
posts: [
{

View File

@ -1,4 +1,4 @@
import { S3Client, PutObjectCommand, GetObjectCommand, ListObjectsV2Command, DeleteObjectCommand } from '@aws-sdk/client-s3';
import { S3Client, PutObjectCommand, GetObjectCommand, ListObjectsV2Command, DeleteObjectCommand, CopyObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
export function getS3Client() {
@ -94,3 +94,17 @@ export async function downloadObject(params: { bucket: string; key: string }): P
console.log('[S3] Download done', { bucket: params.bucket, key: params.key, bytes: buffer.length, contentType });
return { buffer, contentType };
}
export async function copyObject(params: { srcBucket: string; srcKey: string; destBucket: string; destKey: string }) {
const s3 = getS3Client();
const copySource = `/${params.srcBucket}/${encodeURIComponent(params.srcKey).replace(/%2F/g, '/')}`;
console.log('[S3] Copy start', { from: { bucket: params.srcBucket, key: params.srcKey }, to: { bucket: params.destBucket, key: params.destKey } });
await s3.send(new CopyObjectCommand({
Bucket: params.destBucket,
Key: params.destKey,
CopySource: copySource,
MetadataDirective: 'COPY',
}));
console.log('[S3] Copy done', { bucket: params.destBucket, key: params.destKey });
return { bucket: params.destBucket, key: params.destKey };
}