// Global rate limiting map and constants const requestCounter = new Map(); const RATE_LIMIT = 20; // requests per minute const WINDOW_SIZE = 60 * 1000; // 1 minute in milliseconds const MAX_FILE_SIZE = 50 * 1024 * 1024; // 20MB in bytes const MAX_STORAGE = 100 * 1024 * 1024 * 1024; // 100GB in bytes async function getStorageUsage(bucket) { let totalSize = 0; let cursor; do { const listing = await bucket.list({ cursor, include: ['customMetadata', 'httpMetadata'], }); // Sum up sizes of all objects in current page for (const object of listing.objects) { totalSize += object.size; } cursor = listing.cursor; } while (cursor); // Continue until no more pages return totalSize; } function objectNotFound(objectName) { return new Response(`R2 object "${objectName}" not found`, { status: 404, headers: { 'content-type': 'text/html; charset=UTF-8', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, HEAD, PUT, POST, OPTIONS', 'Access-Control-Allow-Headers': '*' } }); } function corsHeaders(headers = new Headers()) { headers.set('Access-Control-Allow-Origin', '*'); headers.set('Access-Control-Allow-Methods', 'GET, HEAD, PUT, POST, OPTIONS'); headers.set('Access-Control-Allow-Headers', '*'); return headers; } function checkRateLimit(ip) { const now = Date.now(); const userRequests = requestCounter.get(ip) || []; // Remove requests older than the window size const validRequests = userRequests.filter(timestamp => now - timestamp < WINDOW_SIZE); if (validRequests.length >= RATE_LIMIT) { return false; } // Add current request timestamp validRequests.push(now); requestCounter.set(ip, validRequests); // Cleanup old entries periodically if (Math.random() < 0.1) { for (const [key, timestamps] of requestCounter.entries()) { const validTimestamps = timestamps.filter(ts => now - ts < WINDOW_SIZE); if (validTimestamps.length === 0) { requestCounter.delete(key); } else { requestCounter.set(key, validTimestamps); } } } return true; } export default { async fetch(request, env) { // Get client IP const clientIP = request.headers.get('cf-connecting-ip') || 'unknown'; // Check rate limit if (!checkRateLimit(clientIP)) { return new Response('Rate limit exceeded. Please try again later.', { status: 429, headers: corsHeaders(new Headers({ 'Retry-After': '60' })) }); } const url = new URL(request.url); const objectName = url.pathname.slice(1); console.log(`${request.method} object ${objectName}: ${request.url}`); // Get current storage usage const currentUsage = await getStorageUsage(env.MY_BUCKET); console.log(`Current R2 storage usage: ${(currentUsage / (1024 * 1024 * 1024)).toFixed(2)} GB`); // Handle CORS preflight if (request.method === 'OPTIONS') { return new Response(null, { headers: corsHeaders() }); } if (request.method === 'GET' || request.method === 'HEAD') { if (objectName === '') { if (request.method === 'HEAD') { return new Response(undefined, { status: 400, headers: corsHeaders() }); } const options = { prefix: url.searchParams.get('prefix') ?? undefined, delimiter: url.searchParams.get('delimiter') ?? undefined, cursor: url.searchParams.get('cursor') ?? undefined, include: ['customMetadata', 'httpMetadata'], }; console.log(JSON.stringify(options)); const listing = await env.MY_BUCKET.list(options); return new Response('specify your path,current total storage used:' +currentUsage/1000/1000/1000+' GB', { headers: corsHeaders(new Headers({ 'content-type': 'application/json; charset=UTF-8' })) }); } if (request.method === 'GET') { const object = await env.MY_BUCKET.get(objectName, { range: request.headers, onlyIf: request.headers, }); if (object === null) { return objectNotFound(objectName); } const headers = new Headers(); object.writeHttpMetadata(headers); headers.set('etag', object.httpEtag); if (object.range) { headers.set("content-range", `bytes ${object.range.offset}-${object.range.end ?? object.size - 1}/${object.size}`); } const status = object.body ? (request.headers.get("range") !== null ? 206 : 200) : 304; return new Response(object.body, { headers: corsHeaders(headers), status }); } const object = await env.MY_BUCKET.head(objectName); if (object === null) { return objectNotFound(objectName); } const headers = new Headers(); object.writeHttpMetadata(headers); headers.set('etag', object.httpEtag); return new Response(null, { headers: corsHeaders(headers) }); } if (request.method === 'PUT' || request.method === 'POST') { // Check if storage usage exceeds limit if (currentUsage >= MAX_STORAGE) { return new Response('Storage limit exceeded (100GB). Delete some files before uploading.', { status: 507, // Insufficient Storage headers: corsHeaders() }); } // Check content length const contentLength = parseInt(request.headers.get('content-length') || '0'); if (contentLength > MAX_FILE_SIZE) { return new Response('File too large. Maximum size is 20MB.', { status: 413, headers: corsHeaders() }); } // Handle chunked uploads if (!contentLength) { const chunks = []; let totalSize = 0; for await (const chunk of request.body) { totalSize += chunk.length; if (totalSize > MAX_FILE_SIZE) { return new Response('File too large. Maximum size is 20MB.', { status: 413, headers: corsHeaders() }); } chunks.push(chunk); } request.body = new Blob(chunks); } // Check if this upload would exceed storage limit if (currentUsage + contentLength >= MAX_STORAGE) { return new Response('This upload would exceed the 100GB storage limit.', { status: 507, headers: corsHeaders() }); } const object = await env.MY_BUCKET.put(objectName, request.body, { httpMetadata: request.headers, }); return new Response(objectName, { headers: corsHeaders(new Headers({ 'etag': object.httpEtag, 'X-Storage-Usage': `${(currentUsage / (1024 * 1024 * 1024)).toFixed(2)}GB` })) }); } if (request.method === 'DELETE') { return new Response('delete unsupported', { headers: corsHeaders() }); } return new Response(`Unsupported method`, { status: 400, headers: corsHeaders() }); } }