From 7f58c9c847f44f0dfec87d492e0d9711cae8cd15 Mon Sep 17 00:00:00 2001 From: cialloo Date: Sun, 26 Oct 2025 07:56:19 +0800 Subject: [PATCH] feat: add S3 configuration and utility functions for blog image handling --- .env.example | 3 +++ .github/workflows/ci.yml | 3 +++ .gitignore | 5 ++++ Dockerfile | 8 +++++++ script/ci.sh | 20 +++++++++++++++- src/blog/api.ts | 24 ++++--------------- src/blog/nodes/ImageComponent.tsx | 12 +++++++++- src/blog/nodes/ImageNode.tsx | 9 ++++--- src/blog/s3Config.ts | 39 +++++++++++++++++++++++++++++++ src/blog/types.ts | 5 ---- 10 files changed, 98 insertions(+), 30 deletions(-) create mode 100644 .env.example create mode 100644 src/blog/s3Config.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..93ec36c --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +# S3 Configuration for Blog Images +VITE_S3_ENDPOINT_BLOG=https://s3.example.com +VITE_S3_BUCKET_BLOG=my-blog-bucket diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c495f7..021fed8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,9 @@ env: CONTAINER_REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} CONTAINER_IMAGE_NAME: ${{ secrets.CONTAINER_IMAGE_NAME }} CONTAINER_IMAGE_TAG: ${{ secrets.CONTAINER_IMAGE_TAG }} + # S3 Configuration for Blog + VITE_S3_ENDPOINT_BLOG: ${{ secrets.VITE_S3_ENDPOINT_BLOG }} + VITE_S3_BUCKET_BLOG: ${{ secrets.VITE_S3_BUCKET_BLOG }} jobs: build-and-push: diff --git a/.gitignore b/.gitignore index fa174cb..d2ed52c 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,11 @@ dist dist-ssr *.local +# Environment variables +.env +.env.local +.env.*.local + # Editor directories and files .vscode/* !.vscode/extensions.json diff --git a/Dockerfile b/Dockerfile index e35bdf2..6240b14 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,6 +13,14 @@ RUN npm ci # Copy project files COPY . . +# Build arguments for environment variables +ARG VITE_S3_ENDPOINT_BLOG +ARG VITE_S3_BUCKET_BLOG + +# Set environment variables for Vite build +ENV VITE_S3_ENDPOINT_BLOG=${VITE_S3_ENDPOINT_BLOG} +ENV VITE_S3_BUCKET_BLOG=${VITE_S3_BUCKET_BLOG} + # Build the application RUN npm run build diff --git a/script/ci.sh b/script/ci.sh index 33ae414..0f23f88 100644 --- a/script/ci.sh +++ b/script/ci.sh @@ -13,6 +13,10 @@ CONTAINER_REGISTRY_PASSWORD="${CONTAINER_REGISTRY_PASSWORD:-password}" CONTAINER_IMAGE_NAME="${CONTAINER_IMAGE_NAME:-image-name}" CONTAINER_IMAGE_TAG="${CONTAINER_IMAGE_TAG:-latest}" +# Application Environment Variables +VITE_S3_ENDPOINT_BLOG="${VITE_S3_ENDPOINT_BLOG:-}" +VITE_S3_BUCKET_BLOG="${VITE_S3_BUCKET_BLOG:-}" + # ============================================================================= # Functions # ============================================================================= @@ -33,6 +37,8 @@ print_help() { echo " CONTAINER_REGISTRY_PASSWORD Registry password (default: password)" echo " CONTAINER_IMAGE_NAME Image name (default: image-name)" echo " CONTAINER_IMAGE_TAG Image tag (default: latest)" + echo " VITE_S3_ENDPOINT_BLOG S3 endpoint for blog images" + echo " VITE_S3_BUCKET_BLOG S3 bucket for blog images" echo "" echo "Examples:" echo " $0 build" @@ -51,8 +57,20 @@ build_image() { echo "Image: ${FULL_IMAGE_NAME}" echo "" + # Prepare build args for S3 configuration + BUILD_ARGS="" + if [ -n "${VITE_S3_ENDPOINT_BLOG}" ]; then + BUILD_ARGS="${BUILD_ARGS} --build-arg VITE_S3_ENDPOINT_BLOG=${VITE_S3_ENDPOINT_BLOG}" + echo "S3 Endpoint (Blog): ${VITE_S3_ENDPOINT_BLOG}" + fi + if [ -n "${VITE_S3_BUCKET_BLOG}" ]; then + BUILD_ARGS="${BUILD_ARGS} --build-arg VITE_S3_BUCKET_BLOG=${VITE_S3_BUCKET_BLOG}" + echo "S3 Bucket (Blog): ${VITE_S3_BUCKET_BLOG}" + fi + echo "" + # Build the Docker image using Dockerfile in current directory - docker build -t "${FULL_IMAGE_NAME}" . + docker build ${BUILD_ARGS} -t "${FULL_IMAGE_NAME}" . # Check if build was successful if [ $? -eq 0 ]; then diff --git a/src/blog/api.ts b/src/blog/api.ts index 0fd99fb..ca10bb1 100644 --- a/src/blog/api.ts +++ b/src/blog/api.ts @@ -3,9 +3,9 @@ */ import { apiRequest } from '../utils/api'; +import { getS3Url } from './s3Config'; import type { UploadPresignedURLResponse, - DownloadPresignedURLResponse, CreatePostResponse, } from './types'; @@ -27,22 +27,6 @@ export async function getUploadPresignedURL(fileName: string): Promise { - const response = await apiRequest(`${API_BASE}/file/download`, { - method: 'POST', - body: JSON.stringify({ fileKey }), - }); - - if (!response.ok) { - throw new Error(`Failed to get download URL: ${response.statusText}`); - } - - return response.json(); -} - /** * Upload file to S3 using presigned URL */ @@ -97,10 +81,10 @@ export async function uploadImage( // Step 2: Upload file to S3 await uploadFileToS3(uploadUrl, file, onProgress); - // Step 3: Get download URL - const { url: downloadUrl } = await getDownloadPresignedURL(fileKey); + // Step 3: Generate public S3 URL from fileKey + const url = getS3Url(fileKey); - return { fileKey, url: downloadUrl }; + return { fileKey, url }; } /** diff --git a/src/blog/nodes/ImageComponent.tsx b/src/blog/nodes/ImageComponent.tsx index 491c76e..12e1acf 100644 --- a/src/blog/nodes/ImageComponent.tsx +++ b/src/blog/nodes/ImageComponent.tsx @@ -18,10 +18,12 @@ import { useEffect, useRef, useState, + useMemo, } from 'react'; import ImageResizer from './ImageResizer'; import { $isImageNode } from './ImageNode'; +import { getS3Url } from '../s3Config'; type ImageStatus = | { error: true } @@ -143,6 +145,14 @@ export default function ImageComponent({ const [editor] = useLexicalComposerContext(); const [isLoadError, setIsLoadError] = useState(false); + // Generate actual image URL from fileKey or use src + const imageUrl = useMemo(() => { + if (_fileKey) { + return getS3Url(_fileKey); + } + return src; // Fallback to src for data URLs during upload + }, [_fileKey, src]); + const $onEscape = useCallback( () => { if (isSelected) { @@ -259,7 +269,7 @@ export default function ImageComponent({ ) : ( { } static importJSON(serializedNode: SerializedImageNode): ImageNode { - const { altText, height, width, maxWidth, src, fileKey, uploadProgress, uploadError } = serializedNode; + const { altText, height, width, maxWidth, fileKey, uploadProgress, uploadError } = serializedNode; + + // Generate src from fileKey if available, otherwise use empty string + const src = fileKey ? '' : ''; // src will be generated from fileKey when rendering + return $createImageNode({ altText, height, @@ -150,7 +153,7 @@ export class ImageNode extends DecoratorNode { altText: this.getAltText(), height: this.__height === 'inherit' ? 0 : this.__height, maxWidth: this.__maxWidth, - src: this.getSrc(), + // Don't export src - it's mutable and will be generated from fileKey type: 'image', version: 1, width: this.__width === 'inherit' ? 0 : this.__width, diff --git a/src/blog/s3Config.ts b/src/blog/s3Config.ts new file mode 100644 index 0000000..6a6d916 --- /dev/null +++ b/src/blog/s3Config.ts @@ -0,0 +1,39 @@ +/** + * S3 configuration utility for Blog + */ + +// S3 endpoint and bucket from environment variables +export const S3_ENDPOINT_BLOG = import.meta.env.VITE_S3_ENDPOINT_BLOG || 'https://s3.amazonaws.com'; +export const S3_BUCKET_BLOG = import.meta.env.VITE_S3_BUCKET_BLOG || 'default-bucket'; + +/** + * Generate S3 URL from file key + * @param fileKey - The S3 file key (e.g., "uploads/image-123.png") + * @returns Full S3 URL to access the file + */ +export function getS3Url(fileKey: string): string { + // Remove leading slash if present + const key = fileKey.startsWith('/') ? fileKey.slice(1) : fileKey; + + // Construct URL: https://s3.example.com/bucket-name/uploads/image-123.png + return `${S3_ENDPOINT_BLOG}/${S3_BUCKET_BLOG}/${key}`; +} + +/** + * Extract file key from S3 URL + * @param url - Full S3 URL + * @returns File key or null if not a valid S3 URL + */ +export function extractFileKeyFromUrl(url: string): string | null { + try { + const urlObj = new URL(url); + const pathParts = urlObj.pathname.split('/'); + + // Remove empty parts and bucket name + const filteredParts = pathParts.filter(p => p && p !== S3_BUCKET_BLOG); + + return filteredParts.join('/') || null; + } catch { + return null; + } +} diff --git a/src/blog/types.ts b/src/blog/types.ts index 1b3e465..8530db4 100644 --- a/src/blog/types.ts +++ b/src/blog/types.ts @@ -8,11 +8,6 @@ export interface UploadPresignedURLResponse { expireAt: number; } -export interface DownloadPresignedURLResponse { - url: string; - expireAt: number; -} - export interface CreatePostResponse { postId: string; }