CDN (Cloudflare R2)

Overview

The application uses Cloudflare R2 for file storage and Supabase for database management. Files are uploaded using a two-step process:

  1. Get a presigned URL from R2
  2. Upload the file directly to R2 using the presigned URL

Basic Implementation Pattern

1. Frontend Component

Create a file input component that handles file selection:

<template>
  <div>
    <input type="file" @change="handleFileSelect" />
  </div>
</template>
<script setup>
const handleFileSelect = async (event) => {
  const file = event.target.files[0];
  if (!file) return;
  await uploadFile(file);
};
</script>

2. Upload Function

Create a composable for R2 uploads (e.g., useR2Upload):

export const useR2Upload = () => {
  // Single file upload
  const uploadSingleFile = async (file: File, path: string, user_id: string) => {
    // 1. Get presigned URL from backend
    const { data } = await useFetch("/api/storage/upload/path", {
      method: "POST",
      body: {
        user_id,
        file: {
          name: file.name,
          type: file.type,
        },
      },
    });
    // 2. Upload to R2 using presigned URL
    await fetch(data.value.uploadUrl, {
      method: "PUT",
      body: file,
      headers: {
        "Content-Type": file.type,
      },
    });
    return data.value.publicUrl;
  };
  // Multiple files upload
  const uploadMultipleFiles = async (files: File[], path: string, user_id: string) => {
    // 1. Get presigned URLs for all files
    const fileInfo = files.map((file) => ({
      fileName: file.name,
      contentType: file.type,
    }));
    const { data } = await useFetch("/api/storage/upload/path", {
      method: "POST",
      body: {
        user_id,
        files: fileInfo,
      },
    });
    // 2. Upload all files to R2
    const uploadPromises = files.map((file, index) =>
      fetch(data.value[index].uploadUrl, {
        method: "PUT",
        body: file,
        headers: {
          "Content-Type": file.type,
        },
      })
    );
    await Promise.all(uploadPromises);
    return data.value.map((result) => result.publicUrl);
  };
  return {
    uploadSingleFile,
    uploadMultipleFiles,
  };
};

3. Backend API Endpoint

Create an API endpoint to generate presigned URLs:

// server/api/storage/upload/[path].ts

import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

const s3Client = new S3Client({
  region: "auto",
  endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com/`,
  credentials: {
    accessKeyId: process.env.R2_ACCESS_KEY_ID!,
    secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
  },
});

export default defineEventHandler(async (event) => {
  const body = await readBody(event);
  const { user_id, files } = body;
  // For single file
  if (!Array.isArray(files)) {
    const file_key = `users/${user_id}/path/${files.name}`;
    const uploadUrl = await getSignedUrl(
      s3Client,
      new PutObjectCommand({
        Bucket: process.env.R2_BUCKET_NAME,
        Key: file_key,
        ContentType: files.type,
      }),
      { expiresIn: 3600 }
    );
    return {
      uploadUrl,
      publicUrl: `https://cdn.example.com/${file_key}`,
    };
  }
  // For multiple files
  const results = await Promise.all(
    files.map(async (file) => {
      const file_key = `users/${user_id}/path/${file.fileName}`;
      const uploadUrl = await getSignedUrl(
        s3Client,
        new PutObjectCommand({
          Bucket: process.env.R2_BUCKET_NAME,
          Key: file_key,
          ContentType: file.contentType,
        }),
        { expiresIn: 3600 }
      );
      return {
        uploadUrl,
        publicUrl: `https://cdn.example.com/${file_key}`,
      };
    })
  );
  return results;
});

Usage Examples

Single File Upload (e.g., Avatar)

// In component script
const uploadAvatar = async (file) => {
  const { uploadSingleFile } = useR2Upload();
  const publicUrl = await uploadSingleFile(file, "avatar", user_id);
  // Update database with new URL
  await supabase.from("profiles").update({ avatar_url: publicUrl }).eq("id", user_id);
};

Multiple Files Upload (e.g., Product Images)

// In component script
const uploadProductImages = async (files) => {
  const { uploadMultipleFiles } = useR2Upload();
  const publicUrls = await uploadMultipleFiles(files, "products", user_id);
  // Update database with new URLs
  await supabase
    .from("products")
    .update({
      images: publicUrls,
    })
    .eq("id", product_id);
};

File Structure

├── composables/
│ └── useR2Upload.ts
├── server/
│ └── api/
│ └── storage/
│ ├── upload/
│ │ └── [path].ts
│ └── delete.ts
└── components/
└── FileUploader.vue

Important Notes

  • Always validate file types and sizes before upload
  • Use environment variables for R2 credentials
  • Handle errors appropriately
  • Consider implementing file deletion when needed
  • URLs are stored in Supabase after successful upload