Compare commits
2 Commits
a0f0f65d20
...
158dd9e85c
| Author | SHA1 | Date | |
|---|---|---|---|
| 158dd9e85c | |||
| 82e3392b36 |
12
package.json
12
package.json
@@ -49,7 +49,7 @@
|
||||
"dependencies": {
|
||||
"@kevisual/ai": "^0.0.24",
|
||||
"@kevisual/auth": "^2.0.3",
|
||||
"@kevisual/query": "^0.0.38",
|
||||
"@kevisual/query": "^0.0.39",
|
||||
"@types/busboy": "^1.5.4",
|
||||
"@types/send": "^1.2.1",
|
||||
"@types/ws": "^8.18.1",
|
||||
@@ -77,16 +77,16 @@
|
||||
"@kevisual/file-listener": "^0.0.2",
|
||||
"@kevisual/local-app-manager": "0.1.32",
|
||||
"@kevisual/logger": "^0.0.4",
|
||||
"@kevisual/oss": "0.0.18",
|
||||
"@kevisual/permission": "^0.0.3",
|
||||
"@kevisual/router": "0.0.65",
|
||||
"@kevisual/oss": "0.0.19",
|
||||
"@kevisual/permission": "^0.0.4",
|
||||
"@kevisual/router": "0.0.66",
|
||||
"@kevisual/types": "^0.0.12",
|
||||
"@kevisual/use-config": "^1.0.28",
|
||||
"@kevisual/use-config": "^1.0.30",
|
||||
"@types/archiver": "^7.0.0",
|
||||
"@types/bun": "^1.3.8",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/node": "^25.1.0",
|
||||
"@types/node": "^25.2.0",
|
||||
"@types/pg": "^8.16.0",
|
||||
"@types/semver": "^7.7.1",
|
||||
"@types/xml2js": "^0.4.14",
|
||||
|
||||
88
pnpm-lock.yaml
generated
88
pnpm-lock.yaml
generated
@@ -19,8 +19,8 @@ importers:
|
||||
specifier: ^2.0.3
|
||||
version: 2.0.3
|
||||
'@kevisual/query':
|
||||
specifier: ^0.0.38
|
||||
version: 0.0.38
|
||||
specifier: ^0.0.39
|
||||
version: 0.0.39
|
||||
'@types/busboy':
|
||||
specifier: ^1.5.4
|
||||
version: 1.5.4
|
||||
@@ -98,20 +98,20 @@ importers:
|
||||
specifier: ^0.0.4
|
||||
version: 0.0.4
|
||||
'@kevisual/oss':
|
||||
specifier: 0.0.18
|
||||
version: 0.0.18
|
||||
specifier: 0.0.19
|
||||
version: 0.0.19
|
||||
'@kevisual/permission':
|
||||
specifier: ^0.0.3
|
||||
version: 0.0.3
|
||||
specifier: ^0.0.4
|
||||
version: 0.0.4
|
||||
'@kevisual/router':
|
||||
specifier: 0.0.65
|
||||
version: 0.0.65(typescript@5.9.3)
|
||||
specifier: 0.0.66
|
||||
version: 0.0.66(typescript@5.9.3)
|
||||
'@kevisual/types':
|
||||
specifier: ^0.0.12
|
||||
version: 0.0.12
|
||||
'@kevisual/use-config':
|
||||
specifier: ^1.0.28
|
||||
version: 1.0.28(dotenv@17.2.3)
|
||||
specifier: ^1.0.30
|
||||
version: 1.0.30(dotenv@17.2.3)
|
||||
'@types/archiver':
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.0
|
||||
@@ -125,8 +125,8 @@ importers:
|
||||
specifier: ^9.0.10
|
||||
version: 9.0.10
|
||||
'@types/node':
|
||||
specifier: ^25.1.0
|
||||
version: 25.1.0
|
||||
specifier: ^25.2.0
|
||||
version: 25.2.0
|
||||
'@types/pg':
|
||||
specifier: ^8.16.0
|
||||
version: 8.16.0
|
||||
@@ -876,15 +876,21 @@ packages:
|
||||
'@kevisual/logger@0.0.4':
|
||||
resolution: {integrity: sha512-+fpr92eokSxoGOW1SIRl/27lPuO+zyY+feR5o2Q4YCNlAdt2x64NwC/w8r/3NEC5QenLgd4K0azyKTI2mHbARw==}
|
||||
|
||||
'@kevisual/oss@0.0.18':
|
||||
resolution: {integrity: sha512-vTdXe41inq4oc+bfYIR3xMDm8GZyOAaWq3DBh+Eur9uNOJcIUdgZBVPOm2uSigmjl3PvqekUw8bE/vbWWJAY7w==}
|
||||
'@kevisual/oss@0.0.19':
|
||||
resolution: {integrity: sha512-4Y5krJTqLQOsEwJf7K7a/88t9YHm8PQNuZ5SJDTMopYDOflJlwVjvqiu0lapQ0UrpI+wG6FdfmdmnWpXdQsa1Q==}
|
||||
|
||||
'@kevisual/permission@0.0.3':
|
||||
resolution: {integrity: sha512-8JsA/5O5Ax/z+M+MYpFYdlioHE6jNmWMuFSokBWYs9CCAHNiSKMR01YLkoVDoPvncfH/Y8F5K/IEXRCbptuMNA==}
|
||||
|
||||
'@kevisual/permission@0.0.4':
|
||||
resolution: {integrity: sha512-zwBYPnT/z21W4q2wkklJrxvoYBYWG/+a3iXFDKqXQAnDOcxm/SU1f1N6FQb9KxGKl36/fclVlhxlxqszvKCenQ==}
|
||||
|
||||
'@kevisual/query@0.0.38':
|
||||
resolution: {integrity: sha512-bfvbSodsZyMfwY+1T2SvDeOCKsT/AaIxlVe0+B1R/fNhlg2MDq2CP0L9HKiFkEm+OXrvXcYDMKPUituVUM5J6Q==}
|
||||
|
||||
'@kevisual/query@0.0.39':
|
||||
resolution: {integrity: sha512-3UEPBIvtdykNkrby3hvrgrHdgd17Uq+Pnr4zs+JBzATkU2eKaOqtTUJqdyIEwuySCwzGTxrnlUzWP4tziDQDLQ==}
|
||||
|
||||
'@kevisual/router@0.0.21':
|
||||
resolution: {integrity: sha512-XKTxbNO924cT18UOAGplWErZ+hMze8Y53F2jYCk18v4jsdsvjRho5uXXjJb6HSVsuITMtQR4R3rG0IcM3jkDKQ==}
|
||||
|
||||
@@ -897,14 +903,14 @@ packages:
|
||||
'@kevisual/router@0.0.60':
|
||||
resolution: {integrity: sha512-2v/ZzUstsaq+Uqo+tZX9ys5E+/2erPggCtljv9jTb3NA88ZdHsYUAsd5wUFvLtf9QucpJCzyWEt+InDV/98FKw==}
|
||||
|
||||
'@kevisual/router@0.0.65':
|
||||
resolution: {integrity: sha512-UiGqjLWheDbWOhEBBOSggCnafYFz3tCjLZYDp44ahiyeC2APwFRozz7UYbEq7+amH4Ex1wdqk1AlKmuP7w04og==}
|
||||
'@kevisual/router@0.0.66':
|
||||
resolution: {integrity: sha512-yoiCfKJ8yxrXToh8ud1+/JFqlRexrZmJ0PhofQX3jyfmmyEBQQJFL+2UYewm4FxbG3l7ndBC/NIhu1v5CdwxiQ==}
|
||||
|
||||
'@kevisual/types@0.0.12':
|
||||
resolution: {integrity: sha512-zJXH2dosir3jVrQ6QG4i0+iLQeT9gJ3H+cKXs8ReWboxBSYzUZO78XssVeVrFPsJ33iaAqo4q3DWbSS1dWGn7Q==}
|
||||
|
||||
'@kevisual/use-config@1.0.28':
|
||||
resolution: {integrity: sha512-ngF+LDbjxpXWrZNmnShIKF/jPpAa+ezV+DcgoZIIzHlRnIjE+rr9sLkN/B7WJbiH9C/j1tQXOILY8ujBqILrow==}
|
||||
'@kevisual/use-config@1.0.30':
|
||||
resolution: {integrity: sha512-kPdna0FW/X7D600aMdiZ5UTjbCo6d8d4jjauSc8RMmBwUU6WliFDSPUNKVpzm2BsDX5Nth1IXFPYMqH+wxqAmw==}
|
||||
peerDependencies:
|
||||
dotenv: ^17
|
||||
|
||||
@@ -1408,8 +1414,8 @@ packages:
|
||||
'@types/node@25.0.10':
|
||||
resolution: {integrity: sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==}
|
||||
|
||||
'@types/node@25.1.0':
|
||||
resolution: {integrity: sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA==}
|
||||
'@types/node@25.2.0':
|
||||
resolution: {integrity: sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==}
|
||||
|
||||
'@types/pg@8.16.0':
|
||||
resolution: {integrity: sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==}
|
||||
@@ -3872,7 +3878,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@kevisual/auth': 1.0.5
|
||||
'@kevisual/router': 0.0.21
|
||||
'@kevisual/use-config': 1.0.28(dotenv@17.2.3)
|
||||
'@kevisual/use-config': 1.0.30(dotenv@17.2.3)
|
||||
ioredis: 5.9.2
|
||||
nanoid: 5.1.6
|
||||
pg: 8.18.0
|
||||
@@ -3898,7 +3904,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@kevisual/auth': 1.0.5
|
||||
'@kevisual/router': 0.0.23
|
||||
'@kevisual/use-config': 1.0.28(dotenv@17.2.3)
|
||||
'@kevisual/use-config': 1.0.30(dotenv@17.2.3)
|
||||
ioredis: 5.9.2
|
||||
nanoid: 5.1.6
|
||||
pg: 8.18.0
|
||||
@@ -3937,7 +3943,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@kevisual/code-center-module': 0.0.20(dotenv@17.2.3)
|
||||
'@kevisual/router': 0.0.22
|
||||
'@kevisual/use-config': 1.0.28(dotenv@17.2.3)
|
||||
'@kevisual/use-config': 1.0.30(dotenv@17.2.3)
|
||||
cookie: 1.1.1
|
||||
dayjs: 1.11.19
|
||||
formidable: 3.5.4
|
||||
@@ -3971,14 +3977,20 @@ snapshots:
|
||||
|
||||
'@kevisual/logger@0.0.4': {}
|
||||
|
||||
'@kevisual/oss@0.0.18': {}
|
||||
'@kevisual/oss@0.0.19': {}
|
||||
|
||||
'@kevisual/permission@0.0.3': {}
|
||||
|
||||
'@kevisual/permission@0.0.4': {}
|
||||
|
||||
'@kevisual/query@0.0.38':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@kevisual/query@0.0.39':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@kevisual/router@0.0.21':
|
||||
dependencies:
|
||||
path-to-regexp: 8.3.0
|
||||
@@ -4001,7 +4013,7 @@ snapshots:
|
||||
dependencies:
|
||||
hono: 4.11.5
|
||||
|
||||
'@kevisual/router@0.0.65(typescript@5.9.3)':
|
||||
'@kevisual/router@0.0.66(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@kevisual/dts': 0.0.3(typescript@5.9.3)
|
||||
hono: 4.11.7
|
||||
@@ -4010,7 +4022,7 @@ snapshots:
|
||||
|
||||
'@kevisual/types@0.0.12': {}
|
||||
|
||||
'@kevisual/use-config@1.0.28(dotenv@17.2.3)':
|
||||
'@kevisual/use-config@1.0.30(dotenv@17.2.3)':
|
||||
dependencies:
|
||||
'@kevisual/load': 0.0.6
|
||||
dotenv: 17.2.3
|
||||
@@ -4562,13 +4574,13 @@ snapshots:
|
||||
|
||||
'@types/busboy@1.5.4':
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
'@types/cookie@0.4.1': {}
|
||||
|
||||
'@types/cors@2.8.17':
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
'@types/crypto-js@4.2.2': {}
|
||||
|
||||
@@ -4581,31 +4593,31 @@ snapshots:
|
||||
'@types/jsonwebtoken@9.0.10':
|
||||
dependencies:
|
||||
'@types/ms': 0.7.34
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
'@types/ms@0.7.34': {}
|
||||
|
||||
'@types/node-forge@1.3.11':
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
'@types/node@25.0.10':
|
||||
dependencies:
|
||||
undici-types: 7.16.0
|
||||
|
||||
'@types/node@25.1.0':
|
||||
'@types/node@25.2.0':
|
||||
dependencies:
|
||||
undici-types: 7.16.0
|
||||
|
||||
'@types/pg@8.16.0':
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
pg-protocol: 1.11.0
|
||||
pg-types: 2.2.0
|
||||
|
||||
'@types/readdir-glob@1.1.5':
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
'@types/resolve@1.20.2': {}
|
||||
|
||||
@@ -4613,13 +4625,13 @@ snapshots:
|
||||
|
||||
'@types/send@1.2.1':
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
'@types/validator@13.12.2': {}
|
||||
|
||||
'@types/ws@8.18.1':
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
'@types/xml2js@0.4.14':
|
||||
dependencies:
|
||||
@@ -4789,7 +4801,7 @@ snapshots:
|
||||
|
||||
bun-types@1.3.8:
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
busboy@1.6.0:
|
||||
dependencies:
|
||||
@@ -4999,7 +5011,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/cookie': 0.4.1
|
||||
'@types/cors': 2.8.17
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
accepts: 1.3.8
|
||||
base64id: 2.0.0
|
||||
cookie: 0.4.2
|
||||
@@ -6291,7 +6303,7 @@ snapshots:
|
||||
|
||||
wkx@0.5.0:
|
||||
dependencies:
|
||||
'@types/node': 25.1.0
|
||||
'@types/node': 25.2.0
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
dependencies:
|
||||
|
||||
@@ -62,7 +62,13 @@ export class User extends Model {
|
||||
oauthUser.orgId = id;
|
||||
}
|
||||
const token = await oauth.generateToken(oauthUser, { type: loginType, hasRefreshToken: true, ...expand });
|
||||
return { accessToken: token.accessToken, refreshToken: token.refreshToken, token: token.accessToken };
|
||||
return {
|
||||
accessToken: token.accessToken,
|
||||
refreshToken: token.refreshToken,
|
||||
token: token.accessToken,
|
||||
accessTokenExpiresAt: token.accessTokenExpiresAt,
|
||||
refreshTokenExpiresAt: token.refreshTokenExpiresAt,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* 验证token
|
||||
|
||||
@@ -70,9 +70,16 @@ interface Store<T> {
|
||||
expire: (key: string, ttl?: number) => Promise<void>;
|
||||
delObject: (value?: T) => Promise<void>;
|
||||
keys: (key?: string) => Promise<string[]>;
|
||||
setToken: (value: { accessToken: string; refreshToken: string; value?: T }, opts?: StoreSetOpts) => Promise<void>;
|
||||
setToken: (value: { accessToken: string; refreshToken: string; value?: T }, opts?: StoreSetOpts) => Promise<TokenData>;
|
||||
delKeys: (keys: string[]) => Promise<number>;
|
||||
}
|
||||
|
||||
type TokenData = {
|
||||
accessToken: string;
|
||||
accessTokenExpiresAt: number;
|
||||
refreshToken?: string;
|
||||
refreshTokenExpiresAt?: number;
|
||||
}
|
||||
export class RedisTokenStore implements Store<OauthUser> {
|
||||
redis: Redis;
|
||||
private prefix: string = 'oauth:';
|
||||
@@ -131,7 +138,7 @@ export class RedisTokenStore implements Store<OauthUser> {
|
||||
await this.del(userPrefix + ':token:' + accessToken);
|
||||
}
|
||||
}
|
||||
async setToken(data: { accessToken: string; refreshToken: string; value?: OauthUser }, opts?: StoreSetOpts) {
|
||||
async setToken(data: { accessToken: string; refreshToken: string; value?: OauthUser }, opts?: StoreSetOpts): Promise<TokenData> {
|
||||
const { accessToken, refreshToken, value } = data;
|
||||
let userPrefix = 'user:' + value?.id;
|
||||
if (value?.orgId) {
|
||||
@@ -163,14 +170,20 @@ export class RedisTokenStore implements Store<OauthUser> {
|
||||
|
||||
await this.set(accessToken, JSON.stringify(value), expire);
|
||||
await this.set(userPrefix + ':token:' + accessToken, accessToken, expire);
|
||||
let refreshTokenExpiresAt = Math.min(expire * 7, 60 * 60 * 24 * 30, 60 * 60 * 24 * 365); // 最大为一年
|
||||
if (refreshToken) {
|
||||
let refreshTokenExpire = Math.min(expire * 7, 60 * 60 * 24 * 30, 60 * 60 * 24 * 365); // 最大为一年
|
||||
// 小于7天, 则设置为7天
|
||||
if (refreshTokenExpire < 60 * 60 * 24 * 7) {
|
||||
refreshTokenExpire = 60 * 60 * 24 * 7;
|
||||
if (refreshTokenExpiresAt < 60 * 60 * 24 * 7) {
|
||||
refreshTokenExpiresAt = 60 * 60 * 24 * 7;
|
||||
}
|
||||
await this.set(refreshToken, JSON.stringify(value), refreshTokenExpire);
|
||||
await this.set(userPrefix + ':refreshToken:' + refreshToken, refreshToken, refreshTokenExpire);
|
||||
await this.set(refreshToken, JSON.stringify(value), refreshTokenExpiresAt);
|
||||
await this.set(userPrefix + ':refreshToken:' + refreshToken, refreshToken, refreshTokenExpiresAt);
|
||||
}
|
||||
return {
|
||||
accessToken,
|
||||
accessTokenExpiresAt: expire,
|
||||
refreshToken,
|
||||
refreshTokenExpiresAt,
|
||||
}
|
||||
}
|
||||
async delKeys(keys: string[]) {
|
||||
@@ -206,10 +219,7 @@ export class OAuth<T extends OauthUser> {
|
||||
async generateToken(
|
||||
user: T,
|
||||
expandOpts?: StoreSetOpts,
|
||||
): Promise<{
|
||||
accessToken: string;
|
||||
refreshToken?: string;
|
||||
}> {
|
||||
): Promise<TokenData> {
|
||||
// 拥有refreshToken 为 true 时,accessToken 为 st_ 开头,refreshToken 为 rk_开头
|
||||
// 意思是secretToken 和 secretKey的缩写
|
||||
const accessToken = expandOpts?.hasRefreshToken ? 'st_' + randomId32() : 'sk_' + randomId64();
|
||||
@@ -227,9 +237,9 @@ export class OAuth<T extends OauthUser> {
|
||||
user.oauthExpand.refreshToken = refreshToken;
|
||||
}
|
||||
}
|
||||
await this.store.setToken({ accessToken, refreshToken, value: user }, expandOpts);
|
||||
const tokenData = await this.store.setToken({ accessToken, refreshToken, value: user }, expandOpts);
|
||||
|
||||
return { accessToken, refreshToken };
|
||||
return tokenData;
|
||||
}
|
||||
async saveSecretKey(oauthUser: T, secretKey: string, opts?: StoreSetOpts) {
|
||||
// 生成一个secretKey
|
||||
|
||||
@@ -98,10 +98,19 @@ const getAiProxy = async (req: IncomingMessage, res: ServerResponse, opts: Proxy
|
||||
return true;
|
||||
}
|
||||
const stat = await oss.statObject(objectName);
|
||||
if (!stat) {
|
||||
createNotFoundPage('Invalid proxy url');
|
||||
if (!stat && isOwner) {
|
||||
// createNotFoundPage('文件不存在');
|
||||
res.writeHead(200, { 'content-type': 'application/json' });
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
code: 404,
|
||||
message: 'object not found',
|
||||
}),
|
||||
);
|
||||
logger.debug('no stat', objectName, owner, req.url);
|
||||
return true;
|
||||
} else if (!stat && !isOwner) {
|
||||
return createNotFoundPage('Invalid ai proxy url');
|
||||
}
|
||||
const permissionInstance = new UserPermission({ permission: stat.metaData as Permission, owner: owner });
|
||||
const checkPermission = permissionInstance.checkPermissionSuccess({
|
||||
@@ -112,6 +121,7 @@ const getAiProxy = async (req: IncomingMessage, res: ServerResponse, opts: Proxy
|
||||
logger.info('no permission', checkPermission, loginUser, owner);
|
||||
return createNotFoundPage('no permission');
|
||||
}
|
||||
|
||||
if (showStat) {
|
||||
res.writeHead(200, { 'content-type': 'application/json' });
|
||||
res.end(
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
import Busboy from 'busboy';
|
||||
import { checkAuth } from '../middleware/auth.ts';
|
||||
import { router, clients, writeEvents } from '../router.ts';
|
||||
import { error } from '../middleware/auth.ts';
|
||||
import fs from 'fs';
|
||||
import { useFileStore } from '@kevisual/use-config';
|
||||
import { app, oss } from '@/app.ts';
|
||||
import { getContentType } from '@/utils/get-content-type.ts';
|
||||
import path from 'path';
|
||||
import { createWriteStream } from 'fs';
|
||||
import crypto from 'crypto';
|
||||
import { pipeBusboy } from '@/modules/fm-manager/index.ts';
|
||||
const cacheFilePath = useFileStore('cache-file', { needExists: true });
|
||||
|
||||
router.post('/api/micro-app/upload', async (req, res) => {
|
||||
if (res.headersSent) return; // 如果响应已发送,不再处理
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
const { tokenUser, token } = await checkAuth(req, res);
|
||||
if (!tokenUser) return;
|
||||
|
||||
// 使用 busboy 解析 multipart/form-data
|
||||
const busboy = Busboy({ headers: req.headers, preservePath: true, defCharset: 'utf-8' });
|
||||
const fields: any = {};
|
||||
let file: any = null;
|
||||
let filePromise: Promise<void> | null = null;
|
||||
let bytesReceived = 0;
|
||||
let bytesExpected = parseInt(req.headers['content-length'] || '0');
|
||||
|
||||
busboy.on('field', (fieldname, value) => {
|
||||
fields[fieldname] = value;
|
||||
});
|
||||
|
||||
busboy.on('file', (fieldname, fileStream, info) => {
|
||||
const { filename, encoding, mimeType } = info;
|
||||
// 处理 UTF-8 文件名编码
|
||||
const decodedFilename = typeof filename === 'string' ? Buffer.from(filename, 'latin1').toString('utf8') : filename;
|
||||
const tempPath = path.join(cacheFilePath, `${Date.now()}-${Math.random().toString(36).substring(7)}`);
|
||||
const writeStream = createWriteStream(tempPath);
|
||||
const hash = crypto.createHash('md5');
|
||||
let size = 0;
|
||||
|
||||
filePromise = new Promise<void>((resolve, reject) => {
|
||||
fileStream.on('data', (chunk) => {
|
||||
bytesReceived += chunk.length;
|
||||
size += chunk.length;
|
||||
hash.update(chunk);
|
||||
if (bytesExpected > 0) {
|
||||
const progress = (bytesReceived / bytesExpected) * 100;
|
||||
console.log(`Upload progress: ${progress.toFixed(2)}%`);
|
||||
const data = {
|
||||
progress: progress.toFixed(2),
|
||||
message: `Upload progress: ${progress.toFixed(2)}%`,
|
||||
};
|
||||
writeEvents(req, data);
|
||||
}
|
||||
});
|
||||
|
||||
fileStream.pipe(writeStream);
|
||||
|
||||
writeStream.on('finish', () => {
|
||||
file = {
|
||||
filepath: tempPath,
|
||||
originalFilename: decodedFilename,
|
||||
mimetype: mimeType,
|
||||
hash: hash.digest('hex'),
|
||||
size: size,
|
||||
};
|
||||
resolve();
|
||||
});
|
||||
|
||||
writeStream.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
busboy.on('finish', async () => {
|
||||
// 等待文件写入完成
|
||||
if (filePromise) {
|
||||
try {
|
||||
await filePromise;
|
||||
} catch (err) {
|
||||
console.error(`File write error: ${err.message}`);
|
||||
res.end(error(`File write error: ${err.message}`));
|
||||
return;
|
||||
}
|
||||
}
|
||||
const clearFiles = () => {
|
||||
if (file?.filepath && fs.existsSync(file.filepath)) {
|
||||
fs.unlinkSync(file.filepath);
|
||||
}
|
||||
};
|
||||
|
||||
if (!file) {
|
||||
res.end(error('No file uploaded'));
|
||||
return;
|
||||
}
|
||||
|
||||
let appKey, collection;
|
||||
const { appKey: _appKey, collection: _collecion } = fields;
|
||||
if (Array.isArray(_appKey)) {
|
||||
appKey = _appKey?.[0];
|
||||
} else {
|
||||
appKey = _appKey;
|
||||
}
|
||||
if (Array.isArray(_collecion)) {
|
||||
collection = _collecion?.[0];
|
||||
} else {
|
||||
collection = _collecion;
|
||||
}
|
||||
collection = parseIfJson(collection);
|
||||
|
||||
appKey = appKey || 'micro-app';
|
||||
console.log('Appkey', appKey);
|
||||
console.log('collection', collection);
|
||||
|
||||
// 处理上传的文件
|
||||
const uploadResults = [];
|
||||
const tempPath = file.filepath; // 文件上传时的临时路径
|
||||
const relativePath = file.originalFilename; // 保留表单中上传的文件名 (包含文件夹结构)
|
||||
// 比如 child2/b.txt
|
||||
const minioPath = `private/${tokenUser.username}/${appKey}/${relativePath}`;
|
||||
// 上传到 MinIO 并保留文件夹结构
|
||||
const isHTML = relativePath.endsWith('.html');
|
||||
await oss.fPutObject(minioPath, tempPath, {
|
||||
'Content-Type': getContentType(relativePath),
|
||||
'app-source': 'user-micro-app',
|
||||
'Cache-Control': isHTML ? 'no-cache' : 'max-age=31536000, immutable', // 缓存一年
|
||||
});
|
||||
uploadResults.push({
|
||||
name: relativePath,
|
||||
path: minioPath,
|
||||
hash: file.hash,
|
||||
size: file.size,
|
||||
});
|
||||
fs.unlinkSync(tempPath); // 删除临时文件
|
||||
|
||||
// 受控
|
||||
const r = await app.call({
|
||||
path: 'micro-app',
|
||||
key: 'upload',
|
||||
payload: {
|
||||
token: token,
|
||||
data: {
|
||||
appKey,
|
||||
collection,
|
||||
files: uploadResults,
|
||||
},
|
||||
},
|
||||
});
|
||||
const data: any = {
|
||||
code: r.code,
|
||||
data: r.body,
|
||||
};
|
||||
if (r.message) {
|
||||
data.message = r.message;
|
||||
}
|
||||
res.end(JSON.stringify(data));
|
||||
});
|
||||
|
||||
pipeBusboy(req, res, busboy);
|
||||
});
|
||||
|
||||
|
||||
function parseIfJson(collection: any): any {
|
||||
try {
|
||||
return JSON.parse(collection);
|
||||
} catch (e) {
|
||||
return collection;
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import { router, error, checkAuth, clients, getTaskId, writeEvents, deleteOldClients } from './router.ts';
|
||||
|
||||
router.get('/api/events', async (req, res) => {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
Connection: 'keep-alive',
|
||||
});
|
||||
const taskId = getTaskId(req);
|
||||
if (!taskId) {
|
||||
res.end(error('task-id is required'));
|
||||
return;
|
||||
}
|
||||
// 将客户端连接推送到 clients 数组
|
||||
clients.set(taskId, { client: res, createTime: Date.now() });
|
||||
// 移除客户端连接
|
||||
req.on('close', () => {
|
||||
clients.delete(taskId);
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/api/s1/events', async (req, res) => {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
Connection: 'keep-alive',
|
||||
});
|
||||
const taskId = getTaskId(req);
|
||||
if (!taskId) {
|
||||
res.end(error('task-id is required'));
|
||||
return;
|
||||
}
|
||||
// 将客户端连接推送到 clients 数组
|
||||
clients.set(taskId, { client: res, createTime: Date.now() });
|
||||
writeEvents(req, { progress: 0, message: 'start' });
|
||||
// 不自动关闭连接
|
||||
// res.end('ok');
|
||||
});
|
||||
|
||||
router.get('/api/s1/events/close', async (req, res) => {
|
||||
const taskId = getTaskId(req);
|
||||
if (!taskId) {
|
||||
res.end(error('task-id is required'));
|
||||
return;
|
||||
}
|
||||
deleteOldClients();
|
||||
clients.delete(taskId);
|
||||
res.end('ok');
|
||||
});
|
||||
@@ -1,20 +1,9 @@
|
||||
import { useFileStore } from '@kevisual/use-config';
|
||||
import http from 'node:http';
|
||||
import fs from 'fs';
|
||||
import Busboy from 'busboy';
|
||||
import { app, oss } from '@/app.ts';
|
||||
|
||||
import { getContentType } from '@/utils/get-content-type.ts';
|
||||
import { User } from '@/models/user.ts';
|
||||
import { router, error, checkAuth, writeEvents } from './router.ts';
|
||||
import { router } from './router.ts';
|
||||
import './index.ts';
|
||||
import { handleRequest as PageProxy } from './page-proxy.ts';
|
||||
|
||||
const simpleAppsPrefixs = [
|
||||
"/api/micro-app/",
|
||||
"/api/events",
|
||||
"/api/s1/",
|
||||
"/api/resource/",
|
||||
"/api/wxmsg"
|
||||
];
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
// import './code/upload.ts';
|
||||
import './event.ts';
|
||||
|
||||
import './resources/upload.ts';
|
||||
import './resources/chunk.ts';
|
||||
// import './resources/get-resources.ts';
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import { User } from '@/models/user.ts';
|
||||
import http from 'http';
|
||||
import { parse } from '@kevisual/router/src/server/cookie.ts';
|
||||
export const error = (msg: string, code = 500) => {
|
||||
return JSON.stringify({ code, message: msg });
|
||||
};
|
||||
export const checkAuth = async (req: http.IncomingMessage, res: http.ServerResponse) => {
|
||||
let token = (req.headers?.['authorization'] as string) || (req.headers?.['Authorization'] as string) || '';
|
||||
const url = new URL(req.url || '', 'http://localhost');
|
||||
const resNoPermission = () => {
|
||||
res.statusCode = 401;
|
||||
res.end(error('Invalid authorization'));
|
||||
return { tokenUser: null, token: null };
|
||||
};
|
||||
if (!token) {
|
||||
token = url.searchParams.get('token') || '';
|
||||
}
|
||||
if (!token) {
|
||||
const parsedCookies = parse(req.headers.cookie || '');
|
||||
token = parsedCookies.token || '';
|
||||
}
|
||||
if (!token) {
|
||||
return resNoPermission();
|
||||
}
|
||||
if (token) {
|
||||
token = token.replace('Bearer ', '');
|
||||
}
|
||||
let tokenUser;
|
||||
try {
|
||||
tokenUser = await User.verifyToken(token);
|
||||
} catch (e) {
|
||||
console.log('checkAuth error', e);
|
||||
res.statusCode = 401;
|
||||
res.end(error('Invalid token'));
|
||||
return { tokenUser: null, token: null };
|
||||
}
|
||||
return { tokenUser, token };
|
||||
};
|
||||
|
||||
export const getLoginUser = async (req: http.IncomingMessage) => {
|
||||
let token = (req.headers?.['authorization'] as string) || (req.headers?.['Authorization'] as string) || '';
|
||||
const url = new URL(req.url || '', 'http://localhost');
|
||||
if (!token) {
|
||||
token = url.searchParams.get('token') || '';
|
||||
}
|
||||
if (!token) {
|
||||
const parsedCookies = parse(req.headers.cookie || '');
|
||||
token = parsedCookies.token || '';
|
||||
}
|
||||
|
||||
if (token) {
|
||||
token = token.replace('Bearer ', '');
|
||||
}
|
||||
let tokenUser;
|
||||
try {
|
||||
tokenUser = await User.verifyToken(token);
|
||||
return { tokenUser, token };
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from './auth.ts'
|
||||
@@ -1,237 +0,0 @@
|
||||
import { useFileStore } from '@kevisual/use-config';
|
||||
import { checkAuth, error, router, writeEvents, getKey, getTaskId } from '../router.ts';
|
||||
import Busboy from 'busboy';
|
||||
import { app, oss } from '@/app.ts';
|
||||
|
||||
import { getContentType } from '@/utils/get-content-type.ts';
|
||||
import { User } from '@/models/user.ts';
|
||||
import fs from 'fs';
|
||||
import { ConfigModel } from '@/routes/config/models/model.ts';
|
||||
import { validateDirectory } from './util.ts';
|
||||
import path from 'path';
|
||||
import { createWriteStream } from 'fs';
|
||||
import { pipeBusboy } from '@/modules/fm-manager/index.ts';
|
||||
|
||||
const cacheFilePath = useFileStore('cache-file', { needExists: true });
|
||||
|
||||
router.get('/api/s1/resources/upload/chunk', async (req, res) => {
|
||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||
res.end('Upload API is ready');
|
||||
});
|
||||
|
||||
// /api/s1/resources/upload
|
||||
router.post('/api/s1/resources/upload/chunk', async (req, res) => {
|
||||
const { tokenUser, token } = await checkAuth(req, res);
|
||||
if (!tokenUser) return;
|
||||
const url = new URL(req.url || '', 'http://localhost');
|
||||
const share = !!url.searchParams.get('public');
|
||||
const noCheckAppFiles = !!url.searchParams.get('noCheckAppFiles');
|
||||
|
||||
const taskId = getTaskId(req);
|
||||
const finalFilePath = `${cacheFilePath}/${taskId}`;
|
||||
if (!taskId) {
|
||||
res.end(error('taskId is required'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用 busboy 解析 multipart/form-data
|
||||
const busboy = Busboy({ headers: req.headers, preservePath: true, defCharset: 'utf-8' });
|
||||
const fields: any = {};
|
||||
let file: any = null;
|
||||
let tempPath = '';
|
||||
let filePromise: Promise<void> | null = null;
|
||||
|
||||
busboy.on('field', (fieldname, value) => {
|
||||
fields[fieldname] = value;
|
||||
});
|
||||
|
||||
busboy.on('file', (fieldname, fileStream, info) => {
|
||||
const { filename, encoding, mimeType } = info;
|
||||
// 处理 UTF-8 文件名编码
|
||||
const decodedFilename = typeof filename === 'string' ? Buffer.from(filename, 'latin1').toString('utf8') : filename;
|
||||
tempPath = path.join(cacheFilePath, `${Date.now()}-${Math.random().toString(36).substring(7)}`);
|
||||
const writeStream = createWriteStream(tempPath);
|
||||
|
||||
filePromise = new Promise<void>((resolve, reject) => {
|
||||
fileStream.pipe(writeStream);
|
||||
|
||||
writeStream.on('finish', () => {
|
||||
file = {
|
||||
filepath: tempPath,
|
||||
originalFilename: decodedFilename,
|
||||
mimetype: mimeType,
|
||||
};
|
||||
resolve();
|
||||
});
|
||||
|
||||
writeStream.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
busboy.on('finish', async () => {
|
||||
// 等待文件写入完成
|
||||
if (filePromise) {
|
||||
try {
|
||||
await filePromise;
|
||||
} catch (err) {
|
||||
console.error(`File write error: ${err.message}`);
|
||||
res.end(error(`File write error: ${err.message}`));
|
||||
return;
|
||||
}
|
||||
}
|
||||
const clearFiles = () => {
|
||||
if (tempPath && fs.existsSync(tempPath)) {
|
||||
fs.unlinkSync(tempPath);
|
||||
}
|
||||
if (fs.existsSync(finalFilePath)) {
|
||||
fs.unlinkSync(finalFilePath);
|
||||
}
|
||||
};
|
||||
|
||||
if (!file) {
|
||||
res.end(error('No file uploaded'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle chunked upload logic here
|
||||
let { chunkIndex, totalChunks, appKey, version, username, directory } = getKey(fields, [
|
||||
'chunkIndex',
|
||||
'totalChunks',
|
||||
'appKey',
|
||||
'version',
|
||||
'username',
|
||||
'directory',
|
||||
]);
|
||||
if (!chunkIndex || !totalChunks) {
|
||||
res.end(error('chunkIndex, totalChunks is required'));
|
||||
clearFiles();
|
||||
return;
|
||||
}
|
||||
const relativePath = file.originalFilename;
|
||||
|
||||
const writeStream = fs.createWriteStream(finalFilePath, { flags: 'a' });
|
||||
const readStream = fs.createReadStream(tempPath);
|
||||
readStream.pipe(writeStream);
|
||||
|
||||
writeStream.on('finish', async () => {
|
||||
fs.unlinkSync(tempPath); // 删除临时文件
|
||||
|
||||
// Write event for progress tracking
|
||||
const progress = ((parseInt(chunkIndex) + 1) / parseInt(totalChunks)) * 100;
|
||||
writeEvents(req, {
|
||||
progress,
|
||||
message: `Upload progress: ${progress}%`,
|
||||
});
|
||||
|
||||
if (parseInt(chunkIndex) + 1 === parseInt(totalChunks)) {
|
||||
let uid = tokenUser.id;
|
||||
if (username) {
|
||||
const user = await User.getUserByToken(token);
|
||||
const has = await user.hasUser(username, true);
|
||||
if (!has) {
|
||||
res.end(error('username is not found'));
|
||||
clearFiles();
|
||||
return;
|
||||
}
|
||||
const _user = await User.findOne({ where: { username } });
|
||||
uid = _user?.id || '';
|
||||
}
|
||||
if (!appKey || !version) {
|
||||
const config = await ConfigModel.getUploadConfig({ uid });
|
||||
if (config) {
|
||||
appKey = config.config?.data?.key || '';
|
||||
version = config.config?.data?.version || '';
|
||||
}
|
||||
}
|
||||
if (!appKey || !version) {
|
||||
res.end(error('appKey or version is not found, please check the upload config.'));
|
||||
clearFiles();
|
||||
return;
|
||||
}
|
||||
const { code, message } = validateDirectory(directory);
|
||||
if (code !== 200) {
|
||||
res.end(error(message));
|
||||
clearFiles();
|
||||
return;
|
||||
}
|
||||
const minioPath = `${username || tokenUser.username}/${appKey}/${version}${directory ? `/${directory}` : ''}/${relativePath}`;
|
||||
const metadata: any = {};
|
||||
if (share) {
|
||||
metadata.share = 'public';
|
||||
}
|
||||
// All chunks uploaded, now upload to MinIO
|
||||
await oss.fPutObject(minioPath, finalFilePath, {
|
||||
'Content-Type': getContentType(relativePath),
|
||||
'app-source': 'user-app',
|
||||
'Cache-Control': relativePath.endsWith('.html') ? 'no-cache' : 'max-age=31536000, immutable',
|
||||
...metadata,
|
||||
});
|
||||
|
||||
// Clean up the final file
|
||||
fs.unlinkSync(finalFilePath);
|
||||
const downloadBase = '/api/s1/share';
|
||||
|
||||
const uploadResult = {
|
||||
name: relativePath,
|
||||
path: `${downloadBase}/${minioPath}`,
|
||||
appKey,
|
||||
version,
|
||||
username,
|
||||
};
|
||||
if (!noCheckAppFiles) {
|
||||
// Notify the app
|
||||
const r = await app.call({
|
||||
path: 'app',
|
||||
key: 'detectVersionList',
|
||||
payload: {
|
||||
token: token,
|
||||
data: {
|
||||
appKey,
|
||||
version,
|
||||
username,
|
||||
},
|
||||
},
|
||||
});
|
||||
const data: any = {
|
||||
code: r.code,
|
||||
data: {
|
||||
app: r.body,
|
||||
upload: [uploadResult],
|
||||
},
|
||||
};
|
||||
if (r.message) {
|
||||
data.message = r.message;
|
||||
}
|
||||
console.log('upload data', data);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(data));
|
||||
} else {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
code: 200,
|
||||
message: 'Chunk uploaded successfully',
|
||||
data: { chunkIndex, totalChunks, upload: [uploadResult] },
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
code: 200,
|
||||
message: 'Chunk uploaded successfully',
|
||||
data: {
|
||||
chunkIndex,
|
||||
totalChunks,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
pipeBusboy(req, res, busboy);
|
||||
});
|
||||
@@ -1,290 +0,0 @@
|
||||
import { useFileStore } from '@kevisual/use-config';
|
||||
import { checkAuth, error, router, writeEvents, getKey } from '../router.ts';
|
||||
import Busboy from 'busboy';
|
||||
import { app } from '@/app.ts';
|
||||
|
||||
import { getContentType } from '@/utils/get-content-type.ts';
|
||||
import { User } from '@/models/user.ts';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { createWriteStream } from 'fs';
|
||||
import { pipeBusboy } from '@/modules/fm-manager/pipe-busboy.ts';
|
||||
import { ConfigModel } from '@/routes/config/models/model.ts';
|
||||
import { validateDirectory } from './util.ts';
|
||||
import { pick } from 'es-toolkit';
|
||||
import { getFileStat } from '@/routes/file/index.ts';
|
||||
import { logger } from '@/modules/logger.ts';
|
||||
import { oss } from '@/modules/s3.ts';
|
||||
|
||||
const cacheFilePath = useFileStore('cache-file', { needExists: true });
|
||||
|
||||
router.get('/api/s1/resources/upload', async (req, res) => {
|
||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||
res.end('Upload API is ready');
|
||||
});
|
||||
export const parseIfJson = (data = '{}') => {
|
||||
try {
|
||||
const _data = JSON.parse(data);
|
||||
if (typeof _data === 'object') return _data;
|
||||
return {};
|
||||
} catch (error) {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
router.post('/api/s1/resources/upload/check', async (req, res) => {
|
||||
const { tokenUser, token } = await checkAuth(req, res);
|
||||
if (!tokenUser) {
|
||||
res.end(error('Token is invalid.'));
|
||||
return;
|
||||
}
|
||||
console.log('data', req.url);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
const data = await router.getBody(req);
|
||||
type Data = {
|
||||
appKey: string;
|
||||
version: string;
|
||||
username: string;
|
||||
directory: string;
|
||||
files: { path: string; hash: string }[];
|
||||
};
|
||||
let { appKey, version, username, directory, files } = pick(data, ['appKey', 'version', 'username', 'directory', 'files']) as Data;
|
||||
let uid = tokenUser.id;
|
||||
if (username) {
|
||||
const user = await User.getUserByToken(token);
|
||||
const has = await user.hasUser(username, true);
|
||||
if (!has) {
|
||||
res.end(error('username is not found'));
|
||||
return;
|
||||
}
|
||||
const _user = await User.findOne({ where: { username } });
|
||||
uid = _user?.id || '';
|
||||
}
|
||||
if (!appKey || !version) {
|
||||
res.end(error('appKey and version is required'));
|
||||
}
|
||||
|
||||
const { code, message } = validateDirectory(directory);
|
||||
if (code !== 200) {
|
||||
res.end(error(message));
|
||||
return;
|
||||
}
|
||||
type CheckResult = {
|
||||
path: string;
|
||||
stat: any;
|
||||
resourcePath: string;
|
||||
hash: string;
|
||||
uploadHash: string;
|
||||
isUpload?: boolean;
|
||||
};
|
||||
const checkResult: CheckResult[] = [];
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
const relativePath = file.path;
|
||||
const minioPath = `${username || tokenUser.username}/${appKey}/${version}${directory ? `/${directory}` : ''}/${relativePath}`;
|
||||
let stat = await getFileStat(minioPath, true);
|
||||
const statHash = stat?.etag || '';
|
||||
checkResult.push({
|
||||
path: relativePath,
|
||||
uploadHash: file.hash,
|
||||
resourcePath: minioPath,
|
||||
isUpload: statHash === file.hash,
|
||||
stat,
|
||||
hash: statHash,
|
||||
});
|
||||
}
|
||||
res.end(JSON.stringify({ code: 200, data: checkResult }));
|
||||
});
|
||||
|
||||
// /api/s1/resources/upload
|
||||
router.post('/api/s1/resources/upload', async (req, res) => {
|
||||
const { tokenUser, token } = await checkAuth(req, res);
|
||||
if (!tokenUser) {
|
||||
res.end(error('Token is invalid.'));
|
||||
return;
|
||||
}
|
||||
const url = new URL(req.url || '', 'http://localhost');
|
||||
const share = !!url.searchParams.get('public');
|
||||
const meta = parseIfJson(url.searchParams.get('meta'));
|
||||
const noCheckAppFiles = !!url.searchParams.get('noCheckAppFiles');
|
||||
// 使用 busboy 解析 multipart/form-data
|
||||
const busboy = Busboy({ headers: req.headers, preservePath: true, defCharset: 'utf-8' });
|
||||
const fields: any = {};
|
||||
const files: any[] = [];
|
||||
const filePromises: Promise<void>[] = [];
|
||||
let bytesReceived = 0;
|
||||
let bytesExpected = parseInt(req.headers['content-length'] || '0');
|
||||
busboy.on('field', (fieldname, value) => {
|
||||
fields[fieldname] = value;
|
||||
});
|
||||
|
||||
busboy.on('file', (fieldname, fileStream, info) => {
|
||||
const { filename, encoding, mimeType } = info;
|
||||
// 处理 UTF-8 文件名编码(busboy 可能返回 Latin-1 编码的缓冲区)
|
||||
const decodedFilename = typeof filename === 'string' ? Buffer.from(filename, 'latin1').toString('utf8') : filename;
|
||||
const tempPath = path.join(cacheFilePath, `${Date.now()}-${Math.random().toString(36).substring(7)}`);
|
||||
const writeStream = createWriteStream(tempPath);
|
||||
const filePromise = new Promise<void>((resolve, reject) => {
|
||||
fileStream.on('data', (chunk) => {
|
||||
bytesReceived += chunk.length;
|
||||
if (bytesExpected > 0) {
|
||||
const progress = (bytesReceived / bytesExpected) * 100;
|
||||
const data = {
|
||||
progress: progress.toFixed(2),
|
||||
message: `Upload progress: ${progress.toFixed(2)}%`,
|
||||
};
|
||||
console.log('progress-upload', data);
|
||||
writeEvents(req, data);
|
||||
}
|
||||
});
|
||||
|
||||
fileStream.pipe(writeStream);
|
||||
|
||||
writeStream.on('finish', () => {
|
||||
files.push({
|
||||
filepath: tempPath,
|
||||
originalFilename: decodedFilename,
|
||||
mimetype: mimeType,
|
||||
});
|
||||
resolve();
|
||||
});
|
||||
|
||||
writeStream.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
|
||||
filePromises.push(filePromise);
|
||||
});
|
||||
|
||||
busboy.on('finish', async () => {
|
||||
// 等待所有文件写入完成
|
||||
try {
|
||||
await Promise.all(filePromises);
|
||||
} catch (err) {
|
||||
logger.error(`File write error: ${err.message}`);
|
||||
res.end(error(`File write error: ${err.message}`));
|
||||
return;
|
||||
}
|
||||
const clearFiles = () => {
|
||||
files.forEach((file) => {
|
||||
if (file?.filepath && fs.existsSync(file.filepath)) {
|
||||
fs.unlinkSync(file.filepath);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 检查是否有文件上传
|
||||
if (files.length === 0) {
|
||||
res.end(error('files is required'));
|
||||
return;
|
||||
}
|
||||
|
||||
let { appKey, version, username, directory, description } = getKey(fields, ['appKey', 'version', 'username', 'directory', 'description']);
|
||||
let uid = tokenUser.id;
|
||||
if (username) {
|
||||
const user = await User.getUserByToken(token);
|
||||
const has = await user.hasUser(username, true);
|
||||
if (!has) {
|
||||
res.end(error('username is not found'));
|
||||
clearFiles();
|
||||
return;
|
||||
}
|
||||
const _user = await User.findOne({ where: { username } });
|
||||
uid = _user?.id || '';
|
||||
}
|
||||
if (!appKey || !version) {
|
||||
const config = await ConfigModel.getUploadConfig({ uid });
|
||||
if (config) {
|
||||
appKey = config.config?.data?.key || '';
|
||||
version = config.config?.data?.version || '';
|
||||
}
|
||||
}
|
||||
if (!appKey || !version) {
|
||||
res.end(error('appKey or version is not found, please check the upload config.'));
|
||||
clearFiles();
|
||||
return;
|
||||
}
|
||||
const { code, message } = validateDirectory(directory);
|
||||
if (code !== 200) {
|
||||
res.end(error(message));
|
||||
clearFiles();
|
||||
return;
|
||||
}
|
||||
// 逐个处理每个上传的文件
|
||||
const uploadedFiles = files;
|
||||
logger.info(
|
||||
'upload files',
|
||||
uploadedFiles.map((item) => {
|
||||
return pick(item, ['filepath', 'originalFilename']);
|
||||
}),
|
||||
);
|
||||
const uploadResults = [];
|
||||
for (let i = 0; i < uploadedFiles.length; i++) {
|
||||
const file = uploadedFiles[i];
|
||||
// @ts-ignore
|
||||
const tempPath = file.filepath; // 文件上传时的临时路径
|
||||
const relativePath = file.originalFilename; // 保留表单中上传的文件名 (包含文件夹结构)
|
||||
// 比如 child2/b.txt
|
||||
const minioPath = `${username || tokenUser.username}/${appKey}/${version}${directory ? `/${directory}` : ''}/${relativePath}`;
|
||||
// 上传到 MinIO 并保留文件夹结构
|
||||
const isHTML = relativePath.endsWith('.html');
|
||||
const metadata: any = {};
|
||||
if (share) {
|
||||
metadata.share = 'public';
|
||||
}
|
||||
Object.assign(metadata, meta);
|
||||
await oss.fPutObject(minioPath, tempPath, {
|
||||
'Content-Type': getContentType(relativePath),
|
||||
'app-source': 'user-app',
|
||||
'Cache-Control': isHTML ? 'no-cache' : 'max-age=31536000, immutable', // 缓存一年
|
||||
...metadata,
|
||||
});
|
||||
uploadResults.push({
|
||||
name: relativePath,
|
||||
path: minioPath,
|
||||
});
|
||||
fs.unlinkSync(tempPath); // 删除临时文件
|
||||
}
|
||||
if (!noCheckAppFiles) {
|
||||
const _data = { appKey, version, username, files: uploadResults, description, }
|
||||
if (_data.description) {
|
||||
delete _data.description;
|
||||
}
|
||||
// 受控
|
||||
const r = await app.call({
|
||||
path: 'app',
|
||||
key: 'uploadFiles',
|
||||
payload: {
|
||||
token: token,
|
||||
data: _data,
|
||||
},
|
||||
});
|
||||
const data: any = {
|
||||
code: r.code,
|
||||
data: {
|
||||
app: r.body,
|
||||
upload: uploadResults,
|
||||
},
|
||||
};
|
||||
if (r.message) {
|
||||
data.message = r.message;
|
||||
}
|
||||
console.log('upload data', data);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(data));
|
||||
} else {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
code: 200,
|
||||
data: {
|
||||
detect: [],
|
||||
upload: uploadResults,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
pipeBusboy(req, res, busboy);
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
/**
|
||||
* 校验directory是否合法, 合法返回200, 不合法返回500
|
||||
*
|
||||
* directory 不能以/开头,不能以/结尾。不能以.开头,不能以.结尾。
|
||||
* 把directory的/替换掉后,只能包含数字、字母、下划线、中划线
|
||||
* @param directory 目录
|
||||
* @returns
|
||||
*/
|
||||
export const validateDirectory = (directory?: string) => {
|
||||
// 对directory进行校验,不能以/开头,不能以/结尾。不能以.开头,不能以.结尾。
|
||||
if (directory && (directory.startsWith('/') || directory.endsWith('/') || directory.startsWith('..') || directory.endsWith('..'))) {
|
||||
return {
|
||||
code: 500,
|
||||
message: 'directory is invalid',
|
||||
};
|
||||
}
|
||||
// 把directory的/替换掉后,只能包含数字、字母、下划线、中划线
|
||||
// 可以包含.
|
||||
let _directory = directory?.replace(/\//g, '');
|
||||
if (_directory && !/^[a-zA-Z0-9_.-]+$/.test(_directory)) {
|
||||
return {
|
||||
code: 500,
|
||||
message: 'directory is invalid, only number, letter, underline and hyphen are allowed',
|
||||
};
|
||||
}
|
||||
return {
|
||||
code: 200,
|
||||
message: 'directory is valid',
|
||||
};
|
||||
};
|
||||
@@ -1,14 +1,13 @@
|
||||
import { router } from '@/app.ts';
|
||||
import http from 'http';
|
||||
import { useContextKey } from '@kevisual/context';
|
||||
import { checkAuth, error } from './middleware/auth.ts';
|
||||
export { router, checkAuth, error };
|
||||
export { router, };
|
||||
|
||||
/**
|
||||
* 事件客户端
|
||||
*/
|
||||
const eventClientsInit = () => {
|
||||
const clients = new Map<string, { client?: http.ServerResponse; createTime?: number; [key: string]: any }>();
|
||||
const clients = new Map<string, { client?: http.ServerResponse; createTime?: number;[key: string]: any }>();
|
||||
return clients;
|
||||
};
|
||||
export const clients = useContextKey('event-clients', () => eventClientsInit());
|
||||
|
||||
@@ -43,7 +43,7 @@ app
|
||||
console.log('get app manager called');
|
||||
const tokenUser = ctx.state.tokenUser;
|
||||
const id = ctx.query.id;
|
||||
const { key, version } = ctx.query?.data || {};
|
||||
const { key, version, create = false } = ctx.query?.data || {};
|
||||
if (!id && (!key || !version)) {
|
||||
throw new CustomError('id is required');
|
||||
}
|
||||
@@ -59,8 +59,27 @@ app
|
||||
},
|
||||
});
|
||||
}
|
||||
if (!am && create) {
|
||||
am = await AppListModel.create({
|
||||
key,
|
||||
version,
|
||||
uid: tokenUser.id,
|
||||
data: {},
|
||||
});
|
||||
const res = await app.run({ path: 'app', key: "detectVersionList", payload: { data: { appKey: key, version, username: tokenUser.username }, token: ctx.query.token } });
|
||||
if (res.code !== 200) {
|
||||
ctx.throw(res.message || 'detect version list error');
|
||||
}
|
||||
am = await AppListModel.findOne({
|
||||
where: {
|
||||
key,
|
||||
version,
|
||||
uid: tokenUser.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (!am) {
|
||||
throw new CustomError('app not found');
|
||||
ctx.throw('app not found');
|
||||
}
|
||||
console.log('get app', am.id, am.key, am.version);
|
||||
ctx.body = prefixFix(am, tokenUser.username);
|
||||
|
||||
@@ -59,10 +59,7 @@ app
|
||||
}
|
||||
}
|
||||
await config.update({
|
||||
data: {
|
||||
...config.data,
|
||||
...data,
|
||||
},
|
||||
data: data,
|
||||
...rest,
|
||||
});
|
||||
if (config.data?.permission?.share === 'public') {
|
||||
@@ -80,7 +77,7 @@ app
|
||||
});
|
||||
if (config) {
|
||||
await config.update({
|
||||
data: { ...config.data, ...data },
|
||||
data: data,
|
||||
...rest,
|
||||
});
|
||||
ctx.body = config;
|
||||
@@ -177,7 +174,7 @@ app
|
||||
const search: any = id ? { id } : { key };
|
||||
const config = await ConfigModel.findOne({
|
||||
where: {
|
||||
...search
|
||||
...search
|
||||
},
|
||||
});
|
||||
if (config && config.uid === tuid) {
|
||||
|
||||
@@ -153,7 +153,9 @@ app
|
||||
browser: someInfo['user-agent'],
|
||||
host: someInfo.host,
|
||||
});
|
||||
createCookie(token, ctx);
|
||||
createCookie({
|
||||
token: token.accessToken
|
||||
}, ctx);
|
||||
ctx.body = token;
|
||||
})
|
||||
.addTo(app);
|
||||
@@ -259,7 +261,10 @@ app
|
||||
const refreshToken = accessUser.oauthExpand?.refreshToken;
|
||||
if (refreshToken) {
|
||||
const result = await User.oauth.refreshToken(refreshToken);
|
||||
createCookie(result, ctx);
|
||||
createCookie({
|
||||
token: result.accessToken
|
||||
}, ctx);
|
||||
|
||||
ctx.body = result;
|
||||
return;
|
||||
} else if (accessUser) {
|
||||
@@ -268,7 +273,9 @@ app
|
||||
...accessUser.oauthExpand,
|
||||
hasRefreshToken: true,
|
||||
});
|
||||
createCookie(result, ctx);
|
||||
createCookie({
|
||||
token: result.accessToken
|
||||
}, ctx);
|
||||
ctx.body = result;
|
||||
return;
|
||||
}
|
||||
@@ -323,13 +330,17 @@ app
|
||||
if (orgsList.includes(username)) {
|
||||
if (tokenUsername === username) {
|
||||
const result = await User.oauth.resetToken(token);
|
||||
createCookie(result, ctx);
|
||||
createCookie({
|
||||
token: result.accessToken,
|
||||
}, ctx);
|
||||
await User.oauth.delToken(token);
|
||||
ctx.body = result;
|
||||
} else {
|
||||
const user = await User.findOne({ where: { username } });
|
||||
const result = await user.createToken(userId, 'default');
|
||||
createCookie(result, ctx);
|
||||
createCookie({
|
||||
token: result.accessToken,
|
||||
}, ctx);
|
||||
ctx.body = result;
|
||||
}
|
||||
} else {
|
||||
@@ -352,7 +363,9 @@ app
|
||||
const result = await User.oauth.refreshToken(refreshToken);
|
||||
if (result) {
|
||||
console.log('refreshToken result', result);
|
||||
createCookie(result, ctx);
|
||||
createCookie({
|
||||
token: result.accessToken,
|
||||
}, ctx);
|
||||
ctx.body = result;
|
||||
} else {
|
||||
ctx.throw(500, 'Refresh Token Failed, please login again');
|
||||
|
||||
@@ -109,7 +109,9 @@ app
|
||||
const token = JSON.parse(data);
|
||||
if (token.accessToken) {
|
||||
ctx.body = token;
|
||||
createCookie(token, ctx);
|
||||
createCookie({
|
||||
token: token.accessToken,
|
||||
}, ctx);
|
||||
} else {
|
||||
ctx.throw(500, 'Checked error Failed, login failed, please login again');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user