From 8c0e62521eda2126405090b5c6eee63dcabba784 Mon Sep 17 00:00:00 2001 From: naihe <239144498@qq.com> Date: Sun, 20 Apr 2025 22:42:51 +0800 Subject: [PATCH 1/6] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=EF=BC=8C=E5=90=8C=E6=AD=A5=E6=9C=80=E6=96=B0?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E7=89=B9=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Docs/REST API.md | 6 +++--- src/controllers/paste.controller.ts | 10 +++++----- src/routes/paste.routes.ts | 6 +++--- static/js/home.js | 2 +- static/js/render.js | 2 +- static/js/service-worker.js | 2 +- static/js/utils.js | 2 +- 7 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Docs/REST API.md b/Docs/REST API.md index c88bdc6..6269df5 100644 --- a/Docs/REST API.md +++ b/Docs/REST API.md @@ -11,7 +11,7 @@ GET /health ### 1. 上传/更新内容 ```http -POST /s/{访问路径}/{密码} +POST /save/{访问路径}/{密码} ``` - 说明:如果内容存在则更新,不存在则创建 - 权限:更新已有内容需要是创建者 @@ -24,7 +24,7 @@ POST /s/{访问路径}/{密码} import requests def upload_content(path, content, password="", expire=315360000): - url = f'https://qbin.me/s/{path}/{password}' + url = f'https://qbin.me/save/{path}/{password}' headers = { 'Content-Type': 'text/plain', 'x-expire': str(expire), @@ -46,7 +46,7 @@ upload_content('test123', 'Hello World', 'password123') ### 2. 上传文件内容 (不支持更新) ```http -PUT /s/{访问路径}/{密码} +PUT /save/{访问路径}/{密码} ``` - 说明:如果访问路径已存在且未过期,返回409错误 - 请求头: diff --git a/src/controllers/paste.controller.ts b/src/controllers/paste.controller.ts index 5f8091a..fd615f3 100644 --- a/src/controllers/paste.controller.ts +++ b/src/controllers/paste.controller.ts @@ -45,7 +45,7 @@ export async function getRaw(ctx: Context) { ctx.state.metadata = { etag: full.hash, time: full.time }; ctx.response.headers.set("Pragma", "no-cache"); ctx.response.headers.set("Cache-Control", "no-cache, must-revalidate"); // private , must-revalidate | , max-age=3600 - ctx.response.headers.set("Content-Type", full.mime); + ctx.response.headers.set("Content-Type", full.mime || full.type); ctx.response.headers.set("Content-Length", full.len.toString()); ctx.response.body = full.content; } @@ -64,11 +64,11 @@ export async function queryRaw(ctx: Context) { } ctx.response.status = 200; - ctx.response.headers.set("Content-Type", meta.mime); + ctx.response.headers.set("Content-Type", meta.mime || meta.type); ctx.response.headers.set("Content-Length", meta.len.toString()); } -/** POST/PUT /s/:key/:pwd? 统一入口:若 key 已存在则更新,否则创建 */ +/** POST/PUT /save/:key/:pwd? 统一入口:若 key 已存在则更新,否则创建 */ export async function save(ctx: Context) { const { key, pwd } = parsePathParams(ctx.params); if (reservedPaths.has(key.toLowerCase())) { @@ -83,7 +83,7 @@ export async function save(ctx: Context) { : await createNew(ctx, key, pwd, repo); } -/** DELETE /d/:key/:pwd? */ +/** DELETE /delete/:key/:pwd? */ export async function remove(ctx: Context) { const { key, pwd } = parsePathParams(ctx.params); if (reservedPaths.has(key.toLowerCase())) throw new Response(ctx, 403, ResponseMessages.PATH_RESERVED); @@ -198,7 +198,7 @@ async function assembleMetadata( if (len > MAX_UPLOAD_FILE_SIZE) { throw new Response(ctx, 413, ResponseMessages.CONTENT_TOO_LARGE); } - const mime = headers.get("Content-Type") ?? "application/octet-stream"; + const mime = headers.get("Content-Type") || "application/octet-stream"; if (!mimeTypeRegex.test(mime)) { throw new Response(ctx, 415, ResponseMessages.INVALID_CONTENT_TYPE); } diff --git a/src/routes/paste.routes.ts b/src/routes/paste.routes.ts index e2500fa..0267353 100644 --- a/src/routes/paste.routes.ts +++ b/src/routes/paste.routes.ts @@ -11,8 +11,8 @@ const router = new Router(); router .get("/r/:key?/:pwd?", getRaw) .head("/r/:key?/:pwd?", queryRaw) - .post("/s/:key/:pwd?", save) - .put("/s/:key/:pwd?", save) - .delete("/d/:key/:pwd?", remove); + .post("/save/:key/:pwd?", save) + .put("/save/:key/:pwd?", save) + .delete("/delete/:key/:pwd?", remove); export default router; \ No newline at end of file diff --git a/static/js/home.js b/static/js/home.js index 6f61b05..b1b7225 100644 --- a/static/js/home.js +++ b/static/js/home.js @@ -1171,7 +1171,7 @@ class QBinHome { return; } try { - const response = await fetch(`/d/${fkey}/${pwd}`, { + const response = await fetch(`/delete/${fkey}/${pwd}`, { method: 'DELETE', credentials: 'include' }); diff --git a/static/js/render.js b/static/js/render.js index 20cbf00..4150e28 100644 --- a/static/js/render.js +++ b/static/js/render.js @@ -609,7 +609,7 @@ class QBinViewer { } async handleDelete() { - const path = `/d/${this.currentPath.key}/${this.currentPath.pwd}`; + const path = `/delete/${this.currentPath.key}/${this.currentPath.pwd}`; try { const response = await fetch(path, {method: 'DELETE'}); if (response.ok) { diff --git a/static/js/service-worker.js b/static/js/service-worker.js index 5ea825c..0fa20a5 100644 --- a/static/js/service-worker.js +++ b/static/js/service-worker.js @@ -4,7 +4,7 @@ */ // 缓存配置 -const CACHE_VERSION = 'v1.52'; +const CACHE_VERSION = 'v1.53'; const STATIC_CACHE_NAME = `qbin-static-${CACHE_VERSION}`; const DYNAMIC_CACHE_NAME = `qbin-dynamic-${CACHE_VERSION}`; const CDN_CACHE_NAME = `qbin-cdn-${CACHE_VERSION}`; diff --git a/static/js/utils.js b/static/js/utils.js index 6d1b230..9c1c8f3 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -462,7 +462,7 @@ const API = { "Content-Type": mimetype, }; - const response = await this.fetchNet(`/s/${key}/${pwd}`, { + const response = await this.fetchNet(`/save/${key}/${pwd}`, { method, body: content, headers From 1dd8bd90750a64fc802f4ee92f6aba19fe6058b1 Mon Sep 17 00:00:00 2001 From: naihe <239144498@qq.com> Date: Sun, 20 Apr 2025 22:50:04 +0800 Subject: [PATCH 2/6] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=EF=BC=8C=E5=90=8C=E6=AD=A5=E6=9C=80=E6=96=B0?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E7=89=B9=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/constants.ts | 2 +- src/controllers/paste.controller.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/config/constants.ts b/src/config/constants.ts index a3d0afd..cb943b9 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -7,7 +7,7 @@ import {get_env} from "./env.ts"; export const TOKEN_EXPIRE = parseInt(get_env("TOKEN_EXPIRE", "31536000")); // JWT 过期时长(秒) export const MAX_UPLOAD_FILE_SIZE = parseInt(get_env("MAX_UPLOAD_FILE_SIZE", "52428800")); // 5MB -export const PASTE_STORE = "qbin"; // KV 命名空间 +export const PASTE_STORE = "qbinv2"; // KV 命名空间 export const CACHE_CHANNEL = "qbin-cache-sync"; export const jwtSecret = get_env("JWT_SECRET", "input-your-jwtSecret"); // 从环境变量获取jwt密钥 diff --git a/src/controllers/paste.controller.ts b/src/controllers/paste.controller.ts index fd615f3..d32df88 100644 --- a/src/controllers/paste.controller.ts +++ b/src/controllers/paste.controller.ts @@ -45,7 +45,7 @@ export async function getRaw(ctx: Context) { ctx.state.metadata = { etag: full.hash, time: full.time }; ctx.response.headers.set("Pragma", "no-cache"); ctx.response.headers.set("Cache-Control", "no-cache, must-revalidate"); // private , must-revalidate | , max-age=3600 - ctx.response.headers.set("Content-Type", full.mime || full.type); + ctx.response.headers.set("Content-Type", full.mime); ctx.response.headers.set("Content-Length", full.len.toString()); ctx.response.body = full.content; } @@ -64,7 +64,7 @@ export async function queryRaw(ctx: Context) { } ctx.response.status = 200; - ctx.response.headers.set("Content-Type", meta.mime || meta.type); + ctx.response.headers.set("Content-Type", meta.mime); ctx.response.headers.set("Content-Length", meta.len.toString()); } From f862fe5ebc0486b4b1257b7c47adf884f89479ff Mon Sep 17 00:00:00 2001 From: naihe <239144498@qq.com> Date: Sun, 20 Apr 2025 22:55:55 +0800 Subject: [PATCH 3/6] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=EF=BC=8C=E5=90=8C=E6=AD=A5=E6=9C=80=E6=96=B0?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E7=89=B9=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/cache.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/utils/cache.ts b/src/utils/cache.ts index 6accd10..704dc71 100644 --- a/src/utils/cache.ts +++ b/src/utils/cache.ts @@ -28,11 +28,11 @@ export async function isCached(key: string, pwd?: string | undefined, repo): Pro if ("pwd" in memData) return memData; } - const kvResult = await kv.get([PASTE_STORE, key]); - if (!kvResult.value){ - memCache.set(key, {'pwd': "!"}); // 减少内查询 - return null; - } + // const kvResult = await kv.get([PASTE_STORE, key]); + // if (!kvResult.value){ + // memCache.set(key, {'pwd': "!"}); // 减少内查询 + // return null; + // } // 解决pg到kv批量同步问题 if (kvResult.value === true){ @@ -55,11 +55,11 @@ export async function checkCached(key: string, pwd?: string | undefined, repo): if ("content" in memData) return memData; } - const kvResult = await kv.get([PASTE_STORE, key]); - if (!kvResult.value){ - memCache.set(key, {'pwd': "!"}); // 减少内查询 - return null; - } + // const kvResult = await kv.get([PASTE_STORE, key]); + // if (!kvResult.value){ + // memCache.set(key, {'pwd': "!"}); // 减少内查询 + // return null; + // } if (!checkPassword(kvResult.value.pwd, pwd)){ memCache.set(key, {'pwd': kvResult.value.pwd}); // 减少内查询 return null; From 1fde8f41d4a462125c14c0505f141f6d1cd3ca52 Mon Sep 17 00:00:00 2001 From: naihe <239144498@qq.com> Date: Sun, 20 Apr 2025 23:01:46 +0800 Subject: [PATCH 4/6] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=EF=BC=8C=E5=90=8C=E6=AD=A5=E6=9C=80=E6=96=B0?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E7=89=B9=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/constants.ts | 2 +- src/utils/cache.ts | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/config/constants.ts b/src/config/constants.ts index cb943b9..e33a59f 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -7,7 +7,7 @@ import {get_env} from "./env.ts"; export const TOKEN_EXPIRE = parseInt(get_env("TOKEN_EXPIRE", "31536000")); // JWT 过期时长(秒) export const MAX_UPLOAD_FILE_SIZE = parseInt(get_env("MAX_UPLOAD_FILE_SIZE", "52428800")); // 5MB -export const PASTE_STORE = "qbinv2"; // KV 命名空间 +export const PASTE_STORE = "qbinv1"; // KV 命名空间 export const CACHE_CHANNEL = "qbin-cache-sync"; export const jwtSecret = get_env("JWT_SECRET", "input-your-jwtSecret"); // 从环境变量获取jwt密钥 diff --git a/src/utils/cache.ts b/src/utils/cache.ts index 704dc71..6accd10 100644 --- a/src/utils/cache.ts +++ b/src/utils/cache.ts @@ -28,11 +28,11 @@ export async function isCached(key: string, pwd?: string | undefined, repo): Pro if ("pwd" in memData) return memData; } - // const kvResult = await kv.get([PASTE_STORE, key]); - // if (!kvResult.value){ - // memCache.set(key, {'pwd': "!"}); // 减少内查询 - // return null; - // } + const kvResult = await kv.get([PASTE_STORE, key]); + if (!kvResult.value){ + memCache.set(key, {'pwd': "!"}); // 减少内查询 + return null; + } // 解决pg到kv批量同步问题 if (kvResult.value === true){ @@ -55,11 +55,11 @@ export async function checkCached(key: string, pwd?: string | undefined, repo): if ("content" in memData) return memData; } - // const kvResult = await kv.get([PASTE_STORE, key]); - // if (!kvResult.value){ - // memCache.set(key, {'pwd': "!"}); // 减少内查询 - // return null; - // } + const kvResult = await kv.get([PASTE_STORE, key]); + if (!kvResult.value){ + memCache.set(key, {'pwd': "!"}); // 减少内查询 + return null; + } if (!checkPassword(kvResult.value.pwd, pwd)){ memCache.set(key, {'pwd': kvResult.value.pwd}); // 减少内查询 return null; From 451abfbe25a2adef2610946265a0326da0bcdc29 Mon Sep 17 00:00:00 2001 From: naihe <239144498@qq.com> Date: Sun, 20 Apr 2025 23:03:28 +0800 Subject: [PATCH 5/6] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=EF=BC=8C=E5=90=8C=E6=AD=A5=E6=9C=80=E6=96=B0?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E7=89=B9=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/constants.ts b/src/config/constants.ts index e33a59f..a3d0afd 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -7,7 +7,7 @@ import {get_env} from "./env.ts"; export const TOKEN_EXPIRE = parseInt(get_env("TOKEN_EXPIRE", "31536000")); // JWT 过期时长(秒) export const MAX_UPLOAD_FILE_SIZE = parseInt(get_env("MAX_UPLOAD_FILE_SIZE", "52428800")); // 5MB -export const PASTE_STORE = "qbinv1"; // KV 命名空间 +export const PASTE_STORE = "qbin"; // KV 命名空间 export const CACHE_CHANNEL = "qbin-cache-sync"; export const jwtSecret = get_env("JWT_SECRET", "input-your-jwtSecret"); // 从环境变量获取jwt密钥 From af5e0a85f1318f2d217ffb6032a4fdc6ce9ab431 Mon Sep 17 00:00:00 2001 From: naihe <239144498@qq.com> Date: Mon, 21 Apr 2025 09:41:35 +0800 Subject: [PATCH 6/6] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=E7=BC=93=E5=AD=98=E4=BB=A3=E7=A0=81=EF=BC=8C=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E6=B8=85=E7=90=86=E8=BF=87=E6=9C=9F=E7=BC=93=E5=AD=98=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/constants.ts | 7 +- src/controllers/admin.controller.ts | 126 +++++++++++++-------- src/db/repositories/IMetadataRepository.ts | 6 +- src/db/repositories/metadataRepository.ts | 27 ++++- src/routes/admin.routes.ts | 5 +- src/utils/types.ts | 11 ++ 6 files changed, 116 insertions(+), 66 deletions(-) diff --git a/src/config/constants.ts b/src/config/constants.ts index a3d0afd..8438911 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -7,7 +7,7 @@ import {get_env} from "./env.ts"; export const TOKEN_EXPIRE = parseInt(get_env("TOKEN_EXPIRE", "31536000")); // JWT 过期时长(秒) export const MAX_UPLOAD_FILE_SIZE = parseInt(get_env("MAX_UPLOAD_FILE_SIZE", "52428800")); // 5MB -export const PASTE_STORE = "qbin"; // KV 命名空间 +export const PASTE_STORE = "qbinv2"; // KV 命名空间 export const CACHE_CHANNEL = "qbin-cache-sync"; export const jwtSecret = get_env("JWT_SECRET", "input-your-jwtSecret"); // 从环境变量获取jwt密钥 @@ -15,13 +15,12 @@ export const exactPaths = ["/favicon.ico", "/document", "/health", "/login", "/s export const prefixPaths = ['/r/', '/p/', '/static/', '/api/login/'] export const basePath = Deno.cwd(); -// 分布式缓存唯一标识 export const EMAIL = get_env("ADMIN_EMAIL", "admin@qbin.github"); export const PASSWORD = get_env("ADMIN_PASSWORD", "qbin"); export const QBIN_ENV = get_env("QBIN_ENV", "prod"); -// 匹配可用字符 +// 校验访问路径和访问密码字符 export const VALID_CHARS_REGEX = /^[a-zA-Z0-9-\.\_]+$/; -// MIME 类型校验正则 +// 上传 MIME类型校验正则 export const mimeTypeRegex = /^[-\w.+]+\/[-\w.+]+(?:;\s*[-\w]+=[-\w]+)*$/i; // 保留访问路径 export const reservedPaths = new Set( diff --git a/src/controllers/admin.controller.ts b/src/controllers/admin.controller.ts index 071637a..14870e8 100644 --- a/src/controllers/admin.controller.ts +++ b/src/controllers/admin.controller.ts @@ -1,83 +1,77 @@ -import {AppState} from "../utils/types.ts"; +import {AppState, KVMeta} from "../utils/types.ts"; import {kv} from "../utils/cache.ts"; import {EMAIL, QBIN_ENV, PASTE_STORE} from "../config/constants.ts"; import {PasteError, Response} from "../utils/response.ts"; import {ResponseMessages} from "../utils/messages.ts"; import {parsePagination} from "../utils/validator.ts"; import {createMetadataRepository} from "../db/repositories/metadataRepository.ts"; +import {getTimestamp} from "../utils/common.ts"; + export async function syncDBToKV(ctx: Context, repo) { try { - // 从数据库获取所有fkeys - const pgFkeys = await repo.getAllFkeys(); - const pgFkeysSet = new Set(pgFkeys); - - // 追踪同步统计信息 - let added = 0; - let removed = 0; - let unchanged = 0; + const now = getTimestamp(); + const rows = await repo.getActiveMetas(); + const dbMap = new Map>(); + for (const r of rows) { + dbMap.set(r.fkey, { + email: r.email, + name: r.uname, + ip: r.ip, + len: r.len, + expire: r.expire, + hash: r.hash, + pwd: r.pwd, + }); + } - // 处理现有KV条目 - const kvEntries = kv.list({ prefix: [PASTE_STORE] }); - const kvFkeysSet = new Set(); const toRemove = []; + const kvFkeys = new Set(); + let removed = 0, added = 0, unchanged = 0; - // 识别需要删除的fkeys(存在于KV中但不在数据库中) - for await (const entry of kvEntries) { + for await (const entry of kv.list({ prefix: [PASTE_STORE] })) { const fkey = entry.key[1] as string; + const kvVal: { expire?: number } = entry.value ?? {}; - if (!pgFkeysSet.has(fkey)) { - toRemove.push(["qbin", fkey]); + // 1. KV 中条目已过期 2. 不存在于数据库(被删除或已过期) + if ( + (kvVal.expire !== undefined && kvVal.expire <= now) || + !dbMap.has(fkey) + ) { + toRemove.push(entry.key); } else { - kvFkeysSet.add(fkey); + kvFkeys.add(fkey); unchanged++; } } - // 批量删除过期的fkeys - const batchSize = 100; // 根据系统限制优化批次大小 + const batchSize = 100; for (let i = 0; i < toRemove.length; i += batchSize) { - const batch = toRemove.slice(i, i + batchSize); - if (batch.length > 0) { - const atomicOp = kv.atomic(); - for (const key of batch) { - atomicOp.delete(key); - } - await atomicOp.commit(); - removed += batch.length; - } + const atomic = kv.atomic(); + for (const key of toRemove.slice(i, i + batchSize)) atomic.delete(key); + await atomic.commit(); + removed += Math.min(batchSize, toRemove.length - i); } - // 识别需要添加的fkeys(存在于数据库但不在KV中) - const toAdd = []; - for (const fkey of pgFkeys) { - if (!kvFkeysSet.has(fkey)) { - toAdd.push(fkey); - } + const toAdd: [string, ReturnType] [] = []; + for (const [fkey, meta] of dbMap) { + if (!kvFkeys.has(fkey)) toAdd.push([fkey, meta]); } - // 批量添加新fkeys for (let i = 0; i < toAdd.length; i += batchSize) { - const batch = toAdd.slice(i, i + batchSize); - if (batch.length > 0) { - const atomicOp = kv.atomic(); - for (const fkey of batch) { - atomicOp.set(["qbin", fkey], true); - } - await atomicOp.commit(); - added += batch.length; + const atomic = kv.atomic(); + for (const [fkey, meta] of toAdd.slice(i, i + batchSize)) { + atomic.set([PASTE_STORE, fkey], meta); } + await atomic.commit(); + added += Math.min(batchSize, toAdd.length - i); } return new Response(ctx, 200, ResponseMessages.SUCCESS, { - stats: { - added, // 新增的fkey数量 - removed, // 移除的fkey数量 - unchanged, // 保持不变的fkey数量 - total: pgFkeys.length // 总fkey数量 - }}); + stats: { added, removed, unchanged, total: rows.length }, + }); } catch (error) { - console.error("同步数据库到KV时出错:", error); + console.error("同步数据库到 KV 时出错: ", error); throw new PasteError(500, ResponseMessages.SERVER_ERROR); } } @@ -99,3 +93,35 @@ export async function getAllStorage(ctx) { pagination: { total, page, pageSize, totalPages }, }); } + +export async function purgeExpiredCacheEntries(ctx){ + const now = getTimestamp(); + let removed = 0; // 被删除的条目 + let kept = 0; // 保留下来的条目 + const BATCH_SZ = 100; + let batch = kv.atomic(); + let counter = 0; + for await (const { key, value } of kv.list({ prefix: [] })) { + const isPasteStore = key[0] === PASTE_STORE; + const isExpired = isPasteStore && value?.expire && value.expire < now; + if (!isPasteStore || isExpired) { + batch = batch.delete(key); + removed++; + counter++; + if (counter === BATCH_SZ) { + await batch.commit(); + batch = kv.atomic(); + counter = 0; + } + } else { + kept++; + } + } + if (counter) { + await batch.commit(); + } + return new Response(ctx, 200, ResponseMessages.SUCCESS, { + removed, + kept, + }); +} \ No newline at end of file diff --git a/src/db/repositories/IMetadataRepository.ts b/src/db/repositories/IMetadataRepository.ts index 894e182..487a9af 100644 --- a/src/db/repositories/IMetadataRepository.ts +++ b/src/db/repositories/IMetadataRepository.ts @@ -1,13 +1,13 @@ -import { Metadata } from "../../utils/types.ts"; +import {KVMeta, Metadata} from "../../utils/types.ts"; export interface IMetadataRepository { - create(data: Metadata): Promise; + create(data: Metadata): Promise; getByFkey(fkey: string): Promise; list(limit?: number, offset?: number): Promise; update(fkey: string, patch: Partial): Promise; delete(fkey: string): Promise; findByMime(mime: string): Promise; - getAllFkeys(): Promise; + getActiveMetas(): Promise; /** * 获取指定邮箱的全部 fkey */ diff --git a/src/db/repositories/metadataRepository.ts b/src/db/repositories/metadataRepository.ts index db019df..0e89cc3 100644 --- a/src/db/repositories/metadataRepository.ts +++ b/src/db/repositories/metadataRepository.ts @@ -1,5 +1,5 @@ -import { eq, and, gt, sql as dsql, count } from "drizzle-orm"; -import { Metadata } from "../../utils/types.ts"; +import { or, eq, and, gt, sql as dsql, count, isNull } from "drizzle-orm"; +import {KVMeta, Metadata} from "../../utils/types.ts"; import { IMetadataRepository } from "./IMetadataRepository.ts"; import { getDb, SupportedDialect } from "../adapters/index.ts"; import { metadataPg, metadataSqlite } from "../models/metadata.ts"; @@ -92,10 +92,25 @@ class MetadataRepository implements IMetadataRepository { .orderBy(dsql`${this.t.time} DESC`).execute()) as Metadata[]; } - async getAllFkeys() { - const rows = await this.run(() => - this.db.select({ fkey: this.t.fkey }).from(this.t).execute()); - return rows.map((r: { fkey: string }) => r.fkey); + async getActiveMetas(): Promise { + const now = getTimestamp(); + return await this.run(() => + this.db.select({ + fkey: this.t.fkey, + email: this.t.email, + uname: this.t.uname, + ip: this.t.ip, + len: this.t.len, + expire: this.t.expire, + hash: this.t.hash, + pwd: this.t.pwd, + }) + .from(this.t) + .where( + or(isNull(this.t.expire), gt(this.t.expire, now)), + ) + .execute() + ); } /** 获取邮箱全部 fkey */ diff --git a/src/routes/admin.routes.ts b/src/routes/admin.routes.ts index 9a8e356..f57b519 100644 --- a/src/routes/admin.routes.ts +++ b/src/routes/admin.routes.ts @@ -3,7 +3,7 @@ import { Response } from "../utils/response.ts"; import { ResponseMessages } from "../utils/messages.ts"; import {createMetadataRepository} from "../db/repositories/metadataRepository.ts"; import {EMAIL, QBIN_ENV} from "../config/constants.ts"; -import {getAllStorage, syncDBToKV} from "../controllers/admin.controller.ts"; +import {purgeExpiredCacheEntries, getAllStorage, syncDBToKV} from "../controllers/admin.controller.ts"; import {migrateToV2} from "../db/helpers/migrate.ts"; import {get_env} from "../config/env.ts"; @@ -28,7 +28,6 @@ router const {rowCount} = await migrateToV2(repo, get_env("DB_CLIENT", "postgres")); return new Response(ctx, 200, ResponseMessages.SUCCESS, {rowCount: rowCount}); }) - // .post("/api/admin/clean", async (ctx) => { - // }); // 清理过期key + .get("/api/cache/purge", purgeExpiredCacheEntries); // 清理过期缓存 export default router; \ No newline at end of file diff --git a/src/utils/types.ts b/src/utils/types.ts index 1c42bdf..59e5865 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -27,6 +27,17 @@ export interface Metadata { hash: number; } +export interface KVMeta { + fkey: string; + email: string | null; + uname: string | null; + ip: string | null; + len: number; + expire: number; + hash: string; + pwd: string | null; +} + export interface SessionStore extends Map {} export interface AuthUser {