// =============================== // Cloudflare W...
创建于:2026年3月31日
使用 GPT-5.4 Thinking 回答 Chat01
创建于:2026年3月31日
使用 GPT-5.4 Thinking 回答 Chat01
// ===============================
// Cloudflare Worker with D1
// Data transfer with automatic expiration (no delete on retrieve by default)
// ===============================
function jsonResponse(body, { status = 200, headers = {} } = {}) {
return new Response(JSON.stringify(body), {
status,
headers: {
"Content-Type": "application/json; charset=utf-8",
...headers,
},
});
}
function getCorsHeaders() {
return {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
};
}
function getClientIp(request) {
return (
request.headers.get("CF-Connecting-IP") ||
request.headers.get("X-Forwarded-For")?.split(",")[0] ||
"unknown"
);
}
export default {
async fetch(request, env) {
const url = new URL(request.url);
const path = url.pathname;
const method = request.method.toUpperCase();
const corsHeaders = getCorsHeaders();
textif (method === "OPTIONS") { return new Response(null, { headers: corsHeaders }); } // ---------- POST /store ---------- if (path === "/store" && method === "POST") { try { const body = await request.json(); const key = body?.key; const data = body?.data; let ttl = body?.ttl; if (!key || data === undefined) { return jsonResponse( { error: "Missing key or data" }, { status: 400, headers: corsHeaders } ); } const defaultTtl = env.DEFAULT_TTL_SECONDS ? parseInt(env.DEFAULT_TTL_SECONDS, 10) : 3600; let ttlSeconds = Number.isFinite(Number(ttl)) ? Number(ttl) : defaultTtl; if (ttlSeconds <= 0) ttlSeconds = defaultTtl; const now = Date.now(); const expiresAt = now + ttlSeconds * 1000; const ip = getClientIp(request); // Check if key already exists const existing = await env.DB.prepare( "SELECT key FROM data_store WHERE key = ?" ).bind(key).first(); if (existing) { // Update existing (overwrite) await env.DB.prepare( "UPDATE data_store SET data = ?, ip = ?, updated_at = ?, expires_at = ? WHERE key = ?" ).bind(JSON.stringify(data), ip, now, expiresAt, key).run(); return jsonResponse( { success: true, action: "updated", key }, { headers: corsHeaders } ); } else { // Insert new await env.DB.prepare( "INSERT INTO data_store (key, data, ip, created_at, updated_at, expires_at) VALUES (?, ?, ?, ?, ?, ?)" ).bind(key, JSON.stringify(data), ip, now, now, expiresAt).run(); return jsonResponse( { success: true, action: "stored", key }, { headers: corsHeaders } ); } } catch (err) { console.error(`[store] Error: ${err.message}`); return jsonResponse( { error: err?.message || "Internal error" }, { status: 500, headers: corsHeaders } ); } } // ---------- GET /retrieve?key=...&delete=... ---------- // delete=true – удаляет запись после получения (как раньше) // delete=false – только читает, не удаляет (по умолчанию) if (path === "/retrieve" && method === "GET") { const key = url.searchParams.get("key"); const shouldDelete = url.searchParams.get("delete") === "true"; if (!key) { return jsonResponse( { error: "Missing key" }, { status: 400, headers: corsHeaders } ); } const now = Date.now(); try { if (shouldDelete) { // Атомарное получение + удаление (как в старом коде) let deleted; try { deleted = await env.DB.prepare( "DELETE FROM data_store WHERE key = ? AND expires_at >= ? RETURNING data, expires_at" ).bind(key, now).first(); } catch (e) { // fallback transaction const msg = (e?.message || "").toLowerCase(); const maybeReturningUnsupported = msg.includes("returning") || msg.includes("syntax error"); if (!maybeReturningUnsupported) throw e; const stmts = [ env.DB.prepare("BEGIN IMMEDIATE"), env.DB.prepare("SELECT data, expires_at FROM data_store WHERE key = ? AND expires_at >= ?").bind(key, now), env.DB.prepare("DELETE FROM data_store WHERE key = ? AND expires_at >= ?").bind(key, now), env.DB.prepare("COMMIT"), ]; let results; try { results = await env.DB.batch(stmts); } catch (txErr) { await env.DB.prepare("ROLLBACK").run(); throw txErr; } const selected = results?.[1]?.results?.[0]; if (selected) deleted = selected; } if (deleted) { let parsed; try { parsed = JSON.parse(deleted.data); } catch { parsed = deleted.data; } return jsonResponse( { success: true, data: parsed, action: "deleted" }, { headers: corsHeaders } ); } // Проверка на истекшую запись const expired = await env.DB.prepare("SELECT expires_at FROM data_store WHERE key = ?").bind(key).first(); if (expired && expired.expires_at < now) { await env.DB.prepare("DELETE FROM data_store WHERE key = ? AND expires_at < ?").bind(key, now).run(); return jsonResponse( { error: "Data expired" }, { status: 410, headers: corsHeaders } ); } return jsonResponse( { error: "Key not found or already retrieved" }, { status: 404, headers: corsHeaders } ); } else { // Только чтение, без удаления const record = await env.DB.prepare( "SELECT data, expires_at FROM data_store WHERE key = ? AND expires_at >= ?" ).bind(key, now).first(); if (!record) { // Проверка на истекшую запись const expired = await env.DB.prepare("SELECT expires_at FROM data_store WHERE key = ?").bind(key).first(); if (expired && expired.expires_at < now) { await env.DB.prepare("DELETE FROM data_store WHERE key = ? AND expires_at < ?").bind(key, now).run(); return jsonResponse( { error: "Data expired" }, { status: 410, headers: corsHeaders } ); } return jsonResponse( { error: "Key not found" }, { status: 404, headers: corsHeaders } ); } let parsed; try { parsed = JSON.parse(record.data); } catch { parsed = record.data; } return jsonResponse( { success: true, data: parsed, action: "peeked" }, { headers: corsHeaders } ); } } catch (err) { console.error(`[retrieve] Error: ${err.message}`); return jsonResponse( { error: err?.message || "Internal error" }, { status: 500, headers: corsHeaders } ); } } // ---------- DELETE /delete?key=... ---------- if (path === "/delete" && method === "DELETE") { const key = url.searchParams.get("key"); if (!key) { return jsonResponse( { error: "Missing key" }, { status: 400, headers: corsHeaders } ); } try { const result = await env.DB.prepare("DELETE FROM data_store WHERE key = ?").bind(key).run(); const deleted = (result?.meta?.changes || 0) > 0; return jsonResponse( { success: deleted }, { headers: corsHeaders } ); } catch (err) { console.error(`[delete] Error: ${err.message}`); return jsonResponse( { error: err?.message || "Internal error" }, { status: 500, headers: corsHeaders } ); } } // ---------- POST /cleanup ---------- if (path === "/cleanup" && method === "POST") { try { const now = Date.now(); const result = await env.DB.prepare("DELETE FROM data_store WHERE expires_at < ?").bind(now).run(); const deleted = result?.meta?.changes || 0; console.log(`[cleanup] Manually deleted ${deleted} expired records`); return jsonResponse( { success: true, deleted }, { headers: corsHeaders } ); } catch (err) { console.error(`[cleanup] Error: ${err.message}`); return jsonResponse( { error: err?.message || "Internal error" }, { status: 500, headers: corsHeaders } ); } } // ---------- Root info ---------- return jsonResponse( { name: "Data Transfer API", endpoints: { store: 'POST /store { "key": "...", "data": any, "ttl": seconds? }', retrieve: 'GET /retrieve?key=...&delete=true|false (delete=false by default)', delete: "DELETE /delete?key=...", cleanup: "POST /cleanup (manual expiration cleanup)", }, defaultTtlSeconds: env.DEFAULT_TTL_SECONDS || 3600, }, { headers: corsHeaders } );
},
// ---------- Scheduled cleanup ----------
async scheduled(event, env, ctx) {
const now = Date.now();
try {
const result = await env.DB.prepare("DELETE FROM data_store WHERE expires_at < ?").bind(now).run();
const deleted = result?.meta?.changes || 0;
console.log([Cron] Deleted ${deleted} expired records at ${new Date().toISOString()});
} catch (err) {
console.error([Cron] Error: ${err.message});
}
},
};curl "https://ваш-worker.workers.dev/retrieve?key=user2&delete=true" сделай чтоб данные обязяательно должны были удалится
Да. Самый надежный и понятный вариант для твоего кейса — в режиме delete=true:
Замени у себя блок if (shouldDelete) { ... } внутри /retrieve на этот:
jsif (shouldDelete) { // 1) Сначала читаем запись const record = await env.DB.prepare( "SELECT data, expires_at FROM data_store WHERE key = ?" ).bind(key).first(); if (!record) { return jsonResponse( { error: "Key not found" }, { status: 404, headers: corsHeaders } ); } // 2) Проверяем TTL if (record.expires_at < now) { await env.DB.prepare( "DELETE FROM data_store WHERE key = ? AND expires_at < ?" ).bind(key, now).run(); return jsonResponse( { error: "Data expired" }, { status: 410, headers: corsHeaders } ); } // 3) Удаляем запись const deleteResult = await env.DB.prepare( "DELETE FROM data_store WHERE key = ? AND expires_at >= ?" ).bind(key, now).run(); const deleted = (deleteResult?.meta?.changes || 0) > 0; // 4) Если удалить не удалось — не считаем запрос успешным if (!deleted) { return jsonResponse( { error: "Failed to delete data" }, { status: 409, headers: corsHeaders } ); } // 5) Доп.проверка: убеждаемся, что записи больше нет const stillExists = await env.DB.prepare( "SELECT key FROM data_store WHERE key = ?" ).bind(key).first(); if (stillExists) { return jsonResponse( { error: "Data was not fully deleted" }, { status: 500, headers: corsHeaders } ); } let parsed; try { parsed = JSON.parse(record.data); } catch { parsed = record.data; } return jsonResponse( { success: true, data: parsed, action: "retrieved_and_deleted" }, { headers: corsHeaders } ); }
Что это меняет:
success: true вернется только если DELETE реально удалил строку.Если хочешь, чтобы удаление было по умолчанию, а не только при delete=true, замени строку:
jsconst shouldDelete = url.searchParams.get("delete") === "true";
на:
jsconst shouldDelete = url.searchParams.get("delete") !== "false";
Тогда:
/retrieve?key=user2 → удалит/retrieve?key=user2&delete=true → удалит/retrieve?key=user2&delete=false → только прочитаетЕще важно: в таблице key должен быть PRIMARY KEY или UNIQUE. Например:
sqlCREATE TABLE IF NOT EXISTS data_store ( key TEXT PRIMARY KEY, data TEXT NOT NULL, ip TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, expires_at INTEGER NOT NULL );
Иначе возможны странности с несколькими одинаковыми ключами.
Твой curl останется таким же:
bashcurl "https://ваш-worker.workers.dev/retrieve?key=user2&delete=true"
Если хочешь, я могу сразу прислать тебе полностью готовый файл целиком с уже внесенными правками.