@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.
@@ -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';