kevisual-oss/src/index.ts
2025-05-12 17:37:30 +08:00

239 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Client, ItemBucketMetadata } from 'minio';
import { ListFileObject, ListObjectResult, OssBaseOperation } from './core/type.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 }> = {
/**
* 已经初始化好的minio client
*/
client: Client;
/**
* 桶名
*/
bucketName: string;
/**
* 前缀
*/
prefix?: string;
} & T;
export class OssBase implements OssBaseOperation {
client?: Client;
bucketName: string;
prefix = '';
/**
* 计算字符串或者对象的的md5值
*/
hash = hash;
constructor(opts: OssBaseOptions) {
if (!opts.client) {
throw new Error('client is required');
}
this.bucketName = opts.bucketName;
this.client = opts.client;
this.prefix = opts?.prefix ?? '';
}
setPrefix(prefix: string) {
this.prefix = prefix;
}
async getObject(objectName: string) {
const bucketName = this.bucketName;
const obj = await this.client.getObject(bucketName, `${this.prefix}${objectName}`);
return obj;
}
async getJson(objectName: string): Promise<Record<string, any>> {
const obj = await this.getObject(objectName);
return new Promise((resolve, reject) => {
let data = '';
obj.on('data', (chunk) => {
data += chunk;
});
obj.on('end', () => {
try {
const jsonData = JSON.parse(data);
resolve(jsonData);
} catch (error) {
reject(new Error('Failed to parse JSON'));
}
});
obj.on('error', (err) => {
reject(err);
});
});
}
/**
* 上传文件, 当是流的时候中断之后的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 size: number = opts?.size;
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 {
putData = data as any;
size = null;
}
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 obj = await this.client.putObject(bucketName, `${this.prefix}${objectName}`, putData, size, metaData);
return obj;
}
async deleteObject(objectName: string) {
const bucketName = this.bucketName;
const obj = await this.client.removeObject(bucketName, `${this.prefix}${objectName}`);
return obj;
}
async listObjects<IS_FILE = false>(objectName: string, opts?: { recursive?: boolean; startAfter?: string }) {
const bucketName = this.bucketName;
const prefix = `${this.prefix}${objectName}`;
const res = await new Promise((resolve, reject) => {
let res: any[] = [];
let hasError = false;
this.client
.listObjectsV2(bucketName, prefix, opts?.recursive ?? false, opts?.startAfter)
.on('data', (data) => {
res.push(data);
})
.on('error', (err) => {
console.error('minio error', prefix, err);
hasError = true;
})
.on('end', () => {
if (hasError) {
reject();
return;
} else {
resolve(res);
}
});
});
return res as IS_FILE extends true ? ListFileObject[] : ListObjectResult[];
}
async fPutObject(objectName: string, filePath: string, metaData?: ItemBucketMetadata) {
const bucketName = this.bucketName;
const obj = await this.client.fPutObject(bucketName, `${this.prefix}${objectName}`, filePath, metaData);
return obj as any;
}
async statObject(objectName: string, checkFile = true) {
const bucketName = this.bucketName;
try {
const obj = await this.client.statObject(bucketName, `${this.prefix}${objectName}`);
if (obj && checkFile && obj.size === 0) {
return null;
}
return obj;
} catch (e) {
if (e.code === 'NotFound') {
return null;
}
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) {
const bucketName = this.bucketName;
const obj = await this.client.copyObject(bucketName, sourceObject, targetObject);
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 {
return new this(opts);
}
static fromBase<T extends OssBase, U>(this: new (opts: OssBaseOptions<U>) => T, createOpts: { oss: OssBase; opts: Partial<OssBaseOptions<U>> }): T {
const base = createOpts.oss;
const opts = createOpts.opts as any;
return new this({
client: base.client,
bucketName: base.bucketName,
...opts,
});
}
}