1
0
mirror of https://github.com/ZeroCatDev/ClassworksKV.git synced 2025-07-01 20:09:23 +00:00

Enable static file serving in app.js, implement a batch import endpoint in kv.js for bulk key-value pair uploads, and add a corresponding rate limiter. Enhance the front-end in index.ejs with a form for batch data import and improve styling for better user experience.

This commit is contained in:
SunWuyuan 2025-05-11 12:04:06 +08:00
parent 4a8b19c0b8
commit 810491fd2f
No known key found for this signature in database
GPG Key ID: A6A54CF66F56BB64
4 changed files with 178 additions and 4 deletions

2
app.js
View File

@ -48,7 +48,7 @@ app.use(logger("dev"));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
// app.use(cookieParser());
// app.use(express.static(join(__dirname, "public")));
app.use(express.static(join(__dirname, "public")));
// 添加请求超时处理中间件
app.use((req, res, next) => {

View File

@ -71,10 +71,24 @@ export const authLimiter = rateLimit({
skipFailedRequests: false, // 失败的认证计入限制
});
// 批量操作限速器(比写操作更严格)
export const batchLimiter = rateLimit({
windowMs: 5 * 60 * 1000, // 5分钟
limit: 5, // 每个IP在windowMs时间内最多允许5个批量操作
standardHeaders: "draft-7",
legacyHeaders: false,
message: "批量操作请求过于频繁,请稍后再试",
keyGenerator: getClientIp,
skipSuccessfulRequests: false,
skipFailedRequests: false,
});
// 创建一个路由处理中间件根据HTTP方法应用不同的限速器
export const methodBasedRateLimiter = (req, res, next) => {
// 根据HTTP方法应用不同限速
if (req.method === "GET") {
// 检查是否是批量导入路由
if (req.method === "POST" && req.path.endsWith("/batch-import")) {
return batchLimiter(req, res, next);
} else if (req.method === "GET") {
// 读操作使用普通API限速
return apiLimiter(req, res, next);
} else if (

View File

@ -142,6 +142,58 @@ router.post(
})
);
/**
* POST /:namespace/batch-import
* 批量导入键值对到指定命名空间
*/
router.post(
"/:namespace/import/batch-import",
checkRestrictedUUID,
errors.catchAsync(async (req, res, next) => {
const { namespace } = req.params;
const data = req.body;
if (!data || Object.keys(data).length === 0) {
return next(errors.createError(400, "请提供有效的JSON数据格式为 {\"key\":{}, \"key2\":{}}"));
}
// 获取客户端IP
const creatorIp =
req.headers["x-forwarded-for"] ||
req.connection.remoteAddress ||
req.socket.remoteAddress ||
req.connection.socket?.remoteAddress ||
"";
const results = [];
const errors = [];
// 批量处理所有键值对
for (const [key, value] of Object.entries(data)) {
try {
const result = await kvStore.upsert(namespace, key, value, creatorIp);
results.push({
key: result.key,
created: result.createdAt.getTime() === result.updatedAt.getTime()
});
} catch (error) {
errors.push({
key,
error: error.message
});
}
}
return res.status(200).json({
namespace,
total: Object.keys(data).length,
successful: results.length,
failed: errors.length,
results,
errors: errors.length > 0 ? errors : undefined
});
})
);
/**
* DELETE /:namespace
* 删除指定命名空间及其所有键值对
@ -201,4 +253,5 @@ router.get(
})
);
export default router;

View File

@ -7,7 +7,51 @@
<title>
<%= readmeValue.title || "Classworks 服务端" %>
</title>
<link rel="stylesheet" href="/stylesheets/style.css">
<style>
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
max-width: 1000px;
margin: 0 auto;
padding: 20px;
}
.readme-content {
background-color: #f5f5f5;
padding: 20px;
border-radius: 5px;
margin-bottom: 20px;
}
.form-section {
background-color: #f0f8ff;
padding: 20px;
border-radius: 5px;
margin-top: 30px;
}
textarea {
width: 100%;
height: 200px;
margin-bottom: 10px;
font-family: monospace;
}
button {
padding: 10px 15px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #45a049;
}
#response {
white-space: pre-wrap;
background-color: #f5f5f5;
padding: 15px;
border-radius: 5px;
margin-top: 20px;
font-family: monospace;
}
</style>
</head>
<body>
@ -19,6 +63,69 @@
<pre><%= readmeValue.readme %></pre>
</div>
<div class="form-section">
<h2>批量数据导入测试</h2>
<form id="batchImportForm">
<div>
<label for="namespace">命名空间:</label>
<input type="text" id="namespace" name="namespace" placeholder="输入命名空间" required>
<button type="button" id="generateUUID">生成UUID</button>
</div>
<div>
<label for="data">JSON数据 (格式: {"key":{}, "key2":{}})</label>
<textarea id="data" name="data" placeholder='{"key1": {"value": "test1"}, "key2": {"value": "test2"}}' required></textarea>
</div>
<button type="submit">导入数据</button>
</form>
<div>
<h3>响应结果:</h3>
<pre id="response"></pre>
</div>
</div>
<script>
document.getElementById('generateUUID').addEventListener('click', async function() {
try {
const response = await fetch('/uuid', {
method: 'GET'
});
const data = await response.json();
document.getElementById('namespace').value = data.namespace;
} catch (error) {
console.error('Error:', error);
document.getElementById('response').textContent = '获取UUID失败: ' + error.message;
}
});
document.getElementById('batchImportForm').addEventListener('submit', async function(e) {
e.preventDefault();
const namespace = document.getElementById('namespace').value;
let jsonData;
try {
jsonData = JSON.parse(document.getElementById('data').value);
} catch (error) {
document.getElementById('response').textContent = 'JSON解析错误: ' + error.message;
return;
}
try {
const response = await fetch(`/${namespace}/batch-import`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(jsonData)
});
const result = await response.json();
document.getElementById('response').textContent = JSON.stringify(result, null, 2);
} catch (error) {
console.error('Error:', error);
document.getElementById('response').textContent = '请求失败: ' + error.message;
}
});
</script>
</body>
</html>