@kevisual/oss 0.0.12 → 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.
@@ -0,0 +1,103 @@
1
+ import * as dotenv from 'dotenv';
2
+ dotenv.config();
3
+
4
+ import { S3Client, ListObjectsV2Command, GetBucketMetadataConfigurationCommand, HeadObjectCommand, CopyObjectCommand } from '@aws-sdk/client-s3';
5
+
6
+ export const s3Client = new S3Client({
7
+ credentials: {
8
+ accessKeyId: process.env.S3_ACCESS_KEY_ID || '',
9
+ secretAccessKey: process.env.S3_SECRET_ACCESS_KEY || '',
10
+ },
11
+ region: process.env.S3_REGION,
12
+ endpoint: 'https://tos-s3-cn-shanghai.volces.com',
13
+ });
14
+
15
+ export const bucketName = process.env.S3_BUCKET_NAME;
16
+
17
+ export async function listS3Objects() {
18
+ const command = new ListObjectsV2Command({
19
+ Bucket: process.env.S3_BUCKET_NAME,
20
+ });
21
+
22
+ try {
23
+ const result = await s3Client.send(command);
24
+ console.log('S3 Objects:', result.Contents);
25
+ return result.Contents;
26
+ } catch (error) {
27
+ console.error('Error listing S3 objects:', error);
28
+ throw error;
29
+ }
30
+ }
31
+
32
+ // listS3Objects();
33
+
34
+ export async function getMetaData(key = 'readme.md') {
35
+ const command = new HeadObjectCommand({
36
+ Bucket: process.env.S3_BUCKET_NAME,
37
+ Key: key,
38
+ });
39
+
40
+ try {
41
+ const result = await s3Client.send(command);
42
+ // 解码 TOS 返回的 URL 编码的元数据值
43
+ const decodedMetadata: Record<string, string> = {};
44
+ if (result.Metadata) {
45
+ for (const [key, value] of Object.entries(result.Metadata)) {
46
+ decodedMetadata[key] = decodeURIComponent(value);
47
+ }
48
+ }
49
+ console.log('Metadata for', result);
50
+
51
+ return decodedMetadata;
52
+ } catch (error) {
53
+ console.error('Error getting metadata for', key, ':', error);
54
+ throw error;
55
+ }
56
+ }
57
+
58
+ // const metadata = await getMetaData();
59
+ // console.log('metadata', metadata);
60
+
61
+ export async function setMetaData(key = 'readme.md', metadata: Record<string, string>) {
62
+ // 注意:S3 不支持直接更新对象的元数据。必须通过复制对象到自身来实现元数据的更新。
63
+ const copySource = `${process.env.S3_BUCKET_NAME}/${key}`;
64
+
65
+ // 分离标准 HTTP 头和自定义元数据
66
+ // 标准头应作为顶层参数,自定义元数据才放在 Metadata 中
67
+ const standardHeaders: Record<string, any> = {};
68
+ const customMetadata: Record<string, string> = {};
69
+
70
+ const standardHeaderKeys = ['content-type', 'cache-control', 'content-disposition', 'content-encoding', 'content-language', 'expires'];
71
+
72
+ for (const [key, value] of Object.entries(metadata)) {
73
+ const lowerKey = key.toLowerCase();
74
+ if (standardHeaderKeys.includes(lowerKey)) {
75
+ // 使用驼峰命名
76
+ const camelKey = lowerKey.split('-').map((word, index) =>
77
+ index === 0 ? word.charAt(0).toUpperCase() + word.slice(1) : word.charAt(0).toUpperCase() + word.slice(1)
78
+ ).join('');
79
+ standardHeaders[camelKey] = value;
80
+ } else {
81
+ customMetadata[key] = value;
82
+ }
83
+ }
84
+
85
+ const command = new CopyObjectCommand({
86
+ Bucket: process.env.S3_BUCKET_NAME,
87
+ Key: key,
88
+ CopySource: copySource,
89
+ ...standardHeaders, // 标准头作为顶层参数
90
+ Metadata: customMetadata, // 只有自定义元数据放在这里
91
+ MetadataDirective: 'REPLACE', // 指定替换元数据
92
+ });
93
+
94
+ try {
95
+ const result = await s3Client.send(command);
96
+ console.log('Metadata updated successfully for', key);
97
+ return result;
98
+ } catch (error) {
99
+ console.error('Error setting metadata for', key, ':', error);
100
+ throw error;
101
+ }
102
+ }
103
+ // setMetaData('readme.md', { 'type': 'app', 'Content-Type': 'text/html' });
@@ -0,0 +1,84 @@
1
+ import { config } from 'dotenv';
2
+ import { ConfigOssService } from '../services/index.ts';
3
+ import { Client } from 'minio';
4
+ import path from 'path';
5
+ import { downloadObject } from '../util/download.ts';
6
+ const cwd = process.cwd();
7
+ config({ path: path.resolve(cwd, '..', '..', '.env.dev') });
8
+
9
+ console.log(
10
+ 'config',
11
+ process.env.MINIO_ENDPOINT,
12
+ process.env.MINIO_USE_SSL,
13
+ process.env.MINIO_ACCESS_KEY,
14
+ process.env.MINIO_SECRET_KEY,
15
+ process.env.MINIO_BUCKET_NAME,
16
+ );
17
+ const client = new Client({
18
+ endPoint: process.env.MINIO_ENDPOINT,
19
+ useSSL: process.env.MINIO_USE_SSL === 'true',
20
+ accessKey: process.env.MINIO_ACCESS_KEY,
21
+ secretKey: process.env.MINIO_SECRET_KEY,
22
+ });
23
+ const configOssService = new ConfigOssService({
24
+ client,
25
+ bucketName: process.env.MINIO_BUCKET_NAME,
26
+ owner: 'admin',
27
+ });
28
+
29
+ const main = async () => {
30
+ configOssService.setPrefix('root');
31
+ const config = await configOssService.statObject('avatar.png');
32
+ console.log(config);
33
+ };
34
+
35
+ main();
36
+ const putJson = async () => {
37
+ const config = await configOssService.putObject(
38
+ 'a.json',
39
+ {
40
+ a: 'a',
41
+ },
42
+ {
43
+ 'content-type': 'application/json',
44
+ 'cache-control': 'no-cache',
45
+ },
46
+ );
47
+ console.log(config);
48
+ };
49
+ // putJson();
50
+ const downloadMain = async () => {
51
+ const stat = await configOssService.statObject('a.json'); // 582af9ef5cdc53d6628f45cb842f874a
52
+ console.log(stat);
53
+ const objectStream = await downloadObject({
54
+ objectName: 'a.json',
55
+ client: configOssService,
56
+ filePath: path.resolve(cwd, 'a.json'),
57
+ });
58
+ // console.log(objectStream);
59
+ };
60
+ // downloadMain();
61
+
62
+ const statInfo = async () => {
63
+ try {
64
+ const stat = await configOssService.statObject('a.json');
65
+ // configOssService.setPrefix('root/');
66
+ // const stat = await configOssService.statObject('avatar.png');
67
+ // const stat = await configOssService.statObject('center/0.0.1/index.html');
68
+ console.log(stat);
69
+ } catch (e) {
70
+ if (e.code === 'NotFound') {
71
+ console.log('not found');
72
+ } else {
73
+ console.log('error', e);
74
+ }
75
+ }
76
+ };
77
+ // statInfo();
78
+
79
+ const listObjects = async () => {
80
+ configOssService.setPrefix('root/avatar.png');
81
+ const list = await configOssService.listObjects('');
82
+ console.log(list);
83
+ };
84
+ // listObjects();
@@ -0,0 +1,23 @@
1
+ import { OssBase } from "@/s3/core.ts";
2
+
3
+ import { s3Client, bucketName } from './common.ts';
4
+
5
+ const oss = new OssBase({
6
+ client: s3Client,
7
+ bucketName: bucketName,
8
+ });
9
+
10
+ // const list = await oss.listObjects('');
11
+
12
+ // console.log(list);
13
+
14
+ // const obj = await oss.getObjectAsString('readme.md');
15
+ // console.log(obj);
16
+
17
+ let putJson = {
18
+ name: 'test',
19
+ age: 18,
20
+ }
21
+ const objPut = await oss.putObject('a.json', putJson)
22
+
23
+ console.log(objPut);
@@ -0,0 +1,73 @@
1
+ import { ServerResponse } from 'node:http';
2
+ import { BucketItemStat } from 'minio';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+
6
+ const viewableExtensions = ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp', 'mp4', 'webm', 'mp3', 'wav', 'ogg', 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'];
7
+ import { OssBase } from '../index.ts';
8
+ /**
9
+ * 过滤 metaData 中的 key, 去除 password, accesskey, secretkey,
10
+ * 并返回过滤后的 metaData
11
+ * @param metaData
12
+ * @returns
13
+ */
14
+ export const filterMetaDataKeys = (metaData: Record<string, string>, clearKeys: string[] = []) => {
15
+ const keys = Object.keys(metaData);
16
+ // remove X-Amz- meta data
17
+ const removeKeys = ['password', 'accesskey', 'secretkey', ...clearKeys];
18
+ const filteredKeys = keys.filter((key) => !removeKeys.includes(key));
19
+ return filteredKeys.reduce((acc, key) => {
20
+ acc[key] = metaData[key];
21
+ return acc;
22
+ }, {} as Record<string, string>);
23
+ };
24
+ type SendObjectOptions = {
25
+ res: ServerResponse;
26
+ client: OssBase;
27
+ objectName: string;
28
+ isDownload?: boolean;
29
+ };
30
+ export const NotFoundFile = (res: ServerResponse, msg?: string, code = 404) => {
31
+ res.writeHead(code, { 'Content-Type': 'text/plain' });
32
+ res.end(msg || 'Not Found File');
33
+ return;
34
+ };
35
+ export const sendObject = async ({ res, objectName, client, isDownload = false }: SendObjectOptions) => {
36
+ let stat: BucketItemStat;
37
+ try {
38
+ stat = await client.statObject(objectName);
39
+ } catch (e) {
40
+ } finally {
41
+ if (!stat || stat.size === 0) {
42
+ return NotFoundFile(res);
43
+ }
44
+ const contentLength = stat.size;
45
+ const etag = stat.etag;
46
+ const lastModified = stat.lastModified.toISOString();
47
+ const filename = objectName.split('/').pop() || 'no-file-name-download'; // Extract filename from objectName
48
+ const fileExtension = filename.split('.').pop()?.toLowerCase() || '';
49
+ const filteredMetaData = filterMetaDataKeys(stat.metaData, ['size', 'etag', 'last-modified']);
50
+ const contentDisposition = viewableExtensions.includes(fileExtension) && !isDownload ? 'inline' : `attachment; filename="${filename}"`;
51
+
52
+ res.writeHead(200, {
53
+ 'Content-Length': contentLength,
54
+ etag,
55
+ 'last-modified': lastModified,
56
+ 'Content-Disposition': contentDisposition,
57
+ ...filteredMetaData,
58
+ });
59
+ const objectStream = await client.getObject(objectName);
60
+
61
+ objectStream.pipe(res, { end: true });
62
+ }
63
+ };
64
+
65
+ export const downloadObject = async ({ objectName, client, filePath }: Pick<SendObjectOptions, 'objectName' | 'client'> & { filePath: string }) => {
66
+ const objectStream = await client.getObject(objectName);
67
+ const dir = path.dirname(filePath);
68
+ if (!fs.existsSync(dir)) {
69
+ fs.mkdirSync(dir, { recursive: true });
70
+ }
71
+ objectStream.pipe(fs.createWriteStream(filePath));
72
+ return objectStream;
73
+ };
@@ -0,0 +1,44 @@
1
+ export const standardHeaderKeys = ['content-type', 'cache-control', 'content-disposition', 'content-encoding', 'content-language', 'expires'];
2
+
3
+ export type StandardHeaders = {
4
+ ContentType?: string;
5
+ CacheControl?: string;
6
+ ContentDisposition?: string;
7
+ ContentEncoding?: string;
8
+ ContentLanguage?: string;
9
+ Expires?: Date;
10
+ };
11
+
12
+ /**
13
+ * 从元数据中提取标准头部和自定义元数据
14
+ * @param metaData 原始元数据
15
+ * @returns 标准头部和自定义元数据
16
+ */
17
+ export function extractStandardHeaders(metaData: Record<string, string>): {
18
+ standardHeaders: StandardHeaders;
19
+ customMetadata: Record<string, string>;
20
+ } {
21
+ const standardHeaders: StandardHeaders = {};
22
+ const customMetadata: Record<string, string> = {};
23
+
24
+ for (const [key, value] of Object.entries(metaData)) {
25
+ const lowerKey = key.toLowerCase();
26
+ if (lowerKey === 'content-type') {
27
+ standardHeaders.ContentType = value;
28
+ } else if (lowerKey === 'cache-control') {
29
+ standardHeaders.CacheControl = value;
30
+ } else if (lowerKey === 'content-disposition') {
31
+ standardHeaders.ContentDisposition = value;
32
+ } else if (lowerKey === 'content-encoding') {
33
+ standardHeaders.ContentEncoding = value;
34
+ } else if (lowerKey === 'content-language') {
35
+ standardHeaders.ContentLanguage = value;
36
+ } else if (lowerKey === 'expires') {
37
+ standardHeaders.Expires = new Date(value);
38
+ } else {
39
+ customMetadata[key] = value;
40
+ }
41
+ }
42
+
43
+ return { standardHeaders, customMetadata };
44
+ }
@@ -0,0 +1,49 @@
1
+ import path from 'node:path';
2
+ // 获取文件的 content-type
3
+ export const getContentType = (filePath: string) => {
4
+ const extname = path.extname(filePath);
5
+ const contentType = {
6
+ '.html': 'text/html; charset=utf-8',
7
+ '.js': 'text/javascript; charset=utf-8',
8
+ '.css': 'text/css; charset=utf-8',
9
+ '.txt': 'text/plain; charset=utf-8',
10
+ '.json': 'application/json; charset=utf-8',
11
+ '.png': 'image/png',
12
+ '.jpg': 'image/jpg',
13
+ '.gif': 'image/gif',
14
+ '.svg': 'image/svg+xml',
15
+ '.wav': 'audio/wav',
16
+ '.mp4': 'video/mp4',
17
+ '.md': 'text/markdown; charset=utf-8', // utf-8配置
18
+ '.ico': 'image/x-icon', // Favicon 图标
19
+ '.webp': 'image/webp', // WebP 图像格式
20
+ '.webm': 'video/webm', // WebM 视频格式
21
+ '.ogg': 'audio/ogg', // Ogg 音频格式
22
+ '.mp3': 'audio/mpeg', // MP3 音频格式
23
+ '.m4a': 'audio/mp4', // M4A 音频格式
24
+ '.m3u8': 'application/vnd.apple.mpegurl', // HLS 播放列表
25
+ '.ts': 'video/mp2t', // MPEG Transport Stream
26
+ '.pdf': 'application/pdf', // PDF 文档
27
+ '.doc': 'application/msword', // Word 文档
28
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // Word 文档 (新版)
29
+ '.ppt': 'application/vnd.ms-powerpoint', // PowerPoint 演示文稿
30
+ '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', // PowerPoint (新版)
31
+ '.xls': 'application/vnd.ms-excel', // Excel 表格
32
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // Excel 表格 (新版)
33
+ '.csv': 'text/csv; charset=utf-8', // CSV 文件
34
+ '.xml': 'application/xml; charset=utf-8', // XML 文件
35
+ '.rtf': 'application/rtf', // RTF 文本文件
36
+ '.eot': 'application/vnd.ms-fontobject', // Embedded OpenType 字体
37
+ '.ttf': 'font/ttf', // TrueType 字体
38
+ '.woff': 'font/woff', // Web Open Font Format 1.0
39
+ '.woff2': 'font/woff2', // Web Open Font Format 2.0
40
+ '.otf': 'font/otf', // OpenType 字体
41
+ '.wasm': 'application/wasm', // WebAssembly 文件
42
+ '.pem': 'application/x-pem-file', // PEM 证书文件
43
+ '.crt': 'application/x-x509-ca-cert', // CRT 证书文件
44
+ '.yaml': 'application/x-yaml; charset=utf-8', // YAML 文件
45
+ '.yml': 'application/x-yaml; charset=utf-8', // YAML 文件(别名)
46
+ '.zip': 'application/octet-stream',
47
+ };
48
+ return contentType[extname] || 'application/octet-stream';
49
+ };
@@ -0,0 +1,26 @@
1
+ import crypto from 'node:crypto';
2
+ // 582af9ef5cdc53d6628f45cb842f874a
3
+ // const hashStr = '{"a":"a"}';
4
+ // const hash = crypto.createHash('md5').update(hashStr).digest('hex');
5
+ // console.log(hash);
6
+
7
+ /**
8
+ * 计算字符串的md5值
9
+ * @param str
10
+ * @returns
11
+ */
12
+ export const hash = (str: string | Buffer | Object) => {
13
+ let hashStr: string | Buffer;
14
+ if (str instanceof Buffer) {
15
+ hashStr = str;
16
+ } else if (str instanceof Object) {
17
+ hashStr = JSON.stringify(str, null, 2);
18
+ } else {
19
+ hashStr = str;
20
+ }
21
+ return crypto.createHash('md5').update(hashStr).digest('hex');
22
+ };
23
+
24
+ export const hashSringify = (str: Object) => {
25
+ return JSON.stringify(str, null, 2);
26
+ };
@@ -0,0 +1,5 @@
1
+ export * from './hash.ts';
2
+
3
+ export * from './get-content-type.ts';
4
+
5
+ export * from './extract-standard-headers.ts';
@@ -1,161 +0,0 @@
1
- import * as minio from 'minio';
2
- import { ItemBucketMetadata, Client } from 'minio';
3
- import * as minio_dist_esm_internal_type_mjs from 'minio/dist/esm/internal/type.mjs';
4
- import * as stream from 'stream';
5
-
6
- type UploadedObjectInfo = {
7
- etag: string;
8
- lastModified?: Date;
9
- size?: number;
10
- versionId: string;
11
- metadata?: ItemBucketMetadata;
12
- };
13
- type StatObjectResult = {
14
- size: number;
15
- etag: string;
16
- lastModified: Date;
17
- metaData: ItemBucketMetadata;
18
- versionId?: string | null;
19
- };
20
- type ListFileObject = {
21
- name: string;
22
- size: number;
23
- lastModified: Date;
24
- etag: string;
25
- };
26
- type ListDirectoryObject = {
27
- prefix: string;
28
- size: number;
29
- };
30
- type ListObjectResult = ListFileObject | ListDirectoryObject;
31
- interface OssBaseOperation {
32
- prefix: string;
33
- setPrefix(prefix: string): void;
34
- /**
35
- * 获取对象
36
- * @param objectName 对象名
37
- */
38
- getObject(objectName: string): Promise<any>;
39
- /**
40
- * 上传对象
41
- * @param objectName 对象名
42
- * @param data 数据
43
- */
44
- putObject(objectName: string, data: Buffer | string, metaData?: ItemBucketMetadata): Promise<UploadedObjectInfo>;
45
- /**
46
- * 上传文件
47
- * @param objectName 对象名
48
- * @param filePath 文件路径
49
- */
50
- fPutObject(objectName: string, filePath: string, metaData?: ItemBucketMetadata): Promise<UploadedObjectInfo>;
51
- /**
52
- * 获取对象信息
53
- * @param objectName 对象名
54
- */
55
- statObject(objectName: string): Promise<StatObjectResult>;
56
- /**
57
- * 删除对象
58
- * @param objectName 对象名
59
- */
60
- deleteObject(objectName: string): Promise<any>;
61
- /**
62
- * 列出对象
63
- * @param objectName 对象名
64
- * @param opts 选项
65
- * @param opts.recursive 是否递归
66
- * @param opts.startAfter 开始位置
67
- */
68
- listObjects(objectName: string, opts?: {
69
- /**
70
- * 是否递归
71
- */
72
- recursive?: boolean;
73
- /**
74
- * 开始位置
75
- */
76
- startAfter?: string;
77
- }): Promise<ListObjectResult[]>;
78
- /**
79
- * 复制对象
80
- * @param sourceObject 源对象
81
- * @param targetObject 目标对象
82
- */
83
- copyObject: Client['copyObject'];
84
- }
85
- interface OssService extends OssBaseOperation {
86
- owner: string;
87
- }
88
-
89
- type OssBaseOptions<T = {
90
- [key: string]: any;
91
- }> = {
92
- /**
93
- * 已经初始化好的minio client
94
- */
95
- client: Client;
96
- /**
97
- * 桶名
98
- */
99
- bucketName: string;
100
- /**
101
- * 前缀
102
- */
103
- prefix?: string;
104
- } & T;
105
- declare class OssBase implements OssBaseOperation {
106
- client?: Client;
107
- bucketName: string;
108
- prefix: string;
109
- /**
110
- * 计算字符串或者对象的的md5值
111
- */
112
- hash: (str: string | Buffer | Object) => string;
113
- constructor(opts: OssBaseOptions);
114
- setPrefix(prefix: string): void;
115
- getObject(objectName: string): Promise<stream.Readable>;
116
- getJson(objectName: string): Promise<Record<string, any>>;
117
- /**
118
- * 上传文件, 当是流的时候,中断之后的etag会变,所以传递的时候不要嵌套async await,例如 busboy 监听文件流内部的时候,不要用check
119
- * @param objectName
120
- * @param data
121
- * @param metaData
122
- * @param options 如果文件本身存在,则复制原有的meta的内容
123
- * @returns
124
- */
125
- putObject(objectName: string, data: Buffer | string | Object, metaData?: ItemBucketMetadata, opts?: {
126
- check?: boolean;
127
- isStream?: boolean;
128
- size?: number;
129
- }): Promise<minio_dist_esm_internal_type_mjs.UploadedObjectInfo>;
130
- deleteObject(objectName: string): Promise<void>;
131
- listObjects<IS_FILE = false>(objectName: string, opts?: {
132
- recursive?: boolean;
133
- startAfter?: string;
134
- }): Promise<IS_FILE extends true ? ListFileObject[] : ListObjectResult[]>;
135
- fPutObject(objectName: string, filePath: string, metaData?: ItemBucketMetadata): Promise<any>;
136
- statObject(objectName: string, checkFile?: boolean): Promise<minio.BucketItemStat>;
137
- /**
138
- * 检查文件hash是否一致
139
- * @param objectName
140
- * @param hash
141
- * @returns
142
- */
143
- checkObjectHash(objectName: string, hash: string, meta?: ItemBucketMetadata): Promise<{
144
- success: boolean;
145
- metaData: ItemBucketMetadata | null;
146
- obj: any;
147
- equalMeta?: boolean;
148
- }>;
149
- getMetadata(pathname: string, meta?: ItemBucketMetadata): ItemBucketMetadata;
150
- copyObject(sourceObject: any, targetObject: any): Promise<minio_dist_esm_internal_type_mjs.CopyObjectResult>;
151
- replaceObject(objectName: string, meta: {
152
- [key: string]: string;
153
- }): Promise<minio_dist_esm_internal_type_mjs.CopyObjectResult>;
154
- static create<T extends OssBase, U>(this: new (opts: OssBaseOptions<U>) => T, opts: OssBaseOptions<U>): T;
155
- static fromBase<T extends OssBase, U>(this: new (opts: OssBaseOptions<U>) => T, createOpts: {
156
- oss: OssBase;
157
- opts: Partial<OssBaseOptions<U>>;
158
- }): T;
159
- }
160
-
161
- export { type ListFileObject as L, OssBase as O, type StatObjectResult as S, type UploadedObjectInfo as U, type OssBaseOptions as a, type ListDirectoryObject as b, type ListObjectResult as c, type OssBaseOperation as d, type OssService as e };