feat: add me

This commit is contained in:
xion 2024-10-14 16:23:29 +08:00
parent 1eb65859e6
commit 64f89b60dc
5 changed files with 82 additions and 118 deletions

@ -1 +1 @@
Subproject commit f0468539dc53f89a509187a95ba9823702f62105
Subproject commit c92f817d667e33e8fcdfad4d44f4a63df0534db5

View File

@ -3,13 +3,10 @@ import http from 'http';
import fs, { rm } from 'fs';
import path from 'path';
import { IncomingForm } from 'formidable';
import { checkToken } from '@abearxiong/auth';
import { useConfig } from '@abearxiong/use-config';
import { app, minioClient } from '@/app.ts';
import { bucketName } from '@/modules/minio.ts';
import { getContentType } from '@/utils/get-content-type.ts';
import { User } from '@/models/user.ts';
const { tokenSecret } = useConfig<{ tokenSecret: string }>();
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 \
@ -24,76 +21,15 @@ export const uploadMiddleware = async (req: http.IncomingMessage, res: http.Serv
res.end('Upload API is ready');
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) => {
return JSON.stringify({ code: 500, message: msg });
};
const checkAuth = async () => {
const authroization = req.headers?.['authorization'] as string;
if (!authroization) {
res.statusCode = 401;
res.end(error('Invalid authorization'));
return;
return { tokenUser: null, token: null };
}
const token = authroization.split(' ')[1];
let tokenUser;
@ -102,8 +38,57 @@ export const uploadMiddleware = async (req: http.IncomingMessage, res: http.Serv
} catch (e) {
res.statusCode = 401;
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;
}
// 逐个处理每个上传的文件
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
const form = new IncomingForm({
@ -170,6 +155,7 @@ export const uploadMiddleware = async (req: http.IncomingMessage, res: http.Serv
});
fs.unlinkSync(tempPath); // 删除临时文件
}
// 受控
const r = await app.call({
path: 'app',
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;
}

View File

@ -25,6 +25,7 @@ export class User extends Model {
declare owner: string;
declare orgId: string;
declare email: string;
declare avatar: string;
async createToken(uid?: string) {
const { id, username, type } = this;
const expireTime = 60 * 60 * 24 * 7; // 7 days
@ -85,6 +86,7 @@ export class User extends Model {
description: this.description,
needChangePassword: this.needChangePassword,
type: this.type,
avatar: this.avatar,
orgs,
};
}
@ -131,6 +133,10 @@ User.init(
type: DataTypes.STRING,
allowNull: true,
},
avatar: {
type: DataTypes.STRING,
allowNull: true,
},
salt: {
type: DataTypes.STRING,
allowNull: true,

View File

@ -5,7 +5,13 @@ import { v4 as uuidv4 } from 'uuid';
import { ContainerModel } from '../container/models/index.ts';
import { Op } from 'sequelize';
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
.route({
path: 'page',
@ -82,6 +88,7 @@ app
.route('page', 'updateNode')
.define(async (ctx) => {
const { id, nodeData } = ctx.query.data;
const force = ctx.query.force;
if (!id) {
throw new CustomError('id is required');
}
@ -98,16 +105,16 @@ app
flag = true;
const { data, ...rest } = nodeItem;
const { style, ...restData } = data || {};
let newStyle = force ? { ...style } : { ...item?.data?.style, ...style };
clearBlank(newStyle);
console.log('newStyle', newStyle);
const newNodeItem = {
...item,
...rest,
data: {
...item?.data,
...restData,
style: {
...item?.data?.style,
...style,
},
style: newStyle,
},
};
console.log('newNodeItem', newNodeItem);

View File

@ -67,7 +67,7 @@ app
middleware: ['auth'],
})
.define(async (ctx) => {
const { username, password, description } = ctx.query;
const { username, password, description, avatar, email } = ctx.query.data || {};
const state = ctx.state?.tokenUser || {};
const { id } = state;
const user = await User.findByPk(id);
@ -83,6 +83,12 @@ app
if (description) {
user.description = description;
}
if (avatar) {
user.avatar = avatar;
}
if (email) {
user.email = email;
}
await user.save();
ctx.body = await user.getInfo();
})