239 lines
7.1 KiB
TypeScript
239 lines
7.1 KiB
TypeScript
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,
|
||
});
|
||
}
|
||
}
|