| |
| const requestCounter = new Map(); |
| const RATE_LIMIT = 20; |
| const WINDOW_SIZE = 60 * 1000; |
| const MAX_FILE_SIZE = 50 * 1024 * 1024; |
| const MAX_STORAGE = 100 * 1024 * 1024 * 1024; |
|
|
| async function getStorageUsage(bucket) { |
| let totalSize = 0; |
| let cursor; |
| |
| do { |
| const listing = await bucket.list({ |
| cursor, |
| include: ['customMetadata', 'httpMetadata'], |
| }); |
| |
| |
| for (const object of listing.objects) { |
| totalSize += object.size; |
| } |
| |
| cursor = listing.cursor; |
| } while (cursor); |
| |
| return totalSize; |
| } |
|
|
| function objectNotFound(objectName) { |
| return new Response(`<html><body>R2 object "<b>${objectName}</b>" not found</body></html>`, { |
| 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) || []; |
| |
| |
| const validRequests = userRequests.filter(timestamp => now - timestamp < WINDOW_SIZE); |
| |
| if (validRequests.length >= RATE_LIMIT) { |
| return false; |
| } |
| |
| |
| validRequests.push(now); |
| requestCounter.set(ip, validRequests); |
| |
| |
| 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) { |
| |
| const clientIP = request.headers.get('cf-connecting-ip') || 'unknown'; |
| |
| |
| 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}`); |
|
|
| |
| const currentUsage = await getStorageUsage(env.MY_BUCKET); |
| console.log(`Current R2 storage usage: ${(currentUsage / (1024 * 1024 * 1024)).toFixed(2)} GB`); |
|
|
| |
| 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') { |
| |
| if (currentUsage >= MAX_STORAGE) { |
| return new Response('Storage limit exceeded (100GB). Delete some files before uploading.', { |
| status: 507, |
| headers: corsHeaders() |
| }); |
| } |
|
|
| |
| 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() |
| }); |
| } |
|
|
| |
| 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); |
| } |
|
|
| |
| 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() |
| }); |
| } |
| } |