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 = { /** * 已经初始化好的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> { 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(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(this: new (opts: OssBaseOptions) => T, opts: OssBaseOptions): T { return new this(opts); } static fromBase(this: new (opts: OssBaseOptions) => T, createOpts: { oss: OssBase; opts: Partial> }): T { const base = createOpts.oss; const opts = createOpts.opts as any; return new this({ client: base.client, bucketName: base.bucketName, ...opts, }); } }