Skip to content

Commit bd2cfbd

Browse files
committed
refactor(core): 重组 API 响应处理和请求解析
- 引入专用的 DatabaseService 类用于 SQLite 操作 - 增强请求体解析以支持多种内容类型 - 实现统一的响应辅助方法以保证 API 输出一致性 - 通过正确的 URL 构造器检查改进 URL 验证 - 重构代理逻辑,改进头部和路径管理 - 优化工具函数以改善代码组织
1 parent fc1a582 commit bd2cfbd

3 files changed

Lines changed: 260 additions & 170 deletions

File tree

workers/src/api.js

Lines changed: 56 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,15 @@ const api = new Hono();
55
async function GetReqJson(request) {
66
let data = {};
77
try {
8-
data = (await request.json()) || {};
8+
const contentType = request.headers.get('Content-Type') || '';
9+
if (contentType.includes('application/json')) {
10+
data = (await request.json()) || {};
11+
} else if (contentType.includes('application/x-www-form-urlencoded')) {
12+
const formData = await request.formData();
13+
data = Object.fromEntries(formData);
14+
}
915
} catch (e) {
10-
console.log(e.message);
16+
console.error('Failed to parse request body:', e.message);
1117
}
1218
return data;
1319
}
@@ -19,12 +25,33 @@ export default class ControllerAPI {
1925
this.utils = utils;
2026
}
2127

28+
// Response helper functions
29+
createResponse(code, msg, data = null, status = 200, headers = {}) {
30+
return Response.json({ code, msg, data }, { status, headers });
31+
}
32+
33+
createErrorResponse(code, msg, status = 400) {
34+
return this.createResponse(code, msg, null, status);
35+
}
36+
37+
createSuccessResponse(data = null, msg = 'Success') {
38+
return this.createResponse(0, msg, data);
39+
}
40+
41+
createCookieResponse(code, msg, data, cookieName, cookieValue, expires = null) {
42+
const cookieString = expires
43+
? `${cookieName}=${cookieValue}; path=/; expires=${new Date(expires).toUTCString()}`
44+
: `${cookieName}=${cookieValue}; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`;
45+
46+
return this.createResponse(code, msg, data, 200, { 'Set-Cookie': cookieString });
47+
}
48+
2249
async Gateway() {
2350
const { request } = this.utils;
2451
const url = new URL(request.url);
2552
const action = String(url.searchParams.get('action'));
2653
if (!this[action]) {
27-
return Response.json({ code: 1000, msg: 'Invalid action.', data: null }, { status: 404 });
54+
return this.createErrorResponse(1000, 'Invalid action.', 404);
2855
}
2956
return await this[action]();
3057
}
@@ -38,48 +65,31 @@ export default class ControllerAPI {
3865
const token = await SHA256(JSON.stringify([Math.random(), time]));
3966
const expires = time + 86400000;
4067
await STORE.put('token', JSON.stringify({ token, expires }));
41-
return Response.json(
42-
{ code: 0, msg: 'Success', data: token },
43-
{
44-
headers: {
45-
'Set-Cookie': `token=${token}; path=/; expires=${new Date(expires).toUTCString()}`,
46-
},
47-
}
48-
);
68+
return this.createCookieResponse(0, 'Success', token, 'token', token, expires);
4969
}
50-
return Response.json({ code: 1001, msg: 'Password error.', data: null }, { status: 401 });
70+
return this.createErrorResponse(1001, 'Password error.', 401);
5171
}
5272

5373
// logout
5474
async logout() {
5575
const { STORE } = this.utils;
5676
await STORE.delete('token');
57-
return Response.json(
58-
{ code: 0, msg: 'Success', data: null },
59-
{
60-
headers: {
61-
'Set-Cookie': `token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`,
62-
},
63-
}
64-
);
77+
return this.createCookieResponse(0, 'Success', null, 'token', '');
6578
}
6679

6780
// generate a random slug
6881
async randomize() {
69-
return Response.json({ code: 0, msg: 'Success', data: await this.utils.Slug() }, { status: 200 });
82+
return this.createSuccessResponse(await this.utils.Slug());
7083
}
7184

7285
async check_slug() {
7386
const { request } = this.utils;
7487
const { slug } = await GetReqJson(request);
7588
const obj = await this.utils.ParseFirst(slug);
76-
return Response.json(
77-
{
78-
code: obj.url ? 1040 : 0,
79-
msg: obj.url ? 'Slug already exists.' : 'Success',
80-
data: !!obj.url,
81-
},
82-
{ status: 200 }
89+
return this.createResponse(
90+
obj.url ? 1040 : 0,
91+
obj.url ? 'Slug already exists.' : 'Success',
92+
!!obj.url
8393
);
8494
}
8595

@@ -90,8 +100,8 @@ export default class ControllerAPI {
90100
let { url, slug, creation } = body;
91101

92102
// check if url is valid
93-
if (!(await CheckURL(url))) {
94-
return Response.json({ code: 1050, msg: 'Invalid URL.', data: null }, { status: 400 });
103+
if (!CheckURL(url)) {
104+
return this.createErrorResponse(1050, 'Invalid URL.');
95105
}
96106

97107
// if slug is not provided, generate one
@@ -101,17 +111,17 @@ export default class ControllerAPI {
101111
// check if slug already exists
102112
const { url: existed } = await this.utils.ParseFirst(slug);
103113
if (existed) {
104-
return Response.json({ code: 1051, msg: 'Slug already exists.', data: null }, { status: 400 });
114+
return this.createErrorResponse(1051, 'Slug already exists.');
105115
}
106116
}
107117

108118
// save url
109119
const { success, meta: details } = await STORE.put(slug, JSON.stringify(body));
110-
return Response.json({
111-
code: 0,
112-
msg: success ? 'Success' : JSON.stringify(details),
113-
data: success ? slug : null,
114-
});
120+
return this.createResponse(
121+
success ? 0 : 1052,
122+
success ? 'Success' : JSON.stringify(details),
123+
success ? slug : null
124+
);
115125
}
116126

117127
// get all slugs
@@ -150,11 +160,11 @@ export default class ControllerAPI {
150160
}
151161

152162
// 直接返回数据库中的结果,点击数已经通过Counter函数保持最新
153-
return Response.json({
154-
code: success ? 0 : 1052,
155-
msg: 'Success',
156-
data: { results: filteredResults, count: filteredCount, rows, page },
157-
});
163+
return this.createResponse(
164+
success ? 0 : 1052,
165+
'Success',
166+
{ results: filteredResults, count: filteredCount, rows, page }
167+
);
158168
}
159169

160170
// delete a slug
@@ -163,11 +173,11 @@ export default class ControllerAPI {
163173
let body = await GetReqJson(request);
164174
let { slug } = body;
165175
const { success, meta: details } = await STORE.delete(slug);
166-
return Response.json({
167-
code: success ? 0 : 1060,
168-
msg: success ? 'Success' : JSON.stringify(details),
169-
data: null,
170-
});
176+
return this.createResponse(
177+
success ? 0 : 1060,
178+
success ? 'Success' : JSON.stringify(details),
179+
null
180+
);
171181
}
172182
}
173183

workers/src/utils.js

Lines changed: 97 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,60 @@
1+
// Database service class
2+
class DatabaseService {
3+
constructor(sqlite) {
4+
this.db = sqlite;
5+
this.init();
6+
}
7+
8+
init() {
9+
this.db.prepare(
10+
"CREATE TABLE IF NOT EXISTS slug (key TEXT PRIMARY KEY, value TEXT, creation INTEGER DEFAULT (strftime('%s', 'now')))"
11+
).run();
12+
}
13+
14+
async value(key) {
15+
const stmt = this.db.prepare('SELECT value FROM slug WHERE key = ? LIMIT 1').bind(key);
16+
return await stmt.first('value');
17+
}
18+
19+
async count({ where = '1=1' } = {}) {
20+
const stmt = this.db.prepare(`SELECT COUNT(*) as count FROM slug WHERE key <> 'token' AND ${where}`);
21+
return await stmt.first('count');
22+
}
23+
24+
async get({ where = '1=1', orderby = 'creation', rows = 10, page = 1 } = {}) {
25+
const offset = Math.max(page - 1, 0) * rows;
26+
const stmt = this.db.prepare(`SELECT * FROM slug WHERE key <> 'token' AND ${where} ORDER BY ${orderby} DESC LIMIT ${rows} OFFSET ${offset}`);
27+
return await stmt.all();
28+
}
29+
30+
async put(key, value) {
31+
const existingValue = await this.value(key);
32+
let stmt;
33+
if (existingValue) {
34+
stmt = this.db.prepare('UPDATE slug SET value = ?1 WHERE key = ?2').bind(value, key);
35+
} else {
36+
stmt = this.db.prepare('INSERT INTO slug (key, value) VALUES (?1, ?2)').bind(key, value);
37+
}
38+
return await stmt.run();
39+
}
40+
41+
async delete(key) {
42+
const stmt = this.db.prepare('DELETE FROM slug WHERE key = ?').bind(key);
43+
return await stmt.run();
44+
}
45+
}
46+
147
export default class Utils {
248
request = {};
349
env = {};
450
PASSWORD = '';
5-
STORE = new (function () {})();
51+
STORE = null;
652

753
constructor(request, env) {
854
this.request = request;
9-
this.env = env; // 保存环境变量引用
55+
this.env = env;
1056
this.PASSWORD = env.PASSWORD;
11-
this.STORE = new (function (SQL) {
12-
let stmt = null;
13-
14-
// 创建主表
15-
SQL.prepare(
16-
"CREATE TABLE IF NOT EXISTS slug (key TEXT PRIMARY KEY, value TEXT, creation INTEGER DEFAULT (strftime('%s', 'now')))"
17-
).run();
18-
19-
this.value = async (key) => {
20-
stmt = SQL.prepare('SELECT value FROM slug WHERE key = ? LIMIT 1').bind(key);
21-
const val = await stmt.first('value');
22-
return val;
23-
};
24-
this.count = async ({ where }) => {
25-
where = where || '1=1';
26-
stmt = SQL.prepare(`SELECT COUNT(*) as count FROM slug WHERE key <> 'token' AND ${where}`);
27-
const val = await stmt.first('count');
28-
return val;
29-
};
30-
this.get = async ({ where, orderby, rows, page }) => {
31-
where = where || '1=1';
32-
orderby = orderby || 'creation';
33-
rows = rows || 10;
34-
page = Math.max(page - 1, 0) * rows;
35-
stmt = SQL.prepare(`SELECT * FROM slug WHERE key <> 'token' AND ${where} ORDER BY ${orderby} DESC LIMIT ${rows} OFFSET ${page}`);
36-
return await stmt.all();
37-
};
38-
this.put = async (key, value) => {
39-
if (await this.value(key)) {
40-
stmt = SQL.prepare('UPDATE slug SET value = ?1 WHERE key = ?2').bind(value, key);
41-
} else {
42-
stmt = SQL.prepare('INSERT INTO slug (key, value) VALUES (?1, ?2)').bind(key, value);
43-
}
44-
return await stmt.run();
45-
};
46-
this.delete = async (key) => {
47-
stmt = await SQL.prepare('DELETE FROM slug WHERE key = ?').bind(key);
48-
return await stmt.run();
49-
};
50-
})(env.SQLITE);
57+
this.STORE = new DatabaseService(env.SQLITE);
5158
}
5259

5360
async SHA256(text) {
@@ -87,12 +94,11 @@ export default class Utils {
8794
return value;
8895
}
8996

90-
async CheckURL(url) {
91-
const exp = new RegExp(/http(s)?:\/\/([\w-]+\.)+[\w-]+(\/[\w- .\/?%&=]*)?/);
92-
if (exp.test(url) == true) {
93-
if (url[0] == 'h') return true;
94-
else return false;
95-
} else {
97+
CheckURL(url) {
98+
try {
99+
const urlObj = new URL(url);
100+
return ['http:', 'https:'].includes(urlObj.protocol);
101+
} catch {
96102
return false;
97103
}
98104
}
@@ -111,4 +117,47 @@ export default class Utils {
111117
async ParseFirst(key) {
112118
return this.Parse(await this.STORE.value(key), {});
113119
}
120+
121+
// HTTP and URL utility functions
122+
static buildUrlWithPath(baseUrl, additionalPath) {
123+
if (!additionalPath) return baseUrl;
124+
return baseUrl.endsWith('/') ? baseUrl + additionalPath.substring(1) : baseUrl + additionalPath;
125+
}
126+
127+
static createDynamicScript(mode, url, notes = '') {
128+
return `window.__PAGE__ = '${mode}'; window.__URL__ = '${url}'; window.__NOTES__ = ${JSON.stringify(notes)};`;
129+
}
130+
131+
static async fetchWithOptions(url, options = {}) {
132+
try {
133+
const response = await fetch(url, options);
134+
const isHtml = response.headers.get('Content-Type')?.includes('text/html');
135+
136+
return {
137+
response,
138+
isHtml,
139+
text: async () => await response.text(),
140+
success: response.ok
141+
};
142+
} catch (error) {
143+
return {
144+
response: null,
145+
isHtml: false,
146+
text: async () => '',
147+
success: false,
148+
error
149+
};
150+
}
151+
}
152+
153+
static isHtmlResponse(response) {
154+
return response?.headers.get('Content-Type')?.includes('text/html') || false;
155+
}
156+
157+
static cleanProxyHeaders(headers) {
158+
const cleanHeaders = new Headers(headers);
159+
cleanHeaders.delete('content-encoding');
160+
cleanHeaders.delete('content-length');
161+
return cleanHeaders;
162+
}
114163
}

0 commit comments

Comments
 (0)