|
|
|
|
|
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() |
|
|
}); |
|
|
} |
|
|
} |