diff --git a/apps/web/eslint.config.js b/apps/web/eslint.config.js index acfc754..022268c 100644 --- a/apps/web/eslint.config.js +++ b/apps/web/eslint.config.js @@ -38,15 +38,4 @@ export default [ // add more generic rule sets here, such as: // js.configs.recommended, ...eslintPluginAstro.configs.recommended, - { - rules: { - // override/add rules settings here, such as: - // "astro/no-set-html-directive": "error" - }, - languageOptions: { - parserOptions: { - project: "./tsconfig.json", - }, - }, - }, ]; diff --git a/apps/web/src/layouts/Layout.astro b/apps/web/src/layouts/Layout.astro index 3567314..324fcb3 100644 --- a/apps/web/src/layouts/Layout.astro +++ b/apps/web/src/layouts/Layout.astro @@ -6,7 +6,7 @@ Cloudflare App with Astro | Atyantik Technologies diff --git a/apps/web/src/pages/api/storage.ts b/apps/web/src/pages/api/storage.ts deleted file mode 100644 index c575dce..0000000 --- a/apps/web/src/pages/api/storage.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { APIRoute } from "astro"; -import { listStorageRecords } from "@services/database"; - -/** - * POST route handler for uploading images to R2. - */ -export const GET: APIRoute = async ({ locals }) => { - try { - const cache = locals.runtime.env.CACHE; - const cachedStorageRecords = await cache.get("storage_records"); - if (cachedStorageRecords) { - return new Response(cachedStorageRecords, { - status: 200, - headers: { - "Content-Type": "application/json", - "X-Cache": "HIT", - }, - }); - } - const storageRecords = await listStorageRecords(locals.dbClient); - await cache.put("storage_records", JSON.stringify(storageRecords)); - return new Response(JSON.stringify(storageRecords), { - status: 200, - headers: { - "Content-Type": "application/json", - "X-Cache": "MISS", - }, - }); - } catch (ex) { - return new Response( - JSON.stringify({ - error: ex instanceof Error ? ex.message : "An error occurred", - }), - { - status: 500, - headers: { "Content-Type": "application/json" }, - }, - ); - } - - // Respond with the image URL -}; diff --git a/apps/web/src/pages/gallery.astro b/apps/web/src/pages/gallery.astro new file mode 100644 index 0000000..55f75e1 --- /dev/null +++ b/apps/web/src/pages/gallery.astro @@ -0,0 +1,91 @@ +--- +import Layout from "@layouts/Layout.astro"; +import { listStorageRecords } from "@services/database"; + +const cache = Astro.locals.runtime.env.CACHE; +let cachedStorageRecords = await cache.get("storage_records"); +if (!cachedStorageRecords) { + const storageRecords = await listStorageRecords(Astro.locals.dbClient); + await cache.put("storage_records", JSON.stringify(storageRecords)); + cachedStorageRecords = JSON.stringify(storageRecords); +} +const records = JSON.parse(cachedStorageRecords) as Awaited< + ReturnType +>; +--- + + +
+
+

The Gallery

+

The gallery resets in every 5 minutes!

+

Click here to upload new picture →

+
+
+ { + records.map((record) => ( + {record.originalName} + )) + } +
+
+
+ + diff --git a/apps/web/src/pages/index.astro b/apps/web/src/pages/index.astro index 1427761..d740f81 100644 --- a/apps/web/src/pages/index.astro +++ b/apps/web/src/pages/index.astro @@ -12,6 +12,9 @@ const serverTime = Date.now(); ⚡ Blazing Fast, Budget Friendly, Built to Scale

+

+ Checkout Gallery Example +

diff --git a/apps/web/src/utils/r2-storage.util.ts b/apps/web/src/utils/r2-storage.util.ts index 961f9bb..fa17626 100644 --- a/apps/web/src/utils/r2-storage.util.ts +++ b/apps/web/src/utils/r2-storage.util.ts @@ -8,7 +8,7 @@ import type { R2Bucket } from "@cloudflare/workers-types"; */ export function validateFile(file: File | null): File { if (!file || !(file instanceof File)) { - throw new Error("No image file provided or invalid file type."); + throw new Error("No file provided or invalid file type."); } return file; } @@ -50,25 +50,3 @@ export async function uploadFile( }, }); } - -/** - * Constructs the CDN URL based on the environment. - * @param mode - The current environment mode ('production' or others). - * @param cdnUrl - The CDN base URL from environment variables. - * @param requestUrl - The original request URL. - * @param key - The unique key of the uploaded file. - * @returns The full CDN URL as a string. - */ -export function constructCdnUrl( - mode: string, - cdnUrl: string | undefined, - requestUrl: string, - key: string, -): string { - const isProduction = mode === "production"; - const baseCdnUrl = - isProduction && typeof cdnUrl === "string" && cdnUrl.length - ? cdnUrl - : new URL("/cdn/", requestUrl); - return new URL(key, baseCdnUrl).toString(); -} diff --git a/apps/web/src/pages/api/upload.ts b/apps/web/src/utils/upload.util.ts similarity index 73% rename from apps/web/src/pages/api/upload.ts rename to apps/web/src/utils/upload.util.ts index deac777..3488705 100644 --- a/apps/web/src/pages/api/upload.ts +++ b/apps/web/src/utils/upload.util.ts @@ -1,11 +1,5 @@ -import type { APIRoute } from "astro"; import type { R2Bucket } from "@cloudflare/workers-types"; -import { - constructCdnUrl, - fileExists, - uploadFile, - validateFile, -} from "@utils/r2-storage.util"; +import { fileExists, uploadFile, validateFile } from "@utils/r2-storage.util"; import { computeShortHash } from "@utils/hash.util"; import type { DrizzleD1Database } from "drizzle-orm/d1"; import { @@ -35,15 +29,12 @@ function generateKey(hashHex: string, fileName: string): string { * @throws Error if any step fails. */ async function handleUpload( - formData: FormData, + formFile: File, storage: R2Bucket, - cdnUrlEnv: string | undefined, - mode: string, - requestUrl: string, db: DrizzleD1Database, ): Promise { // Validate the image - const file = validateFile(formData.get("file") as File); + const file = validateFile(formFile); // Read the file content as ArrayBuffer const arrayBuffer = await file.arrayBuffer(); @@ -79,7 +70,7 @@ async function handleUpload( } // Construct the CDN URL - const fileUrl = constructCdnUrl(mode, cdnUrlEnv, requestUrl, key); + const fileUrl = `/cdn/${key}`; return { ...fileFromKey, url: fileUrl, @@ -87,10 +78,13 @@ async function handleUpload( } /** - * POST route handler for uploading images to R2. + * Handles the File Upload from the request. + * @param file - The uploaded file. + * @param locals - The request locals. + * @returns The URL of the uploaded image. + * @throws Error if any step fails. */ -export const POST: APIRoute = async ({ request, locals }) => { - const { PUBLIC_CDN_URL } = locals.runtime.env; +export const handleFile = async (file: File, locals: globalThis.App.Locals) => { // @ts-expect-error we are using STORAGE from wrangler and types which has different // signatures than the one from the worker const storage = locals.runtime.env.STORAGE as R2Bucket; @@ -99,28 +93,13 @@ export const POST: APIRoute = async ({ request, locals }) => { throw new Error("You need to add storage binding to the environment."); } const { dbClient } = locals; - const mode = import.meta.env.MODE; - const requestUrl = request.url; - try { - // Parse form data - const formData = await request.formData(); // Handle the upload process - const fileData = await handleUpload( - formData, - storage, - PUBLIC_CDN_URL, - mode, - requestUrl, - dbClient, - ); + const fileData = await handleUpload(file, storage, dbClient); // Empty the cache cache.delete("storage_records"); // Respond with the image URL - return new Response(JSON.stringify(fileData), { - status: 200, - headers: { "Content-Type": "application/json" }, - }); + return fileData; } catch (error) { let errorMessage = "Failed to upload image. Please try again later."; let status = 500; @@ -134,10 +113,6 @@ export const POST: APIRoute = async ({ request, locals }) => { ? error.message : "Failed to upload image. Please try again later."; } - - return new Response(JSON.stringify({ error: errorMessage }), { - status, - headers: { "Content-Type": "application/json" }, - }); + throw new Error(errorMessage); } }; diff --git a/apps/worker/package.json b/apps/worker/package.json index 95a4d73..c8a2811 100644 --- a/apps/worker/package.json +++ b/apps/worker/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "deploy": "npm run setup && wrangler deploy", - "dev": "npm run setup && wrangler dev --persist-to=../../.wrangler/state", + "dev": "npm run setup && wrangler dev --test-scheduled --persist-to=../../.wrangler/state", "start": "npm run setup && wrangler dev", "test": "npm run setup && CI=true vitest run", "setup": "node ../../scripts/generate-wrangler.json.js && wrangler types" diff --git a/apps/worker/src/index.ts b/apps/worker/src/index.ts index 6444a05..ec8916c 100644 --- a/apps/worker/src/index.ts +++ b/apps/worker/src/index.ts @@ -43,20 +43,18 @@ export default { ctx.waitUntil( (async () => { // Clear the storage every 5th minute - if (new Date().getMinutes() % 5 === 0) { + if (event.cron.startsWith('*/5')) { const DB = await getDBClient(this, env.DB); const STORAGE = env.STORAGE; const CACHE = env.CACHE; - DB.transaction(async (tx) => { - // Get all storage Records - const storageRecords = await listStorageRecords(tx); - // Remove each storage record from - for (const record of storageRecords) { - await STORAGE.delete(record.key); - } - await clearStorageRecords(tx); - await CACHE.delete('storage_records'); - }); + // Get all storage Records + const storageRecords = await listStorageRecords(DB); + // Remove each storage record from + for (const record of storageRecords) { + await STORAGE.delete(record.key); + } + await clearStorageRecords(DB); + await CACHE.delete('storage_records'); } })(), );