@kevisual/oss 0.0.13 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,24 +1,27 @@
1
1
  {
2
2
  "name": "@kevisual/oss",
3
- "version": "0.0.13",
3
+ "version": "0.0.14",
4
4
  "main": "dist/index.js",
5
5
  "scripts": {
6
6
  "build": "bun run bun.config.ts"
7
7
  },
8
8
  "files": [
9
- "dist"
9
+ "dist",
10
+ "src"
10
11
  ],
11
12
  "keywords": [],
12
13
  "author": "abearxiong <xiongxiao@xiongxiao.me>",
13
14
  "license": "MIT",
14
15
  "type": "module",
15
16
  "devDependencies": {
16
- "@types/bun": "^1.3.3",
17
- "@types/node": "^24.10.1",
17
+ "@types/bun": "^1.3.5",
18
+ "@types/node": "^25.0.3",
18
19
  "bun-plugin-dts": "^0.3.0",
19
- "dotenv": "^16.5.0",
20
- "minio": "^8.0.5",
21
- "tsup": "^8.4.0"
20
+ "dotenv": "^17.2.3",
21
+ "minio": "^8.0.6",
22
+ "@aws-sdk/client-s3": "^3.0.0",
23
+ "es-toolkit": "^1.43.0",
24
+ "fast-glob": "^3.3.3"
22
25
  },
23
26
  "exports": {
24
27
  ".": {
@@ -37,9 +40,5 @@
37
40
  "publishConfig": {
38
41
  "access": "public"
39
42
  },
40
- "dependencies": {
41
- "@types/lodash": "^4.17.21",
42
- "fast-glob": "^3.3.3",
43
- "lodash": "^4.17.21"
44
- }
43
+ "dependencies": {}
45
44
  }
@@ -0,0 +1,26 @@
1
+ import { Client, CopyDestinationOptions, CopySourceOptions } from 'minio';
2
+
3
+ type CopyObjectOpts = {
4
+ bucketName: string;
5
+ newMetadata: Record<string, string>;
6
+ objectName: string;
7
+ client: Client;
8
+ };
9
+ /**
10
+ * 复制对象 REPLACE 替换
11
+ * @param param0
12
+ * @returns
13
+ */
14
+ export const copyObject = async ({ bucketName, newMetadata, objectName, client }: CopyObjectOpts) => {
15
+ const source = new CopySourceOptions({ Bucket: bucketName, Object: objectName });
16
+ const stat = await client.statObject(bucketName, objectName);
17
+ const sourceMetadata = stat.metaData;
18
+ const destination = new CopyDestinationOptions({
19
+ Bucket: bucketName,
20
+ Object: objectName,
21
+ UserMetadata: { ...sourceMetadata, ...newMetadata },
22
+ MetadataDirective: 'REPLACE',
23
+ });
24
+ const copyResult = await client.copyObject(source, destination);
25
+ return copyResult;
26
+ };
@@ -0,0 +1,87 @@
1
+ import { ItemBucketMetadata, Client } from 'minio';
2
+ export type UploadedObjectInfo = {
3
+ etag: string;
4
+ lastModified?: Date;
5
+ size?: number;
6
+ versionId: string;
7
+ metadata?: ItemBucketMetadata;
8
+ };
9
+ export type StatObjectResult = {
10
+ size: number;
11
+ etag: string;
12
+ lastModified: Date;
13
+ metaData: ItemBucketMetadata;
14
+ versionId?: string | null;
15
+ };
16
+ export type ListFileObject = {
17
+ name: string;
18
+ size: number;
19
+ lastModified: Date;
20
+ etag: string;
21
+ };
22
+ export type ListDirectoryObject = {
23
+ prefix: string;
24
+ size: number;
25
+ };
26
+ export type ListObjectResult = ListFileObject | ListDirectoryObject;
27
+ export interface OssBaseOperation {
28
+ prefix: string;
29
+ setPrefix(prefix: string): void;
30
+ /**
31
+ * 获取对象
32
+ * @param objectName 对象名
33
+ */
34
+ getObject(objectName: string): Promise<any>;
35
+ /**
36
+ * 上传对象
37
+ * @param objectName 对象名
38
+ * @param data 数据
39
+ */
40
+ putObject(objectName: string, data: Buffer | string, metaData?: ItemBucketMetadata): Promise<UploadedObjectInfo>;
41
+ /**
42
+ * 上传文件
43
+ * @param objectName 对象名
44
+ * @param filePath 文件路径
45
+ */
46
+ fPutObject(objectName: string, filePath: string, metaData?: ItemBucketMetadata): Promise<UploadedObjectInfo>;
47
+ /**
48
+ * 获取对象信息
49
+ * @param objectName 对象名
50
+ */
51
+ statObject(objectName: string): Promise<StatObjectResult>;
52
+ /**
53
+ * 删除对象
54
+ * @param objectName 对象名
55
+ */
56
+ deleteObject(objectName: string): Promise<any>;
57
+ /**
58
+ * 列出对象
59
+ * @param objectName 对象名
60
+ * @param opts 选项
61
+ * @param opts.recursive 是否递归
62
+ * @param opts.startAfter 开始位置
63
+ */
64
+ listObjects(
65
+ objectName: string,
66
+ opts?: {
67
+ /**
68
+ * 是否递归
69
+ */
70
+ recursive?: boolean;
71
+ /**
72
+ * 开始位置
73
+ */
74
+ startAfter?: string;
75
+ },
76
+ ): Promise<ListObjectResult[]>;
77
+ /**
78
+ * 复制对象
79
+ * @param sourceObject 源对象
80
+ * @param targetObject 目标对象
81
+ */
82
+ copyObject: Client['copyObject'];
83
+ }
84
+
85
+ export interface OssService extends OssBaseOperation {
86
+ owner: string;
87
+ }
package/src/index.ts ADDED
@@ -0,0 +1,244 @@
1
+ import { Client, ItemBucketMetadata } from 'minio';
2
+ import { ListFileObject, ListObjectResult, OssBaseOperation } from './core/type.ts';
3
+ import { hash } from './util/hash.ts';
4
+ import { copyObject } from './core/copy-object.ts';
5
+ import { getContentType } from './util/get-content-type.ts';
6
+ import { omit } from 'es-toolkit'
7
+ export type OssBaseOptions<T = { [key: string]: any }> = {
8
+ /**
9
+ * 已经初始化好的minio client
10
+ */
11
+ client: Client;
12
+ /**
13
+ * 桶名
14
+ */
15
+ bucketName: string;
16
+ /**
17
+ * 前缀
18
+ */
19
+ prefix?: string;
20
+ } & T;
21
+
22
+ export class OssBase implements OssBaseOperation {
23
+ client?: Client;
24
+ bucketName: string;
25
+ prefix = '';
26
+ /**
27
+ * 计算字符串或者对象的的md5值
28
+ */
29
+ hash = hash;
30
+ constructor(opts: OssBaseOptions) {
31
+ if (!opts.client) {
32
+ throw new Error('client is required');
33
+ }
34
+ this.bucketName = opts.bucketName;
35
+ this.client = opts.client;
36
+ this.prefix = opts?.prefix ?? '';
37
+ }
38
+
39
+ setPrefix(prefix: string) {
40
+ this.prefix = prefix;
41
+ }
42
+
43
+ async getObject(objectName: string) {
44
+ const bucketName = this.bucketName;
45
+ const obj = await this.client.getObject(bucketName, `${this.prefix}${objectName}`);
46
+ return obj;
47
+ }
48
+
49
+ async getJson(objectName: string): Promise<Record<string, any>> {
50
+ const obj = await this.getObject(objectName);
51
+ return new Promise((resolve, reject) => {
52
+ let data = '';
53
+ obj.on('data', (chunk) => {
54
+ data += chunk;
55
+ });
56
+ obj.on('end', () => {
57
+ try {
58
+ const jsonData = JSON.parse(data);
59
+ resolve(jsonData);
60
+ } catch (error) {
61
+ reject(new Error('Failed to parse JSON'));
62
+ }
63
+ });
64
+ obj.on('error', (err) => {
65
+ reject(err);
66
+ });
67
+ });
68
+ }
69
+ /**
70
+ * 上传文件, 当是流的时候,中断之后的etag会变,所以传递的时候不要嵌套async await,例如 busboy 监听文件流内部的时候,不要用check
71
+ * @param objectName
72
+ * @param data
73
+ * @param metaData
74
+ * @param options 如果文件本身存在,则复制原有的meta的内容
75
+ * @returns
76
+ */
77
+ async putObject(
78
+ objectName: string,
79
+ data: Buffer | string | Object,
80
+ metaData: ItemBucketMetadata = {},
81
+ opts?: { check?: boolean; isStream?: boolean; size?: number },
82
+ ) {
83
+ let putData: Buffer | string;
84
+ let size: number = opts?.size;
85
+ const isStream = opts?.isStream;
86
+ if (!isStream) {
87
+ if (typeof data === 'string') {
88
+ putData = data;
89
+ size = putData.length;
90
+ } else {
91
+ putData = JSON.stringify(data);
92
+ size = putData.length;
93
+ }
94
+ } else {
95
+ putData = data as any;
96
+ // 对于流式上传,如果没有提供 size,会导致多部分上传,ETag 会是 ****-1 格式
97
+ // 必须提供准确的 size 才能得到标准的 MD5 格式 ETag
98
+ if (!size) {
99
+ throw new Error('Stream upload requires size parameter to avoid multipart upload and get standard MD5 ETag');
100
+ }
101
+ }
102
+ if (opts?.check) {
103
+ const obj = await this.statObject(objectName, true);
104
+ if (obj) {
105
+ const omitMeda = ['size', 'content-type', 'cache-control', 'app-source'];
106
+ const objMeta = JSON.parse(JSON.stringify(omit(obj.metaData, omitMeda)));
107
+ metaData = {
108
+ ...objMeta,
109
+ ...metaData,
110
+ };
111
+ }
112
+ }
113
+
114
+ const bucketName = this.bucketName;
115
+ const obj = await this.client.putObject(bucketName, `${this.prefix}${objectName}`, putData, size, metaData);
116
+ return obj;
117
+ }
118
+
119
+ async deleteObject(objectName: string) {
120
+ const bucketName = this.bucketName;
121
+ const obj = await this.client.removeObject(bucketName, `${this.prefix}${objectName}`);
122
+ return obj;
123
+ }
124
+
125
+ async listObjects<IS_FILE = false>(objectName: string, opts?: { recursive?: boolean; startAfter?: string }) {
126
+ const bucketName = this.bucketName;
127
+ const prefix = `${this.prefix}${objectName}`;
128
+ const res = await new Promise((resolve, reject) => {
129
+ let res: any[] = [];
130
+ let hasError = false;
131
+ this.client
132
+ .listObjectsV2(bucketName, prefix, opts?.recursive ?? false, opts?.startAfter)
133
+ .on('data', (data) => {
134
+ res.push(data);
135
+ })
136
+ .on('error', (err) => {
137
+ console.error('minio error', prefix, err);
138
+ hasError = true;
139
+ })
140
+ .on('end', () => {
141
+ if (hasError) {
142
+ reject();
143
+ return;
144
+ } else {
145
+ resolve(res);
146
+ }
147
+ });
148
+ });
149
+ return res as IS_FILE extends true ? ListFileObject[] : ListObjectResult[];
150
+ }
151
+
152
+ async fPutObject(objectName: string, filePath: string, metaData?: ItemBucketMetadata) {
153
+ const bucketName = this.bucketName;
154
+ const obj = await this.client.fPutObject(bucketName, `${this.prefix}${objectName}`, filePath, metaData);
155
+ return obj as any;
156
+ }
157
+ /**
158
+ * 获取完整的对象名称
159
+ * @param objectName
160
+ * @returns
161
+ */
162
+ async getObjectName(objectName: string) {
163
+ return `${this.prefix}${objectName}`;
164
+ }
165
+ async statObject(objectName: string, checkFile = true) {
166
+ const bucketName = this.bucketName;
167
+ try {
168
+ const obj = await this.client.statObject(bucketName, `${this.prefix}${objectName}`);
169
+ return obj;
170
+ } catch (e) {
171
+ if (e.code === 'NotFound') {
172
+ return null;
173
+ }
174
+ throw e;
175
+ }
176
+ }
177
+ /**
178
+ * 检查文件hash是否一致
179
+ * @param objectName
180
+ * @param hash
181
+ * @returns
182
+ */
183
+ async checkObjectHash(
184
+ objectName: string,
185
+ hash: string,
186
+ meta?: ItemBucketMetadata,
187
+ ): Promise<{ success: boolean; metaData: ItemBucketMetadata | null; obj: any; equalMeta?: boolean }> {
188
+ const obj = await this.statObject(`${this.prefix}${objectName}`, true);
189
+ if (!obj) {
190
+ return { success: false, metaData: null, obj: null, equalMeta: false };
191
+ }
192
+ let metaData: ItemBucketMetadata = {};
193
+ const omitMeda = ['content-type', 'cache-control', 'app-source'];
194
+ const objMeta = omit(obj.metaData, omitMeda);
195
+ metaData = {
196
+ ...objMeta,
197
+ };
198
+ let equalMeta = false;
199
+ if (meta) {
200
+ equalMeta = JSON.stringify(metaData) === JSON.stringify(meta);
201
+ }
202
+ return { success: obj.etag === hash, metaData, obj, equalMeta };
203
+ }
204
+ getMetadata(pathname: string, meta: ItemBucketMetadata = { 'app-source': 'user-app' }) {
205
+ const isHtml = pathname.endsWith('.html');
206
+ if (isHtml) {
207
+ meta = {
208
+ ...meta,
209
+ 'content-type': 'text/html; charset=utf-8',
210
+ 'cache-control': 'no-cache',
211
+ };
212
+ } else {
213
+ meta = {
214
+ ...meta,
215
+ 'content-type': getContentType(pathname),
216
+ 'cache-control': 'max-age=31536000, immutable',
217
+ };
218
+ }
219
+ return meta;
220
+ }
221
+
222
+ async copyObject(sourceObject: any, targetObject: any) {
223
+ const bucketName = this.bucketName;
224
+ const obj = await this.client.copyObject(bucketName, sourceObject, targetObject);
225
+ return obj;
226
+ }
227
+ async replaceObject(objectName: string, meta: { [key: string]: string }) {
228
+ const { bucketName, client } = this;
229
+ return copyObject({ bucketName, client, objectName: `${this.prefix}${objectName}`, newMetadata: meta });
230
+ }
231
+ static create<T extends OssBase, U>(this: new (opts: OssBaseOptions<U>) => T, opts: OssBaseOptions<U>): T {
232
+ return new this(opts);
233
+ }
234
+
235
+ static fromBase<T extends OssBase, U>(this: new (opts: OssBaseOptions<U>) => T, createOpts: { oss: OssBase; opts: Partial<OssBaseOptions<U>> }): T {
236
+ const base = createOpts.oss;
237
+ const opts = createOpts.opts as any;
238
+ return new this({
239
+ client: base.client,
240
+ bucketName: base.bucketName,
241
+ ...opts,
242
+ });
243
+ }
244
+ }
@@ -0,0 +1,46 @@
1
+ import { S3Client, CopyObjectCommand, HeadObjectCommand } from '@aws-sdk/client-s3';
2
+ import { extractStandardHeaders } from '../util/extract-standard-headers.ts';
3
+
4
+ type CopyObjectOpts = {
5
+ bucketName: string;
6
+ newMetadata: Record<string, string>;
7
+ objectName: string;
8
+ client: S3Client;
9
+ };
10
+
11
+ /**
12
+ * 复制对象 REPLACE 替换(使用 AWS SDK 实现)
13
+ * @param opts 复制选项
14
+ * @returns 复制结果
15
+ */
16
+ export const copyObject = async ({ bucketName, newMetadata, objectName, client }: CopyObjectOpts) => {
17
+ // 获取当前对象的元数据
18
+ const headCommand = new HeadObjectCommand({
19
+ Bucket: bucketName,
20
+ Key: objectName,
21
+ });
22
+ const headResponse = await client.send(headCommand);
23
+ const sourceMetadata = headResponse.Metadata || {};
24
+
25
+ // 合并元数据
26
+ const mergedMeta = { ...sourceMetadata, ...newMetadata };
27
+ const { standardHeaders, customMetadata } = extractStandardHeaders(mergedMeta);
28
+
29
+ // 执行复制操作(同一对象,用于更新元数据)
30
+ const copyCommand = new CopyObjectCommand({
31
+ Bucket: bucketName,
32
+ CopySource: `${bucketName}/${objectName}`,
33
+ Key: objectName,
34
+ ContentType: standardHeaders.ContentType,
35
+ CacheControl: standardHeaders.CacheControl,
36
+ ContentDisposition: standardHeaders.ContentDisposition,
37
+ ContentEncoding: standardHeaders.ContentEncoding,
38
+ ContentLanguage: standardHeaders.ContentLanguage,
39
+ Expires: standardHeaders.Expires,
40
+ Metadata: customMetadata,
41
+ MetadataDirective: 'REPLACE',
42
+ });
43
+
44
+ const copyResult = await client.send(copyCommand);
45
+ return copyResult;
46
+ };