feat: add S3 configuration and utility functions for blog image handling
All checks were successful
CI - Build and Push / Build and Push Docker Image (push) Successful in 17s
All checks were successful
CI - Build and Push / Build and Push Docker Image (push) Successful in 17s
This commit is contained in:
3
.env.example
Normal file
3
.env.example
Normal file
@@ -0,0 +1,3 @@
|
||||
# S3 Configuration for Blog Images
|
||||
VITE_S3_ENDPOINT_BLOG=https://s3.example.com
|
||||
VITE_S3_BUCKET_BLOG=my-blog-bucket
|
||||
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -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:
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -12,6 +12,11 @@ dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
20
script/ci.sh
20
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
|
||||
|
||||
@@ -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<UploadPre
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get presigned URL for file download
|
||||
*/
|
||||
export async function getDownloadPresignedURL(fileKey: string): Promise<DownloadPresignedURLResponse> {
|
||||
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 };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<boolean>(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({
|
||||
) : (
|
||||
<LazyImage
|
||||
className={isFocused ? 'focused' : null}
|
||||
src={src}
|
||||
src={imageUrl}
|
||||
altText={altText}
|
||||
imageRef={imageRef}
|
||||
width={width}
|
||||
|
||||
@@ -47,7 +47,6 @@ export type SerializedImageNode = Spread<
|
||||
altText: string;
|
||||
height?: number;
|
||||
maxWidth: number;
|
||||
src: string;
|
||||
width?: number;
|
||||
fileKey?: string;
|
||||
uploadProgress?: number;
|
||||
@@ -85,7 +84,11 @@ export class ImageNode extends DecoratorNode<JSX.Element> {
|
||||
}
|
||||
|
||||
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<JSX.Element> {
|
||||
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,
|
||||
|
||||
39
src/blog/s3Config.ts
Normal file
39
src/blog/s3Config.ts
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,6 @@ export interface UploadPresignedURLResponse {
|
||||
expireAt: number;
|
||||
}
|
||||
|
||||
export interface DownloadPresignedURLResponse {
|
||||
url: string;
|
||||
expireAt: number;
|
||||
}
|
||||
|
||||
export interface CreatePostResponse {
|
||||
postId: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user