feat: add S3 object copying and enhanced media URL rewriting for Ghost posts
This commit is contained in:
parent
5a00636063
commit
cd799a2024
@ -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(/&/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: [
|
||||
{
|
||||
|
||||
@ -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 };
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user