feat: 上传资源和下载资源更新
This commit is contained in:
12
src/routes/app-manager/export.ts
Normal file
12
src/routes/app-manager/export.ts
Normal 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;
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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'; // 可以做到网页代理
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}`,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user