Refactor storage integration from MinIO to S3

- Removed MinIO client and related imports from various modules.
- Introduced S3 client and OSS integration for object storage.
- Updated all references to MinIO methods with corresponding S3 methods.
- Added new flowme table schema to the database.
- Adjusted upload and download routes to utilize S3 for file operations.
- Removed obsolete MinIO-related files and routes.
- Ensured compatibility with existing application logic while transitioning to S3.
This commit is contained in:
2026-01-31 05:12:56 +08:00
parent 08023d6878
commit 6100e9833d
24 changed files with 1440 additions and 328 deletions

View File

@@ -4,8 +4,7 @@ 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, minioClient } from '@/app.ts';
import { bucketName } from '@/modules/minio.ts';
import { app, oss } from '@/app.ts';
import { getContentType } from '@/utils/get-content-type.ts';
import path from 'path';
import { createWriteStream } from 'fs';
@@ -123,7 +122,7 @@ router.post('/api/micro-app/upload', async (req, res) => {
const minioPath = `private/${tokenUser.username}/${appKey}/${relativePath}`;
// 上传到 MinIO 并保留文件夹结构
const isHTML = relativePath.endsWith('.html');
await minioClient.fPutObject(bucketName, minioPath, tempPath, {
await oss.fPutObject(minioPath, tempPath, {
'Content-Type': getContentType(relativePath),
'app-source': 'user-micro-app',
'Cache-Control': isHTML ? 'no-cache' : 'max-age=31536000, immutable', // 缓存一年

View File

@@ -2,9 +2,8 @@ import { useFileStore } from '@kevisual/use-config';
import http from 'node:http';
import fs from 'fs';
import Busboy from 'busboy';
import { app, minioClient } from '@/app.ts';
import { app, oss } from '@/app.ts';
import { bucketName } from '@/modules/minio.ts';
import { getContentType } from '@/utils/get-content-type.ts';
import { User } from '@/models/user.ts';
import { router, error, checkAuth, writeEvents } from './router.ts';
@@ -151,7 +150,7 @@ router.post('/api/app/upload', async (req, res) => {
const minioPath = `${username || tokenUser.username}/${appKey}/${version}/${relativePath}`;
// 上传到 MinIO 并保留文件夹结构
const isHTML = relativePath.endsWith('.html');
await minioClient.fPutObject(bucketName, minioPath, tempPath, {
await oss.fPutObject(minioPath, tempPath, {
'Content-Type': getContentType(relativePath),
'app-source': 'user-app',
'Cache-Control': isHTML ? 'no-cache' : 'max-age=31536000, immutable', // 缓存一年

View File

@@ -1,18 +0,0 @@
import { minioClient } from '@/app.ts';
import { bucketName } from '@/modules/minio.ts';
import { router } from '../router.ts';
router.post('/api/minio', async (ctx) => {
let { username, appKey } = { username: '', appKey: '' };
const path = `${username}/${appKey}`;
const res = await minioClient.listObjectsV2(bucketName, path, true);
const file = res.filter((item) => item.isFile);
const fileList = file.map((item) => {
return {
name: item.name,
size: item.size,
};
});
// ctx.body = fileList;
});

View File

@@ -1,107 +0,0 @@
/**
* 更新时间2025-03-17
* 第二次更新2025-03-22
*/
import { minioClient } from '@/app.ts';
import { IncomingMessage, ServerResponse } from 'http';
import { bucketName } from '@/modules/minio.ts';
import { getLoginUser } from '../middleware/auth.ts';
import { BucketItemStat } from 'minio';
import { UserPermission, Permission } from '@kevisual/permission';
import { pipeMinioStream } from '@/modules/fm-manager/index.ts';
/**
* 过滤 metaData 中的 key, 去除 password, accesskey, secretkey
* 并返回过滤后的 metaData
* @param metaData
* @returns
*/
const filterKeys = (metaData: Record<string, string>, clearKeys: string[] = []) => {
const keys = Object.keys(metaData);
// remove X-Amz- meta data
const removeKeys = ['password', 'accesskey', 'secretkey', ...clearKeys];
const filteredKeys = keys.filter((key) => !removeKeys.includes(key));
return filteredKeys.reduce((acc, key) => {
acc[key] = metaData[key];
return acc;
}, {} as Record<string, string>);
};
export const NotFoundFile = (res: ServerResponse, msg?: string, code = 404) => {
res.writeHead(code, { 'Content-Type': 'text/plain' });
res.end(msg || 'Not Found File');
return;
};
export const shareType = ['public', 'private', 'protected'] as const;
export type ShareType = (typeof shareType)[number];
export const authMinio = async (req: IncomingMessage, res: ServerResponse, objectName: string) => {
let stat: BucketItemStat;
try {
stat = await minioClient.statObject(bucketName, objectName);
} catch (e) {
return NotFoundFile(res);
}
const [userKey, ...rest] = objectName.split('/');
const _url = new URL(req.url || '', 'http://localhost');
const password = _url.searchParams.get('p') || '';
const isDownload = !!_url.searchParams.get('download');
const metaData = stat.metaData || {};
const filteredMetaData = filterKeys(metaData, ['size', 'etag', 'last-modified']);
if (stat.size === 0) {
return NotFoundFile(res);
}
const { tokenUser } = await getLoginUser(req);
const username = tokenUser?.username;
const owner = userKey;
const permission = new UserPermission({
permission: metaData as Permission,
owner,
});
const checkPermissionResult = permission.checkPermissionSuccess({
username,
password,
});
if (!checkPermissionResult.success) {
return NotFoundFile(res, checkPermissionResult.message, checkPermissionResult.code);
}
const contentLength = stat.size;
const etag = stat.etag;
const lastModified = stat.lastModified.toISOString();
const filename = objectName.split('/').pop() || 'no-file-name-download'; // Extract filename from objectName
const fileExtension = filename.split('.').pop()?.toLowerCase() || '';
const viewableExtensions = [
'jpg',
'jpeg',
'png',
'gif',
'svg',
'webp',
'mp4',
'webm',
'mp3',
'wav',
'ogg',
'pdf',
'doc',
'docx',
'xls',
'xlsx',
'ppt',
'pptx',
];
const contentDisposition = viewableExtensions.includes(fileExtension) && !isDownload ? 'inline' : `attachment; filename="${filename}"`;
res.writeHead(200, {
'Content-Length': contentLength,
etag,
'last-modified': lastModified,
'Content-Disposition': contentDisposition,
...filteredMetaData,
});
const objectStream = await minioClient.getObject(bucketName, objectName);
// objectStream.pipe(res, { end: true });
pipeMinioStream(objectStream, res);
};

View File

@@ -161,9 +161,8 @@ router.post('/api/s1/resources/upload/chunk', async (req, res) => {
if (share) {
metadata.share = 'public';
}
const bucketName = oss.bucketName;
// All chunks uploaded, now upload to MinIO
await oss.client.fPutObject(bucketName, minioPath, finalFilePath, {
await oss.fPutObject(minioPath, finalFilePath, {
'Content-Type': getContentType(relativePath),
'app-source': 'user-app',
'Cache-Control': relativePath.endsWith('.html') ? 'no-cache' : 'max-age=31536000, immutable',

View File

@@ -1,19 +0,0 @@
import { router } from '@/app.ts';
import { authMinio } from '../minio/get-minio-resource.ts';
// 功能可以抽离为某一个dns请求的服务
router.all('/api/s1/share/*splat', async (req, res) => {
try {
const url = req.url;
const _url = new URL(url || '', 'http://localhost');
let objectName = _url.pathname.replace('/api/s1/share/', '');
objectName = decodeURIComponent(objectName);
await authMinio(req, res, objectName);
} catch (e) {
console.log('get share resource error url', req.url);
console.error('get share resource is error.', e.message);
res.end('get share resource is error.');
}
});

View File

@@ -1,9 +1,8 @@
import { useFileStore } from '@kevisual/use-config';
import { checkAuth, error, router, writeEvents, getKey } from '../router.ts';
import Busboy from 'busboy';
import { app, minioClient } from '@/app.ts';
import { app } from '@/app.ts';
import { bucketName } from '@/modules/minio.ts';
import { getContentType } from '@/utils/get-content-type.ts';
import { User } from '@/models/user.ts';
import fs from 'fs';
@@ -15,6 +14,7 @@ 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 });
@@ -233,7 +233,7 @@ router.post('/api/s1/resources/upload', async (req, res) => {
metadata.share = 'public';
}
Object.assign(metadata, meta);
await minioClient.fPutObject(bucketName, minioPath, tempPath, {
await oss.fPutObject(minioPath, tempPath, {
'Content-Type': getContentType(relativePath),
'app-source': 'user-app',
'Cache-Control': isHTML ? 'no-cache' : 'max-age=31536000, immutable', // 缓存一年