Compare commits

...

4 Commits

Author SHA1 Message Date
ab29a1eb0b update hash and put object 2025-05-12 17:37:30 +08:00
1c4e9d9bb9 fix 2025-05-12 16:23:51 +08:00
2dc567d8d7 fix 2025-05-12 04:29:29 +08:00
bcb8ea1f30 perf 2025-05-12 02:55:53 +08:00
10 changed files with 196 additions and 32 deletions

View File

@@ -1,7 +1,6 @@
{ {
"name": "@kevisual/oss", "name": "@kevisual/oss",
"version": "0.0.1", "version": "0.0.11",
"description": "",
"main": "dist/index.js", "main": "dist/index.js",
"scripts": { "scripts": {
"build": "tsup", "build": "tsup",
@@ -16,13 +15,23 @@
"license": "MIT", "license": "MIT",
"type": "module", "type": "module",
"devDependencies": { "devDependencies": {
"dotenv": "^16.4.7", "dotenv": "^16.5.0",
"minio": "^8.0.5", "minio": "^8.0.5",
"tsup": "^8.4.0" "tsup": "^8.4.0"
}, },
"exports": { "exports": {
"./*": "./dist/*.js", ".": {
"./services": "./dist/services/index.js" "import": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./config": {
"import": "./dist/services/config.js",
"types": "./dist/services/config.d.ts"
},
"./services": {
"import": "./dist/services/index.js",
"types": "./dist/services/index.d.ts"
}
}, },
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"

View File

@@ -3,7 +3,7 @@ import { Client, CopyDestinationOptions, CopySourceOptions } from 'minio';
type CopyObjectOpts = { type CopyObjectOpts = {
bucketName: string; bucketName: string;
newMetadata: Record<string, string>; newMetadata: Record<string, string>;
prefix: string; objectName: string;
client: Client; client: Client;
}; };
/** /**
@@ -11,12 +11,14 @@ type CopyObjectOpts = {
* @param param0 * @param param0
* @returns * @returns
*/ */
export const copyObject = async ({ bucketName, newMetadata, prefix, client }: CopyObjectOpts) => { export const copyObject = async ({ bucketName, newMetadata, objectName, client }: CopyObjectOpts) => {
const source = new CopySourceOptions({ Bucket: bucketName, Object: prefix }); const source = new CopySourceOptions({ Bucket: bucketName, Object: objectName });
const stat = await client.statObject(bucketName, objectName);
const sourceMetadata = stat.metaData;
const destination = new CopyDestinationOptions({ const destination = new CopyDestinationOptions({
Bucket: bucketName, Bucket: bucketName,
Object: prefix, Object: objectName,
UserMetadata: newMetadata, UserMetadata: { ...sourceMetadata, ...newMetadata },
MetadataDirective: 'REPLACE', MetadataDirective: 'REPLACE',
}); });
const copyResult = await client.copyObject(source, destination); const copyResult = await client.copyObject(source, destination);

View File

@@ -1,12 +1,14 @@
import { Client, ItemBucketMetadata } from 'minio'; import { Client, ItemBucketMetadata } from 'minio';
import { ListFileObject, ListObjectResult, OssBaseOperation } from './core/type.ts'; import { ListFileObject, ListObjectResult, OssBaseOperation } from './core/type.ts';
import { hash } from './util/hash.ts'; import { hash } from './util/hash.ts';
import { copyObject } from './core/copy-object.ts';
import omit from 'lodash/omit.js';
import { getContentType } from './util/get-content-type.ts';
export type OssBaseOptions<T = { [key: string]: any }> = { export type OssBaseOptions<T = { [key: string]: any }> = {
/** /**
* 已经初始化好的minio client * 已经初始化好的minio client
*/ */
client?: Client; client: Client;
/** /**
* 桶名 * 桶名
*/ */
@@ -31,7 +33,7 @@ export class OssBase implements OssBaseOperation {
} }
this.bucketName = opts.bucketName; this.bucketName = opts.bucketName;
this.client = opts.client; this.client = opts.client;
this.prefix = opts.prefix; this.prefix = opts?.prefix ?? '';
} }
setPrefix(prefix: string) { setPrefix(prefix: string) {
@@ -64,15 +66,49 @@ export class OssBase implements OssBaseOperation {
}); });
}); });
} }
/**
async putObject(objectName: string, data: Buffer | string | Object, metaData?: ItemBucketMetadata) { * 上传文件, 当是流的时候中断之后的etag会变所以传递的时候不要嵌套async await例如 busboy 监听文件流内部的时候不要用check
* @param objectName
* @param data
* @param metaData
* @param options 如果文件本身存在则复制原有的meta的内容
* @returns
*/
async putObject(
objectName: string,
data: Buffer | string | Object,
metaData: ItemBucketMetadata = {},
opts?: { check?: boolean; isStream?: boolean; size?: number },
) {
let putData: Buffer | string; let putData: Buffer | string;
if (typeof data === 'object') { let size: number = opts?.size;
putData = JSON.stringify(data); const isStream = opts?.isStream;
if (!isStream) {
if (data instanceof Object) {
putData = JSON.stringify(data);
size = putData.length;
} else if (typeof data === 'string') {
putData = data;
size = putData.length;
} else {
putData = data;
}
} else { } else {
putData = data; putData = data as any;
size = null;
} }
const size = putData.length; if (opts?.check) {
const obj = await this.statObject(objectName, true);
if (obj) {
const omitMeda = ['size', 'content-type', 'cache-control', 'app-source'];
const objMeta = JSON.parse(JSON.stringify(omit(obj.metaData, omitMeda)));
metaData = {
...objMeta,
...metaData,
};
}
}
const bucketName = this.bucketName; const bucketName = this.bucketName;
const obj = await this.client.putObject(bucketName, `${this.prefix}${objectName}`, putData, size, metaData); const obj = await this.client.putObject(bucketName, `${this.prefix}${objectName}`, putData, size, metaData);
return obj; return obj;
@@ -117,10 +153,13 @@ export class OssBase implements OssBaseOperation {
return obj as any; return obj as any;
} }
async statObject(objectName: string) { async statObject(objectName: string, checkFile = true) {
const bucketName = this.bucketName; const bucketName = this.bucketName;
try { try {
const obj = await this.client.statObject(bucketName, `${this.prefix}${objectName}`); const obj = await this.client.statObject(bucketName, `${this.prefix}${objectName}`);
if (obj && checkFile && obj.size === 0) {
return null;
}
return obj; return obj;
} catch (e) { } catch (e) {
if (e.code === 'NotFound') { if (e.code === 'NotFound') {
@@ -129,13 +168,60 @@ export class OssBase implements OssBaseOperation {
throw e; throw e;
} }
} }
/**
* 检查文件hash是否一致
* @param objectName
* @param hash
* @returns
*/
async checkObjectHash(
objectName: string,
hash: string,
meta?: ItemBucketMetadata,
): Promise<{ success: boolean; metaData: ItemBucketMetadata | null; obj: any; equalMeta?: boolean }> {
const obj = await this.statObject(`${this.prefix}${objectName}`, true);
if (!obj) {
return { success: false, metaData: null, obj: null, equalMeta: false };
}
let metaData: ItemBucketMetadata = {};
const omitMeda = ['content-type', 'cache-control', 'app-source'];
const objMeta = omit(obj.metaData, omitMeda);
metaData = {
...objMeta,
};
let equalMeta = false;
if (meta) {
equalMeta = JSON.stringify(metaData) === JSON.stringify(meta);
}
return { success: obj.etag === hash, metaData, obj, equalMeta };
}
getMetadata(pathname: string, meta: ItemBucketMetadata = { 'app-source': 'user-app' }) {
const isHtml = pathname.endsWith('.html');
if (isHtml) {
meta = {
...meta,
'content-type': 'text/html; charset=utf-8',
'cache-control': 'no-cache',
};
} else {
meta = {
...meta,
'content-type': getContentType(pathname),
'cache-control': 'max-age=31536000, immutable',
};
}
return meta;
}
async copyObject(sourceObject: any, targetObject: any) { async copyObject(sourceObject: any, targetObject: any) {
const bucketName = this.bucketName; const bucketName = this.bucketName;
const obj = await this.client.copyObject(bucketName, sourceObject, targetObject); const obj = await this.client.copyObject(bucketName, sourceObject, targetObject);
return obj; return obj;
} }
async replaceObject(objectName: string, meta: { [key: string]: string }) {
const { bucketName, client } = this;
return copyObject({ bucketName, client, objectName: `${this.prefix}${objectName}`, newMetadata: meta });
}
static create<T extends OssBase, U>(this: new (opts: OssBaseOptions<U>) => T, opts: OssBaseOptions<U>): T { static create<T extends OssBase, U>(this: new (opts: OssBaseOptions<U>) => T, opts: OssBaseOptions<U>): T {
return new this(opts); return new this(opts);
} }

View File

@@ -1,2 +1,9 @@
export { OssBase, OssBaseOptions } from '../index.ts'; export { OssBase } from '../index.ts';
export type { OssBaseOptions } from '../index.ts';
export { ConfigOssService } from './config.ts'; export { ConfigOssService } from './config.ts';
export * from '../util/download.ts';
export * from '../util/index.ts';
export * from '../core/type.ts';

View File

@@ -4,9 +4,16 @@ import { Client } from 'minio';
import path from 'path'; import path from 'path';
import { downloadObject } from '../util/download.ts'; import { downloadObject } from '../util/download.ts';
const cwd = process.cwd(); const cwd = process.cwd();
dotenv.config({ path: path.resolve(cwd, '..', '..', '.env') }); dotenv.config({ path: path.resolve(cwd, '..', '..', '.env.dev') });
console.log(process.env.MINIO_ENDPOINT, process.env.MINIO_USE_SSL, process.env.MINIO_ACCESS_KEY, process.env.MINIO_SECRET_KEY, process.env.MINIO_BUCKET_NAME); console.log(
'config',
process.env.MINIO_ENDPOINT,
process.env.MINIO_USE_SSL,
process.env.MINIO_ACCESS_KEY,
process.env.MINIO_SECRET_KEY,
process.env.MINIO_BUCKET_NAME,
);
const client = new Client({ const client = new Client({
endPoint: process.env.MINIO_ENDPOINT, endPoint: process.env.MINIO_ENDPOINT,
useSSL: process.env.MINIO_USE_SSL === 'true', useSSL: process.env.MINIO_USE_SSL === 'true',
@@ -25,7 +32,7 @@ const main = async () => {
console.log(config); console.log(config);
}; };
// main(); main();
const putJson = async () => { const putJson = async () => {
const config = await configOssService.putObject( const config = await configOssService.putObject(
'a.json', 'a.json',
@@ -50,7 +57,7 @@ const downloadMain = async () => {
}); });
// console.log(objectStream); // console.log(objectStream);
}; };
downloadMain(); // downloadMain();
const statInfo = async () => { const statInfo = async () => {
try { try {

View File

@@ -1,7 +1,7 @@
import { ServerResponse } from 'http'; import { ServerResponse } from 'node:http';
import { BucketItemStat } from 'minio'; import { BucketItemStat } from 'minio';
import fs from 'fs'; import fs from 'node:fs';
import path from 'path'; import path from 'node:path';
const viewableExtensions = ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp', 'mp4', 'webm', 'mp3', 'wav', 'ogg', 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx']; const viewableExtensions = ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp', 'mp4', 'webm', 'mp3', 'wav', 'ogg', 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'];
import { OssBase } from '../index.ts'; import { OssBase } from '../index.ts';

View File

@@ -0,0 +1,49 @@
import path from 'node:path';
// 获取文件的 content-type
export const getContentType = (filePath: string) => {
const extname = path.extname(filePath);
const contentType = {
'.html': 'text/html; charset=utf-8',
'.js': 'text/javascript; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.txt': 'text/plain; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.png': 'image/png',
'.jpg': 'image/jpg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.wav': 'audio/wav',
'.mp4': 'video/mp4',
'.md': 'text/markdown; charset=utf-8', // utf-8配置
'.ico': 'image/x-icon', // Favicon 图标
'.webp': 'image/webp', // WebP 图像格式
'.webm': 'video/webm', // WebM 视频格式
'.ogg': 'audio/ogg', // Ogg 音频格式
'.mp3': 'audio/mpeg', // MP3 音频格式
'.m4a': 'audio/mp4', // M4A 音频格式
'.m3u8': 'application/vnd.apple.mpegurl', // HLS 播放列表
'.ts': 'video/mp2t', // MPEG Transport Stream
'.pdf': 'application/pdf', // PDF 文档
'.doc': 'application/msword', // Word 文档
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // Word 文档 (新版)
'.ppt': 'application/vnd.ms-powerpoint', // PowerPoint 演示文稿
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', // PowerPoint (新版)
'.xls': 'application/vnd.ms-excel', // Excel 表格
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // Excel 表格 (新版)
'.csv': 'text/csv; charset=utf-8', // CSV 文件
'.xml': 'application/xml; charset=utf-8', // XML 文件
'.rtf': 'application/rtf', // RTF 文本文件
'.eot': 'application/vnd.ms-fontobject', // Embedded OpenType 字体
'.ttf': 'font/ttf', // TrueType 字体
'.woff': 'font/woff', // Web Open Font Format 1.0
'.woff2': 'font/woff2', // Web Open Font Format 2.0
'.otf': 'font/otf', // OpenType 字体
'.wasm': 'application/wasm', // WebAssembly 文件
'.pem': 'application/x-pem-file', // PEM 证书文件
'.crt': 'application/x-x509-ca-cert', // CRT 证书文件
'.yaml': 'application/x-yaml; charset=utf-8', // YAML 文件
'.yml': 'application/x-yaml; charset=utf-8', // YAML 文件(别名)
'.zip': 'application/octet-stream',
};
return contentType[extname] || 'application/octet-stream';
};

View File

@@ -1,4 +1,4 @@
import crypto from 'crypto'; import crypto from 'node:crypto';
// 582af9ef5cdc53d6628f45cb842f874a // 582af9ef5cdc53d6628f45cb842f874a
// const hashStr = '{"a":"a"}'; // const hashStr = '{"a":"a"}';
// const hash = crypto.createHash('md5').update(hashStr).digest('hex'); // const hash = crypto.createHash('md5').update(hashStr).digest('hex');
@@ -11,7 +11,9 @@ import crypto from 'crypto';
*/ */
export const hash = (str: string | Buffer | Object) => { export const hash = (str: string | Buffer | Object) => {
let hashStr: string | Buffer; let hashStr: string | Buffer;
if (typeof str === 'object') { if (str instanceof Buffer) {
hashStr = str;
} else if (str instanceof Object) {
hashStr = JSON.stringify(str, null, 2); hashStr = JSON.stringify(str, null, 2);
} else { } else {
hashStr = str; hashStr = str;

View File

@@ -1 +1,3 @@
export * from './hash.ts'; export * from './hash.ts';
export * from './get-content-type.ts';

View File

@@ -4,7 +4,7 @@ const services = glob.sync('src/services/*.ts');
export default defineConfig({ export default defineConfig({
entry: ['src/index.ts', ...services], entry: ['src/index.ts', ...services],
target: 'node22',
splitting: false, splitting: false,
sourcemap: false, sourcemap: false,
clean: true, clean: true,