Files
237899745 b135987fe8 feat: 添加多项功能和修复
功能新增:
- OSS 存储使用情况显示(文件页面)
- OSS 当日流量统计(阿里云云监控API)
- 分享页面路由修复(/s/xxx 格式支持)

Bug修复:
- 修复分享页面资源路径(相对路径改绝对路径)
- 修复分享码获取逻辑(支持路径格式)
- 修复OSS配额undefined显示问题
- 修复登录流程OSS配置检查
- 修复文件数为null时的显示问题

依赖更新:
- 添加 @alicloud/cms20190101 云监控SDK
- 添加 @alicloud/openapi-client

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 21:04:22 +08:00

397 lines
10 KiB
JavaScript

'use strict';
const zlib = require('zlib');
const http = require('http');
const https = require('https');
const parse = require('url').parse;
const format = require('url').format;
const debugBody = require('debug')('httpx:body');
const debugHeader = require('debug')('httpx:header');
const httpAgent = new http.Agent({ keepAlive: true });
const httpsAgent = new https.Agent({ keepAlive: true });
const TIMEOUT = 3000; // 3s
const READ_TIMER = Symbol('TIMER::READ_TIMER');
const READ_TIME_OUT = Symbol('TIMER::READ_TIME_OUT');
const READ_TIMER_START_AT = Symbol('TIMER::READ_TIMER_START_AT');
/**
* Check the content-encoding header, and auto decompress it.
* @param {Readable} response http response
* @returns Readable
*/
function decompress(response) {
switch (response.headers['content-encoding']) {
// or, just use zlib.createUnzip() to handle both cases
case 'gzip':
return response.pipe(zlib.createGunzip());
case 'deflate':
return response.pipe(zlib.createInflate());
default:
return response;
}
}
var append = function (err, name, message) {
err.name = name + err.name;
err.message = `${message}. ${err.message}`;
return err;
};
const isNumber = function (num) {
return num !== null && !isNaN(num);
};
exports.request = function (url, opts) {
opts || (opts = {});
const parsed = typeof url === 'string' ? parse(url) : url;
let readTimeout, connectTimeout;
if (isNumber(opts.readTimeout) || isNumber(opts.connectTimeout)) {
readTimeout = isNumber(opts.readTimeout) ? Number(opts.readTimeout) : TIMEOUT;
connectTimeout = isNumber(opts.connectTimeout) ? Number(opts.connectTimeout) : TIMEOUT;
} else if (isNumber(opts.timeout)) {
readTimeout = connectTimeout = Number(opts.timeout);
} else {
readTimeout = connectTimeout = TIMEOUT;
}
const isHttps = parsed.protocol === 'https:';
const method = (opts.method || 'GET').toUpperCase();
const defaultAgent = isHttps ? httpsAgent : httpAgent;
const agent = opts.agent || defaultAgent;
var options = {
host: parsed.hostname || 'localhost',
path: parsed.path || '/',
method: method,
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
agent: agent,
headers: opts.headers || {},
// ssl config
key: opts.key || '',
cert: opts.cert || '',
ca: opts.ca || '',
// connect timerout
timeout: connectTimeout
};
if (isHttps && typeof opts.rejectUnauthorized !== 'undefined') {
options.rejectUnauthorized = opts.rejectUnauthorized;
}
if (opts.compression) {
options.headers['accept-encoding'] = 'gzip,deflate';
}
const httplib = isHttps ? https : http;
if (typeof opts.beforeRequest === 'function') {
options = opts.beforeRequest(options);
}
return new Promise((resolve, reject) => {
const request = httplib.request(options);
const body = opts.data;
var fulfilled = (response) => {
if (debugHeader.enabled) {
const requestHeaders = response.req._header;
requestHeaders.split('\r\n').forEach((line) => {
debugHeader('> %s', line);
});
debugHeader('< HTTP/%s %s %s', response.httpVersion, response.statusCode, response.statusMessage);
Object.keys(response.headers).forEach((key) => {
debugHeader('< %s: %s', key, response.headers[key]);
});
}
resolve(response);
};
var rejected = (err) => {
err.message += `${method} ${format(parsed)} failed.`;
// clear response timer when error
if (request.socket && request.socket[READ_TIMER]) {
clearTimeout(request.socket[READ_TIMER]);
}
reject(err);
};
var abort = (err) => {
request.abort();
rejected(err);
};
const startResponseTimer = function (socket) {
const timer = setTimeout(() => {
if (socket[READ_TIMER]) {
clearTimeout(socket[READ_TIMER]);
socket[READ_TIMER] = null;
}
var err = new Error();
var message = `ReadTimeout(${readTimeout})`;
abort(append(err, 'RequestTimeout', message));
}, readTimeout);
// start read-timer
socket[READ_TIME_OUT] = readTimeout;
socket[READ_TIMER] = timer;
socket[READ_TIMER_START_AT] = Date.now();
// don't block the loop
timer.unref();
};
// string
if (!body || 'string' === typeof body || body instanceof Buffer) {
if (debugBody.enabled) {
if (!body) {
debugBody('<no request body>');
} else if ('string' === typeof body) {
debugBody(body);
} else {
debugBody(`Buffer <ignored>, Buffer length: ${body.length}`);
}
}
request.end(body);
} else if ('function' === typeof body.pipe) { // stream
body.pipe(request);
if (debugBody.enabled) {
debugBody('<request body is a stream>');
}
body.once('error', (err) => {
abort(append(err, 'HttpX', 'Stream occor error'));
});
}
request.on('timeout', () => {
var err = new Error();
err.name = 'RequestTimeoutError';
err.message = `ConnectTimeout: Connect ${url} failed.`;
request.destroy();
reject(err);
});
request.on('response', fulfilled);
request.on('error', rejected);
request.once('socket', function (socket) {
// reuse socket
if (socket.readyState === 'opening') {
socket.once('connect', function () {
startResponseTimer(socket);
});
} else {
startResponseTimer(socket);
}
});
});
};
exports.read = function (response, encoding) {
const readable = decompress(response);
return new Promise((resolve, reject) => {
// node.js 14 use response.client
const socket = response.socket || response.client;
const makeReadTimeoutError = () => {
const req = response.req;
var err = new Error();
err.name = 'RequestTimeoutError';
err.message = `ReadTimeout: ${socket[READ_TIME_OUT]}. ${req.method} ${req.path} failed.`;
return err;
};
// check read-timer
let readTimer;
const oldReadTimer = socket[READ_TIMER];
if (!oldReadTimer) {
reject(makeReadTimeoutError());
return;
}
const remainTime = socket[READ_TIME_OUT] - (Date.now() - socket[READ_TIMER_START_AT]);
clearTimeout(oldReadTimer);
if (remainTime <= 0) {
reject(makeReadTimeoutError());
return;
}
readTimer = setTimeout(function () {
reject(makeReadTimeoutError());
}, remainTime);
// start reading data
var onError, onData, onEnd;
var cleanup = function () {
// cleanup
readable.removeListener('error', onError);
readable.removeListener('data', onData);
readable.removeListener('end', onEnd);
// clear read timer
if (readTimer) {
clearTimeout(readTimer);
}
};
const bufs = [];
var size = 0;
onData = function (buf) {
bufs.push(buf);
size += buf.length;
};
onError = function (err) {
cleanup();
reject(err);
};
onEnd = function () {
cleanup();
var buff = Buffer.concat(bufs, size);
debugBody('');
if (encoding) {
const result = buff.toString(encoding);
debugBody(result);
return resolve(result);
}
if (debugBody.enabled) {
debugBody(buff.toString());
}
resolve(buff);
};
readable.on('error', onError);
readable.on('data', onData);
readable.on('end', onEnd);
});
};
function readyToRead(readable) {
return new Promise((resolve, reject) => {
var onReadable, onEnd, onError;
var cleanup = function () {
// cleanup
readable.removeListener('error', onError);
readable.removeListener('end', onEnd);
readable.removeListener('readable', onReadable);
};
onReadable = function () {
cleanup();
resolve(false);
};
onEnd = function () {
cleanup();
resolve(true);
};
onError = function (err) {
cleanup();
reject(err);
};
readable.once('readable', onReadable);
readable.once('end', onEnd);
readable.once('error', onError);
});
}
class Event {
constructor(id, event, data, retry) {
this.id = id;
this.event = event;
this.data = data;
this.retry = retry;
}
}
exports.Event = Event;
const DATA_PREFIX = 'data:';
const EVENT_PREFIX = 'event:';
const ID_PREFIX = 'id:';
const RETRY_PREFIX = 'retry:';
function isDigitsOnly(str) {
for (let i = 0; i < str.length; i++) {
const c = str.charAt(i);
if (c < '0' || c > '9') {
return false;
}
}
return str.length > 0;
}
function tryGetEvents(head, chunk) {
const all = head + chunk;
let start = 0;
const events = [];
for (let i = 0; i < all.length - 1; i++) {
const c = all[i];
const c2 = all[i + 1];
if (c === '\n' && c2 === '\n') {
const part = all.substring(start, i);
const lines = part.split('\n');
const event = new Event();
lines.forEach((line) => {
if (line.startsWith(DATA_PREFIX)) {
event.data = line.substring(DATA_PREFIX.length).trim();
} else if (line.startsWith(EVENT_PREFIX)) {
event.event = line.substring(EVENT_PREFIX.length).trim();
} else if (line.startsWith(ID_PREFIX)) {
event.id = line.substring(ID_PREFIX.length).trim();
} else if (line.startsWith(RETRY_PREFIX)) {
const retry = line.substring(RETRY_PREFIX.length).trim();
if (isDigitsOnly(retry)) {
event.retry = parseInt(retry, 10);
}
} else if (line.startsWith(':')) {
// ignore the line
}
});
events.push(event);
start = i + 2;
}
}
const rest = all.substring(start);
return [events, rest];
}
/**
* consume response and parse to event stream
* @param {ReadableStream} response
* @returns AsyncGenerator<Event, void, unknown>
*/
exports.readAsSSE = async function* (response) {
const readable = decompress(response);
const socket = response.socket || response.client;
clearTimeout(socket[READ_TIMER]);
let rest = '';
while (true) {
const ended = await readyToRead(readable);
if (ended) {
return;
}
let chunk;
while (null !== (chunk = readable.read())) {
const [ events, remain ] = tryGetEvents(rest, chunk.toString());
rest = remain;
if (events && events.length > 0) {
for (const event of events) {
yield event;
}
}
}
}
};