feat: add me
This commit is contained in:
parent
1eb65859e6
commit
64f89b60dc
@ -1 +1 @@
|
|||||||
Subproject commit f0468539dc53f89a509187a95ba9823702f62105
|
Subproject commit c92f817d667e33e8fcdfad4d44f4a63df0534db5
|
@ -3,13 +3,10 @@ import http from 'http';
|
|||||||
import fs, { rm } from 'fs';
|
import fs, { rm } from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { IncomingForm } from 'formidable';
|
import { IncomingForm } from 'formidable';
|
||||||
import { checkToken } from '@abearxiong/auth';
|
|
||||||
import { useConfig } from '@abearxiong/use-config';
|
|
||||||
import { app, minioClient } from '@/app.ts';
|
import { app, minioClient } from '@/app.ts';
|
||||||
import { bucketName } from '@/modules/minio.ts';
|
import { bucketName } from '@/modules/minio.ts';
|
||||||
import { getContentType } from '@/utils/get-content-type.ts';
|
import { getContentType } from '@/utils/get-content-type.ts';
|
||||||
import { User } from '@/models/user.ts';
|
import { User } from '@/models/user.ts';
|
||||||
const { tokenSecret } = useConfig<{ tokenSecret: string }>();
|
|
||||||
const filePath = useFileStore('upload', { needExists: true });
|
const filePath = useFileStore('upload', { needExists: true });
|
||||||
// curl -X POST http://localhost:4000/api/upload -F "file=@readme.md"
|
// curl -X POST http://localhost:4000/api/upload -F "file=@readme.md"
|
||||||
// curl -X POST http://localhost:4000/api/upload \
|
// curl -X POST http://localhost:4000/api/upload \
|
||||||
@ -24,76 +21,15 @@ export const uploadMiddleware = async (req: http.IncomingMessage, res: http.Serv
|
|||||||
res.end('Upload API is ready');
|
res.end('Upload API is ready');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (false && req.method === 'POST' && req.url === '/api/app/upload') {
|
|
||||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
||||||
|
|
||||||
// 检查 Content-Type 是否为 multipart/form-data
|
|
||||||
const contentType = req.headers['content-type'];
|
|
||||||
if (!contentType || !contentType.startsWith('multipart/form-data')) {
|
|
||||||
res.end('Invalid content type, expecting multipart/form-data');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 提取 boundary (边界) 标识
|
|
||||||
const boundary = contentType.split('boundary=')[1];
|
|
||||||
if (!boundary) {
|
|
||||||
res.end('Invalid multipart/form-data format');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将接收到的所有数据存入临时数组中
|
|
||||||
let rawData = Buffer.alloc(0);
|
|
||||||
req.on('data', (chunk) => {
|
|
||||||
rawData = Buffer.concat([rawData, chunk]);
|
|
||||||
});
|
|
||||||
|
|
||||||
req.on('end', () => {
|
|
||||||
// 解析所有文件部分
|
|
||||||
const parts = parseMultipartData(rawData, boundary);
|
|
||||||
|
|
||||||
// 存储上传文件结果
|
|
||||||
const uploadResults = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < parts.length; i++) {
|
|
||||||
const part = parts[i];
|
|
||||||
if (part.filename) {
|
|
||||||
if (!fs.existsSync(filePath)) {
|
|
||||||
fs.mkdirSync(filePath, { recursive: true });
|
|
||||||
}
|
|
||||||
const tempFilePath = path.join(filePath, part.filename);
|
|
||||||
fs.writeFileSync(tempFilePath, part.data);
|
|
||||||
uploadResults.push(`File ${part.filename} uploaded successfully.`);
|
|
||||||
// 上传到 MinIO
|
|
||||||
// minioClient.fPutObject(bucketName, part.filename, tempFilePath, {}, (err, etag) => {
|
|
||||||
// fs.unlinkSync(tempFilePath); // 删除临时文件
|
|
||||||
// if (err) {
|
|
||||||
// uploadResults.push(`Upload error for ${part.filename}: ${err.message}`);
|
|
||||||
// } else {
|
|
||||||
// uploadResults.push(`File ${part.filename} uploaded successfully. ETag: ${etag}`);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // 如果所有文件都处理完毕,返回结果
|
|
||||||
// if (uploadResults.length === parts.length) {
|
|
||||||
// res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
||||||
// res.end(uploadResults.join('\n'));
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res.end(uploadResults.join('\n'));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (req.method === 'POST' && req.url === '/api/app/upload') {
|
|
||||||
if (res.headersSent) return; // 如果响应已发送,不再处理
|
|
||||||
|
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
||||||
const authroization = req.headers?.['authorization'] as string;
|
|
||||||
const error = (msg: string) => {
|
const error = (msg: string) => {
|
||||||
return JSON.stringify({ code: 500, message: msg });
|
return JSON.stringify({ code: 500, message: msg });
|
||||||
};
|
};
|
||||||
|
const checkAuth = async () => {
|
||||||
|
const authroization = req.headers?.['authorization'] as string;
|
||||||
if (!authroization) {
|
if (!authroization) {
|
||||||
res.statusCode = 401;
|
res.statusCode = 401;
|
||||||
res.end(error('Invalid authorization'));
|
res.end(error('Invalid authorization'));
|
||||||
return;
|
return { tokenUser: null, token: null };
|
||||||
}
|
}
|
||||||
const token = authroization.split(' ')[1];
|
const token = authroization.split(' ')[1];
|
||||||
let tokenUser;
|
let tokenUser;
|
||||||
@ -102,8 +38,57 @@ export const uploadMiddleware = async (req: http.IncomingMessage, res: http.Serv
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.statusCode = 401;
|
res.statusCode = 401;
|
||||||
res.end(error('Invalid token'));
|
res.end(error('Invalid token'));
|
||||||
|
return { tokenUser: null, token: null };
|
||||||
|
}
|
||||||
|
return { tokenUser, token };
|
||||||
|
};
|
||||||
|
if (req.method === 'POST' && req.url === '/api/upload') {
|
||||||
|
if (res.headersSent) return; // 如果响应已发送,不再处理
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
const { tokenUser } = await checkAuth();
|
||||||
|
if (!tokenUser) return;
|
||||||
|
// 使用 formidable 解析 multipart/form-data
|
||||||
|
const form = new IncomingForm({
|
||||||
|
multiples: true, // 支持多文件上传
|
||||||
|
uploadDir: filePath, // 上传文件存储目录
|
||||||
|
});
|
||||||
|
// 解析上传的文件
|
||||||
|
form.parse(req, async (err, fields, files) => {
|
||||||
|
if (err) {
|
||||||
|
res.end(error(`Upload error: ${err.message}`));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 逐个处理每个上传的文件
|
||||||
|
const uploadedFiles = Array.isArray(files.file) ? files.file : [files.file];
|
||||||
|
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 = `${tokenUser.username}/${relativePath}`;
|
||||||
|
// 上传到 MinIO 并保留文件夹结构
|
||||||
|
const isHTML = relativePath.endsWith('.html');
|
||||||
|
await minioClient.fPutObject(bucketName, minioPath, tempPath, {
|
||||||
|
'Content-Type': getContentType(relativePath),
|
||||||
|
'app-source': 'user-files',
|
||||||
|
'Cache-Control': isHTML ? 'no-cache' : 'max-age=31536000, immutable', // 缓存一年
|
||||||
|
});
|
||||||
|
uploadResults.push({
|
||||||
|
name: relativePath,
|
||||||
|
path: minioPath,
|
||||||
|
});
|
||||||
|
fs.unlinkSync(tempPath); // 删除临时文件
|
||||||
|
}
|
||||||
|
res.end(JSON.stringify({ code: 200, data: uploadResults }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (req.method === 'POST' && req.url === '/api/app/upload') {
|
||||||
|
if (res.headersSent) return; // 如果响应已发送,不再处理
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
const { tokenUser, token } = await checkAuth();
|
||||||
|
if (!tokenUser) return;
|
||||||
//
|
//
|
||||||
// 使用 formidable 解析 multipart/form-data
|
// 使用 formidable 解析 multipart/form-data
|
||||||
const form = new IncomingForm({
|
const form = new IncomingForm({
|
||||||
@ -170,6 +155,7 @@ export const uploadMiddleware = async (req: http.IncomingMessage, res: http.Serv
|
|||||||
});
|
});
|
||||||
fs.unlinkSync(tempPath); // 删除临时文件
|
fs.unlinkSync(tempPath); // 删除临时文件
|
||||||
}
|
}
|
||||||
|
// 受控
|
||||||
const r = await app.call({
|
const r = await app.call({
|
||||||
path: 'app',
|
path: 'app',
|
||||||
key: 'uploadFiles',
|
key: 'uploadFiles',
|
||||||
@ -193,44 +179,3 @@ export const uploadMiddleware = async (req: http.IncomingMessage, res: http.Serv
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析 multipart/form-data 格式数据,提取各个字段和文件内容
|
|
||||||
* @param {Buffer} buffer - 完整的 HTTP 请求体数据
|
|
||||||
* @param {string} boundary - multipart/form-data 的 boundary 标识符
|
|
||||||
* @returns {Array} 返回包含各个部分数据的数组
|
|
||||||
*/
|
|
||||||
function parseMultipartData(buffer, boundary) {
|
|
||||||
const parts = [];
|
|
||||||
const boundaryBuffer = Buffer.from(`--${boundary}`, 'utf-8');
|
|
||||||
let start = buffer.indexOf(boundaryBuffer) + boundaryBuffer.length + 2; // Skip first boundary and \r\n
|
|
||||||
|
|
||||||
while (start < buffer.length) {
|
|
||||||
// 查找下一个 boundary 的位置
|
|
||||||
const end = buffer.indexOf(boundaryBuffer, start) - 2; // Subtract 2 to remove trailing \r\n
|
|
||||||
if (end <= start) break;
|
|
||||||
|
|
||||||
// 提取单个 part 数据
|
|
||||||
const part = buffer.slice(start, end);
|
|
||||||
start = end + boundaryBuffer.length + 2; // Move start to next part
|
|
||||||
|
|
||||||
// 分割 part 头和内容
|
|
||||||
const headerEndIndex = part.indexOf('\r\n\r\n');
|
|
||||||
const headers = part.slice(0, headerEndIndex).toString();
|
|
||||||
const content = part.slice(headerEndIndex + 4); // Skip \r\n\r\n
|
|
||||||
|
|
||||||
// 解析 headers 以获取字段名称和文件信息
|
|
||||||
const nameMatch = headers.match(/name="([^"]+)"/);
|
|
||||||
const filenameMatch = headers.match(/filename="([^"]+)"/);
|
|
||||||
|
|
||||||
const partData = {
|
|
||||||
name: nameMatch ? nameMatch[1] : null,
|
|
||||||
filename: filenameMatch ? filenameMatch[1] : null,
|
|
||||||
data: content,
|
|
||||||
};
|
|
||||||
|
|
||||||
parts.push(partData);
|
|
||||||
}
|
|
||||||
|
|
||||||
return parts;
|
|
||||||
}
|
|
||||||
|
@ -25,6 +25,7 @@ export class User extends Model {
|
|||||||
declare owner: string;
|
declare owner: string;
|
||||||
declare orgId: string;
|
declare orgId: string;
|
||||||
declare email: string;
|
declare email: string;
|
||||||
|
declare avatar: string;
|
||||||
async createToken(uid?: string) {
|
async createToken(uid?: string) {
|
||||||
const { id, username, type } = this;
|
const { id, username, type } = this;
|
||||||
const expireTime = 60 * 60 * 24 * 7; // 7 days
|
const expireTime = 60 * 60 * 24 * 7; // 7 days
|
||||||
@ -85,6 +86,7 @@ export class User extends Model {
|
|||||||
description: this.description,
|
description: this.description,
|
||||||
needChangePassword: this.needChangePassword,
|
needChangePassword: this.needChangePassword,
|
||||||
type: this.type,
|
type: this.type,
|
||||||
|
avatar: this.avatar,
|
||||||
orgs,
|
orgs,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -131,6 +133,10 @@ User.init(
|
|||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
},
|
},
|
||||||
|
avatar: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
salt: {
|
salt: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
|
@ -5,7 +5,13 @@ import { v4 as uuidv4 } from 'uuid';
|
|||||||
import { ContainerModel } from '../container/models/index.ts';
|
import { ContainerModel } from '../container/models/index.ts';
|
||||||
import { Op } from 'sequelize';
|
import { Op } from 'sequelize';
|
||||||
import { getDeck } from './module/cache-file.ts';
|
import { getDeck } from './module/cache-file.ts';
|
||||||
|
export const clearBlank = (newStyle: any) => {
|
||||||
|
for (let key in newStyle) {
|
||||||
|
if (newStyle[key] === '' || newStyle[key] === undefined || newStyle[key] === null) {
|
||||||
|
delete newStyle[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
app
|
app
|
||||||
.route({
|
.route({
|
||||||
path: 'page',
|
path: 'page',
|
||||||
@ -82,6 +88,7 @@ app
|
|||||||
.route('page', 'updateNode')
|
.route('page', 'updateNode')
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const { id, nodeData } = ctx.query.data;
|
const { id, nodeData } = ctx.query.data;
|
||||||
|
const force = ctx.query.force;
|
||||||
if (!id) {
|
if (!id) {
|
||||||
throw new CustomError('id is required');
|
throw new CustomError('id is required');
|
||||||
}
|
}
|
||||||
@ -98,16 +105,16 @@ app
|
|||||||
flag = true;
|
flag = true;
|
||||||
const { data, ...rest } = nodeItem;
|
const { data, ...rest } = nodeItem;
|
||||||
const { style, ...restData } = data || {};
|
const { style, ...restData } = data || {};
|
||||||
|
let newStyle = force ? { ...style } : { ...item?.data?.style, ...style };
|
||||||
|
clearBlank(newStyle);
|
||||||
|
console.log('newStyle', newStyle);
|
||||||
const newNodeItem = {
|
const newNodeItem = {
|
||||||
...item,
|
...item,
|
||||||
...rest,
|
...rest,
|
||||||
data: {
|
data: {
|
||||||
...item?.data,
|
...item?.data,
|
||||||
...restData,
|
...restData,
|
||||||
style: {
|
style: newStyle,
|
||||||
...item?.data?.style,
|
|
||||||
...style,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
console.log('newNodeItem', newNodeItem);
|
console.log('newNodeItem', newNodeItem);
|
||||||
|
@ -67,7 +67,7 @@ app
|
|||||||
middleware: ['auth'],
|
middleware: ['auth'],
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const { username, password, description } = ctx.query;
|
const { username, password, description, avatar, email } = ctx.query.data || {};
|
||||||
const state = ctx.state?.tokenUser || {};
|
const state = ctx.state?.tokenUser || {};
|
||||||
const { id } = state;
|
const { id } = state;
|
||||||
const user = await User.findByPk(id);
|
const user = await User.findByPk(id);
|
||||||
@ -83,6 +83,12 @@ app
|
|||||||
if (description) {
|
if (description) {
|
||||||
user.description = description;
|
user.description = description;
|
||||||
}
|
}
|
||||||
|
if (avatar) {
|
||||||
|
user.avatar = avatar;
|
||||||
|
}
|
||||||
|
if (email) {
|
||||||
|
user.email = email;
|
||||||
|
}
|
||||||
await user.save();
|
await user.save();
|
||||||
ctx.body = await user.getInfo();
|
ctx.body = await user.getInfo();
|
||||||
})
|
})
|
||||||
|
Loading…
x
Reference in New Issue
Block a user