Skip to content

Instantly share code, notes, and snippets.

@siumhossain
Created September 1, 2025 17:10
Show Gist options
  • Save siumhossain/c766b67b035daf142e1200490263745f to your computer and use it in GitHub Desktop.
Save siumhossain/c766b67b035daf142e1200490263745f to your computer and use it in GitHub Desktop.
cloudflare r2 upload with express js
// server.js
const express = require('express');
const multer = require('multer');
const { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand } = require('@aws-sdk/client-s3');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
const crypto = require('crypto');
const path = require('path');
const fs = require('fs');
require('dotenv').config();

const app = express();
const port = process.env.PORT || 3000;

// Cloudflare R2 configuration
const r2Client = new S3Client({
  region: 'auto',
  endpoint: `https://${process.env.CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com`,
  credentials: {
    accessKeyId: process.env.R2_ACCESS_KEY_ID,
    secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
  },
});

const BUCKET_NAME = process.env.R2_BUCKET_NAME;

// Configure multer for file uploads
const upload = multer({
  dest: 'uploads/',
  limits: {
    fileSize: 100 * 1024 * 1024, // 100MB limit
  },
});

// Middleware
app.use(express.json());
app.use(express.static('public'));

// Helper function to generate unique filename
const generateFileName = (originalName) => {
  const ext = path.extname(originalName);
  const name = path.basename(originalName, ext);
  const timestamp = Date.now();
  const random = crypto.randomBytes(8).toString('hex');
  return `${name}-${timestamp}-${random}${ext}`;
};

// Upload file to R2
app.post('/upload', upload.single('file'), async (req, res) => {
  try {
    if (!req.file) {
      return res.status(400).json({ error: 'No file uploaded' });
    }

    const fileName = generateFileName(req.file.originalname);
    const fileBuffer = fs.readFileSync(req.file.path);
    
    const uploadParams = {
      Bucket: BUCKET_NAME,
      Key: fileName,
      Body: fileBuffer,
      ContentType: req.file.mimetype,
      Metadata: {
        originalName: req.file.originalname,
        uploadedAt: new Date().toISOString(),
      }
    };

    // Upload to R2
    const command = new PutObjectCommand(uploadParams);
    await r2Client.send(command);

    // Clean up temporary file
    fs.unlinkSync(req.file.path);

    // Generate public URL (if bucket is configured for public access)
    const publicUrl = `https://${process.env.R2_PUBLIC_DOMAIN}/${fileName}`;

    res.json({
      success: true,
      message: 'File uploaded successfully',
      data: {
        fileName,
        originalName: req.file.originalname,
        size: req.file.size,
        mimetype: req.file.mimetype,
        url: publicUrl,
        key: fileName
      }
    });

  } catch (error) {
    console.error('Upload error:', error);
    
    // Clean up temporary file if it exists
    if (req.file && fs.existsSync(req.file.path)) {
      fs.unlinkSync(req.file.path);
    }
    
    res.status(500).json({ 
      error: 'Failed to upload file',
      details: error.message 
    });
  }
});

// Upload multiple files
app.post('/upload/multiple', upload.array('files', 10), async (req, res) => {
  try {
    if (!req.files || req.files.length === 0) {
      return res.status(400).json({ error: 'No files uploaded' });
    }

    const uploadPromises = req.files.map(async (file) => {
      const fileName = generateFileName(file.originalname);
      const fileBuffer = fs.readFileSync(file.path);
      
      const uploadParams = {
        Bucket: BUCKET_NAME,
        Key: fileName,
        Body: fileBuffer,
        ContentType: file.mimetype,
        Metadata: {
          originalName: file.originalname,
          uploadedAt: new Date().toISOString(),
        }
      };

      const command = new PutObjectCommand(uploadParams);
      await r2Client.send(command);

      // Clean up temporary file
      fs.unlinkSync(file.path);

      return {
        fileName,
        originalName: file.originalname,
        size: file.size,
        mimetype: file.mimetype,
        url: `https://${process.env.R2_PUBLIC_DOMAIN}/${fileName}`,
        key: fileName
      };
    });

    const uploadedFiles = await Promise.all(uploadPromises);

    res.json({
      success: true,
      message: `${uploadedFiles.length} files uploaded successfully`,
      data: uploadedFiles
    });

  } catch (error) {
    console.error('Multiple upload error:', error);
    
    // Clean up temporary files
    if (req.files) {
      req.files.forEach(file => {
        if (fs.existsSync(file.path)) {
          fs.unlinkSync(file.path);
        }
      });
    }
    
    res.status(500).json({ 
      error: 'Failed to upload files',
      details: error.message 
    });
  }
});

// Generate presigned URL for direct upload
app.post('/upload/presigned', async (req, res) => {
  try {
    const { fileName, contentType } = req.body;
    
    if (!fileName || !contentType) {
      return res.status(400).json({ 
        error: 'fileName and contentType are required' 
      });
    }

    const key = generateFileName(fileName);
    
    const command = new PutObjectCommand({
      Bucket: BUCKET_NAME,
      Key: key,
      ContentType: contentType,
    });

    const signedUrl = await getSignedUrl(r2Client, command, { 
      expiresIn: 3600 // 1 hour
    });

    res.json({
      success: true,
      data: {
        uploadUrl: signedUrl,
        key,
        expiresIn: 3600
      }
    });

  } catch (error) {
    console.error('Presigned URL error:', error);
    res.status(500).json({ 
      error: 'Failed to generate presigned URL',
      details: error.message 
    });
  }
});

// Get file info
app.get('/file/:key', async (req, res) => {
  try {
    const { key } = req.params;
    
    const command = new GetObjectCommand({
      Bucket: BUCKET_NAME,
      Key: key,
    });

    const response = await r2Client.send(command);
    
    res.json({
      success: true,
      data: {
        key,
        contentType: response.ContentType,
        contentLength: response.ContentLength,
        lastModified: response.LastModified,
        metadata: response.Metadata,
        url: `https://${process.env.R2_PUBLIC_DOMAIN}/${key}`
      }
    });

  } catch (error) {
    if (error.name === 'NoSuchKey') {
      return res.status(404).json({ error: 'File not found' });
    }
    
    console.error('Get file error:', error);
    res.status(500).json({ 
      error: 'Failed to get file info',
      details: error.message 
    });
  }
});

// Delete file
app.delete('/file/:key', async (req, res) => {
  try {
    const { key } = req.params;
    
    const command = new DeleteObjectCommand({
      Bucket: BUCKET_NAME,
      Key: key,
    });

    await r2Client.send(command);
    
    res.json({
      success: true,
      message: 'File deleted successfully'
    });

  } catch (error) {
    console.error('Delete file error:', error);
    res.status(500).json({ 
      error: 'Failed to delete file',
      details: error.message 
    });
  }
});

// Error handling middleware
app.use((error, req, res, next) => {
  if (error instanceof multer.MulterError) {
    if (error.code === 'LIMIT_FILE_SIZE') {
      return res.status(400).json({ error: 'File too large' });
    }
    if (error.code === 'LIMIT_FILE_COUNT') {
      return res.status(400).json({ error: 'Too many files' });
    }
  }
  
  console.error('Server error:', error);
  res.status(500).json({ error: 'Internal server error' });
});

// Health check
app.get('/health', (req, res) => {
  res.json({ status: 'OK', timestamp: new Date().toISOString() });
});

app.listen(port, () => {
  console.log(`Server running on port ${port}`);
  console.log(`R2 Bucket: ${BUCKET_NAME}`);
});

// Ensure uploads directory exists
if (!fs.existsSync('uploads')) {
  fs.mkdirSync('uploads');
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment