xpaintdev / apidoc /fileworker.js
suisuyy
Initialize xpaintai project with core files and basic structure
763be49
// 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(`<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) || [];
// 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()
});
}
}