feat: 上传资源和下载资源更新

This commit is contained in:
2025-03-20 02:29:26 +08:00
parent 9b1045d456
commit 0179fe73a3
19 changed files with 747 additions and 225 deletions

View File

@@ -0,0 +1,12 @@
import { app } from '@/app.ts';
export const callDetectAppVersion = async ({ appKey, version, username }: { appKey: string; version: string; username: string }, token: string) => {
const res = await app.call({
path: 'app',
key: 'detect-version-list',
payload: {
token: token,
data: { appKey, version, username },
},
});
return res;
};

View File

@@ -309,20 +309,27 @@ app
})
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
let { key, version, username } = ctx.query?.data || {};
if (!key || !version) {
throw new CustomError('key and version are required');
let { appKey, version, username } = ctx.query?.data || {};
if (!appKey || !version) {
throw new CustomError('appKey and version are required');
}
const uid = await getUidByUsername(app, ctx, username);
const appList = await AppListModel.findOne({ where: { key, version, uid: uid } });
let appList = await AppListModel.findOne({ where: { key: appKey, version, uid } });
if (!appList) {
throw new CustomError('app not found');
appList = await AppListModel.create({
key: appKey,
version,
uid,
data: {
files: [],
},
});
}
const checkUsername = username || tokenUser.username;
const files = await getMinioListAndSetToAppList({ username: checkUsername, appKey: key, version });
const files = await getMinioListAndSetToAppList({ username: checkUsername, appKey, version });
const newFiles = files.map((item) => {
return {
name: item.name.replace(`${checkUsername}/${key}/${version}/`, ''),
name: item.name.replace(`${checkUsername}/${appKey}/${version}/`, ''),
path: item.name,
};
});
@@ -330,17 +337,31 @@ app
const needAddFiles = newFiles.map((item) => {
const findFile = appListFiles.find((appListFile) => appListFile.name === item.name);
if (findFile && findFile.path === item.path) {
return findFile;
return { ...findFile, ...item };
}
return item;
});
await appList.update({ data: { files: needAddFiles } });
setExpire(appList.id, 'test');
const appModel = await AppModel.findOne({ where: { key, version, uid: uid } });
if (appModel) {
await appModel.update({ data: { files: needAddFiles } });
setExpire(appModel.key, appModel.user);
let am = await AppModel.findOne({ where: { key: appKey, uid } });
if (!am) {
am = await AppModel.create({
title: appKey,
key: appKey,
version: version || '0.0.0',
user: checkUsername,
uid,
data: { files: needAddFiles },
proxy: true,
});
} else {
const appModel = await AppModel.findOne({ where: { key: appKey, version, uid } });
if (appModel) {
await appModel.update({ data: { files: needAddFiles } });
setExpire(appModel.key, appModel.user);
}
}
ctx.body = appList;
})
.addTo(app);

View File

@@ -2,13 +2,21 @@ import { sequelize } from '../../../modules/sequelize.ts';
import { DataTypes, Model } from 'sequelize';
type AppPermissionType = 'public' | 'private' | 'protected';
/**
* 共享设置
* 1. 设置公共可以直接访问
* 2. 设置受保护需要登录后访问
* 3. 设置私有只有自己可以访问。\n
* 受保护可以设置密码,设置访问的用户名。切换共享状态后,需要重新设置密码和用户名。
*/
export interface AppData {
files: { name: string; path: string }[];
permission?: {
// 访问权限
type: AppPermissionType; // public, private(Only Self), protected(protected, 通过配置访问)
users?: string[];
orgs?: string[];
// 访问权限, 字段和minio的权限配置一致
share: AppPermissionType; // public, private(Only Self), protected(protected, 通过配置访问)
usernames?: string; // 受保护的访问用户名,多个用逗号分隔
password?: string; // 受保护的访问密码
'expiration-time'?: string; // 受保护的访问过期时间
};
}
export type AppType = 'web-single' | 'web-module'; // 可以做到网页代理

View File

@@ -85,7 +85,7 @@ export class ConfigModel extends Model {
prefix,
};
}
static async setUploadConfig(opts: { uid: string; data: any }) {
static async setUploadConfig(opts: { uid: string; data: { key?: string; version?: string } }) {
const config = await ConfigModel.setConfig('upload', {
uid: opts.uid,
data: opts.data,

View File

@@ -8,23 +8,36 @@ app
middleware: ['auth'],
})
.define(async (ctx) => {
const { id } = ctx.state.tokenUser;
const tokenUser = ctx.state.tokenUser;
const config = await ConfigModel.getUploadConfig({
uid: id,
uid: tokenUser.id,
});
ctx.body = config;
const key = config?.config?.data?.key || '';
const version = config?.config?.data?.version || '';
const username = tokenUser.username;
const prefix = `${key}/${version}/`;
ctx.body = {
key,
version,
username,
prefix,
};
})
.addTo(app);
app
.route({
path: 'config',
key: 'setUploadConfig',
key: 'updateUploadConfig',
middleware: ['auth'],
})
.define(async (ctx) => {
const { id } = ctx.state.tokenUser;
const data = ctx.query.data || {};
const { key, version } = data;
if (!key && !version) {
ctx.throw(400, 'key or version is required');
}
const config = await ConfigModel.setUploadConfig({
uid: id,
data,

View File

@@ -1,109 +1,36 @@
import { CustomError } from '@kevisual/router';
import { app } from '../../app.ts';
import { ContainerModel, ContainerData, Container } from './models/index.ts';
import { uploadMinioContainer } from '../page/module/cache-file.ts';
const list = app.route({
path: 'container',
key: 'list',
middleware: ['auth'],
});
list.run = async (ctx) => {
const tokenUser = ctx.state.tokenUser;
const list = await ContainerModel.findAll({
order: [['updatedAt', 'DESC']],
where: {
uid: tokenUser.id,
},
});
ctx.body = list;
return ctx;
};
list.addTo(app);
app
.route({
path: 'container',
key: 'list',
middleware: ['auth'],
})
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
const list = await ContainerModel.findAll({
order: [['updatedAt', 'DESC']],
where: {
uid: tokenUser.id,
},
attributes: { exclude: ['code'] },
});
ctx.body = list;
return ctx;
})
.addTo(app);
app
.route({
path: 'container',
key: 'get',
})
.define(async (ctx) => {
const id = ctx.query.id;
if (!id) {
throw new CustomError('id is required');
}
ctx.body = await ContainerModel.findByPk(id);
return ctx;
})
.addTo(app);
const add = app.route({
path: 'container',
key: 'update',
middleware: ['auth'],
});
add.run = async (ctx) => {
const tokenUser = ctx.state.tokenUser;
const data = ctx.query.data;
const container = {
...data,
};
let containerModel: ContainerModel | null = null;
if (container.id) {
containerModel = await ContainerModel.findByPk(container.id);
if (containerModel) {
containerModel.update({
...container,
publish: {
...containerModel.publish,
...container.publish,
},
});
await containerModel.save();
}
} else {
try {
containerModel = await ContainerModel.create({
...container,
uid: tokenUser.id,
});
} catch (e) {
console.log('error', e);
}
console.log('containerModel', container);
}
ctx.body = containerModel;
return ctx;
};
add.addTo(app);
const deleteRoute = app.route({
path: 'container',
key: 'delete',
});
deleteRoute.run = async (ctx) => {
const id = ctx.query.id;
const container = await ContainerModel.findByPk(id);
if (container) {
await container.destroy();
}
ctx.body = container;
return ctx;
};
deleteRoute.addTo(app);
app
.route({
path: 'container',
key: 'publish',
// nextRoute: { path: 'resource', key: 'publishContainer' },
middleware: ['auth'],
})
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
const { data, token } = ctx.query;
const { id, publish } = data;
const id = ctx.query.id;
if (!id) {
throw new CustomError('id is required');
}
@@ -111,36 +38,71 @@ app
if (!container) {
throw new CustomError('container not found');
}
container.publish = publish;
await container.save();
const { title, description, key, version, fileName, saveHTML } = publish;
if (container.uid !== tokenUser.id) {
throw new CustomError('container not found');
}
ctx.body = container;
if (!key || !version || !fileName) {
return;
}
if (container.type === 'render-js') {
const uploadResult = await uploadMinioContainer({
key,
tokenUser: ctx.state.tokenUser,
version: version,
code: container.code,
filePath: fileName,
saveHTML,
});
await ctx.call({
path: 'app',
key: 'uploadFiles',
payload: {
token,
data: {
appKey: key,
version,
files: uploadResult,
},
},
});
} else {
ctx.throw(500, 'container type not supported:' + container.type);
}
return ctx;
})
.addTo(app);
app
.route({
path: 'container',
key: 'update',
middleware: ['auth'],
})
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
const data = ctx.query.data;
const { id, ...container } = data;
let containerModel: ContainerModel | null = null;
if (id) {
containerModel = await ContainerModel.findByPk(id);
if (containerModel) {
containerModel.update({
...container,
publish: {
...containerModel.publish,
...container.publish,
},
});
await containerModel.save();
}
} else {
try {
containerModel = await ContainerModel.create({
...container,
uid: tokenUser.id,
});
} catch (e) {
console.log('error', e);
}
console.log('containerModel', container);
}
ctx.body = containerModel;
return ctx;
})
.addTo(app);
app
.route({
path: 'container',
key: 'delete',
middleware: ['auth'],
})
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
const id = ctx.query.id;
const container = await ContainerModel.findByPk(id);
if (!container) {
throw new CustomError('container not found');
}
if (container.uid !== tokenUser.id) {
throw new CustomError('container not found');
}
await container.destroy();
ctx.body = container;
return ctx;
})
.addTo(app);

View File

@@ -1,9 +1,15 @@
import { app } from '@/app.ts';
import { getFileStat, getMinioList } from './module/get-minio-list.ts';
import { getFileStat, getMinioList, deleteFile, updateFileStat } from './module/get-minio-list.ts';
import path from 'path';
import { CustomError } from '@kevisual/router';
import { get } from 'http';
import { callDetectAppVersion } from '../app-manager/export.ts';
/**
* 清理prefix中的'..'
* @param prefix
* @returns
*/
const handlePrefix = (prefix: string) => {
// 清理所有的 '..'
if (!prefix) return '';
@@ -94,3 +100,50 @@ app
};
})
.addTo(app);
app
.route({
path: 'file',
key: 'delete',
middleware: ['auth'],
})
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
const data = ctx.query.data || {};
const { prefix } = getPrefixByUser(data, tokenUser);
const [username, appKey, version] = prefix.slice(1).split('/');
const res = await deleteFile(prefix.slice(1));
if (res.code === 200) {
ctx.body = 'delete success';
} else {
ctx.throw(500, res.message || 'delete failed');
}
const r = await callDetectAppVersion({ appKey, version, username }, ctx.query.token);
if (r.code !== 200) {
console.error('callDetectAppVersion failed', r, prefix);
}
})
.addTo(app);
app
.route({
path: 'file',
key: 'update-metadata',
middleware: ['auth'],
})
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
const data = ctx.query.data || {};
if (!data.metadata || JSON.stringify(data.metadata) === '{}') {
ctx.throw(400, 'metadata is required');
}
const { prefix } = getPrefixByUser(data, tokenUser);
const res = await updateFileStat(prefix.slice(1), data.metadata);
if (res.code === 200) {
ctx.body = 'update metadata success';
} else {
ctx.throw(500, res.message || 'update metadata failed');
}
return ctx;
})
.addTo(app);

View File

@@ -1,6 +1,6 @@
import { minioClient } from '@/app.ts';
import { bucketName } from '@/modules/minio.ts';
import { CopyDestinationOptions, CopySourceOptions } from 'minio';
type MinioListOpt = {
prefix: string;
recursive?: boolean;
@@ -41,9 +41,12 @@ export const getMinioList = async (opts: MinioListOpt): Promise<MinioList> => {
});
});
};
export const getFileStat = async (prefix: string): Promise<any> => {
export const getFileStat = async (prefix: string, isFile?: boolean): Promise<any> => {
try {
const obj = await minioClient.statObject(bucketName, prefix);
if (isFile && obj.size === 0) {
return null;
}
return obj;
} catch (e) {
if (e.code === 'NotFound') {
@@ -54,16 +57,30 @@ export const getFileStat = async (prefix: string): Promise<any> => {
}
};
export const deleteFile = async (prefix: string): Promise<any> => {
export const deleteFile = async (prefix: string): Promise<{ code: number; message: string }> => {
try {
const fileStat = await getFileStat(prefix);
if (!fileStat) {
console.warn(`File not found: ${prefix}`);
return {
code: 404,
message: 'file not found',
};
}
await minioClient.removeObject(bucketName, prefix, {
versionId: 'null',
forceDelete: true,
forceDelete: true, // 强制删除
});
return true;
return {
code: 200,
message: 'delete success',
};
} catch (e) {
console.error('delete File Error not handle', e);
return false;
return {
code: 500,
message: 'delete failed',
};
}
};
@@ -90,3 +107,43 @@ export const getMinioListAndSetToAppList = async (opts: GetMinioListAndSetToAppL
const files = minioList;
return files as MinioFile[];
};
/**
* 更新文件的元数据
* @param prefix 文件前缀
* @param newMetadata 新的元数据
* @returns
*/
export const updateFileStat = async (
prefix: string,
newMetadata: Record<string, string>,
): Promise<{
code: number;
data: any;
message?: string;
}> => {
try {
const source = new CopySourceOptions({ Bucket: bucketName, Object: prefix });
const destination = new CopyDestinationOptions({
Bucket: bucketName,
Object: prefix,
UserMetadata: newMetadata,
MetadataDirective: 'REPLACE',
});
const copyResult = await minioClient.copyObject(source, destination);
console.log('copyResult', copyResult);
console.log(`Metadata for ${prefix} updated successfully.`);
return {
code: 200,
data: copyResult,
message: 'update metadata success',
};
} catch (e) {
console.error('Error updating file stat', e);
return {
code: 500,
data: null,
message: `update metadata failed. ${e.message}`,
};
}
};