s3mini 0.3.0 → 0.5.0

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/src/consts.ts CHANGED
@@ -13,11 +13,12 @@ export const DEFAULT_REQUEST_SIZE_IN_BYTES = 8 * 1024 * 1024;
13
13
 
14
14
  // Headers
15
15
  export const HEADER_AMZ_CONTENT_SHA256 = 'x-amz-content-sha256';
16
+ export const HEADER_AMZ_CHECKSUM_SHA256 = 'x-amz-checksum-sha256';
16
17
  export const HEADER_AMZ_DATE = 'x-amz-date';
17
18
  export const HEADER_HOST = 'host';
18
- export const HEADER_AUTHORIZATION = 'Authorization';
19
- export const HEADER_CONTENT_TYPE = 'Content-Type';
20
- export const HEADER_CONTENT_LENGTH = 'Content-Length';
19
+ export const HEADER_AUTHORIZATION = 'authorization';
20
+ export const HEADER_CONTENT_TYPE = 'content-type';
21
+ export const HEADER_CONTENT_LENGTH = 'content-length';
21
22
  export const HEADER_ETAG = 'etag';
22
23
  export const HEADER_LAST_MODIFIED = 'last-modified';
23
24
 
@@ -32,7 +33,6 @@ export const ERROR_UPLOAD_ID_REQUIRED = `${ERROR_PREFIX}uploadId must be a non-e
32
33
  export const ERROR_PARTS_REQUIRED = `${ERROR_PREFIX}parts must be a non-empty array`;
33
34
  export const ERROR_INVALID_PART = `${ERROR_PREFIX}Each part must have a partNumber (number) and ETag (string)`;
34
35
  export const ERROR_DATA_BUFFER_REQUIRED = `${ERROR_PREFIX}data must be a Buffer or string`;
35
- // const ERROR_PATH_REQUIRED = `${ERROR_PREFIX}path must be a string`;
36
36
  export const ERROR_PREFIX_TYPE = `${ERROR_PREFIX}prefix must be a string`;
37
37
  export const ERROR_MAX_KEYS_TYPE = `${ERROR_PREFIX}maxKeys must be a positive integer`;
38
38
  export const ERROR_DELIMITER_REQUIRED = `${ERROR_PREFIX}delimiter must be a string`;
package/src/index.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  'use strict';
2
2
 
3
- import { s3mini } from './S3.js';
3
+ import { S3mini } from './S3.js';
4
4
  import { sanitizeETag, runInBatches } from './utils.js';
5
5
 
6
6
  // Export the S3 class as default export and named export
7
- export { s3mini, sanitizeETag, runInBatches };
8
- export default s3mini;
7
+ export { S3mini, sanitizeETag, runInBatches };
8
+ export default S3mini;
9
9
 
10
10
  // Re-export types
11
11
  export type {
package/src/types.ts CHANGED
@@ -8,18 +8,14 @@ export interface S3Config {
8
8
  logger?: Logger;
9
9
  }
10
10
 
11
- export interface Crypto {
12
- createHmac: (
13
- algorithm: string,
14
- key: Buffer | string,
15
- ) => {
16
- update: (data: Buffer | string) => { digest: (encoding?: string) => string | Buffer };
17
- digest: (encoding?: string) => string | Buffer;
18
- };
19
- createHash: (algorithm: string) => {
20
- update: (data: Buffer | string) => { digest: (encoding?: string) => string | Buffer };
21
- digest: (encoding?: string) => string | Buffer;
22
- };
11
+ export interface SSECHeaders {
12
+ 'x-amz-server-side-encryption-customer-algorithm': string;
13
+ 'x-amz-server-side-encryption-customer-key': string;
14
+ 'x-amz-server-side-encryption-customer-key-md5': string;
15
+ }
16
+
17
+ export interface AWSHeaders {
18
+ [k: `x-amz-${string}`]: string;
23
19
  }
24
20
 
25
21
  export interface Logger {
@@ -33,6 +29,14 @@ export interface UploadPart {
33
29
  etag: string;
34
30
  }
35
31
 
32
+ export interface ListObject {
33
+ Key: string;
34
+ Size: number;
35
+ LastModified: Date;
36
+ ETag: string;
37
+ StorageClass: string;
38
+ }
39
+
36
40
  export interface CompleteMultipartUploadResult {
37
41
  location: string;
38
42
  bucket: string;
@@ -58,7 +62,7 @@ export interface ListMultipartUploadSuccess {
58
62
  key: string;
59
63
  uploadId: string;
60
64
  size?: number;
61
- mtime?: Date | undefined;
65
+ mtime?: Date;
62
66
  etag?: string;
63
67
  eTag?: string; // for backward compatibility
64
68
  parts: UploadPart[];
@@ -93,3 +97,62 @@ export interface XmlMap {
93
97
  [key: string]: XmlValue | XmlValue[]; // one or many children
94
98
  [key: number]: XmlValue | XmlValue[]; // allow numeric keys
95
99
  }
100
+
101
+ export interface CopyObjectOptions {
102
+ /**
103
+ * Specifies whether the metadata is copied from the source object or replaced with metadata provided in the request.
104
+ * Valid values: 'COPY' | 'REPLACE'
105
+ * Default: 'COPY'
106
+ */
107
+ metadataDirective?: 'COPY' | 'REPLACE';
108
+
109
+ /**
110
+ * Metadata to be set on the destination object when metadataDirective is 'REPLACE'.
111
+ * Keys can be provided with or without 'x-amz-meta-' prefix.
112
+ */
113
+ metadata?: Record<string, string>;
114
+ contentType?: string;
115
+
116
+ /**
117
+ * Storage class for the destination object.
118
+ * Valid values: 'STANDARD' | 'REDUCED_REDUNDANCY' | 'STANDARD_IA' | 'ONEZONE_IA' | 'INTELLIGENT_TIERING' | 'GLACIER' | 'DEEP_ARCHIVE' | 'GLACIER_IR'
119
+ */
120
+ storageClass?: string;
121
+
122
+ /**
123
+ * Specifies whether the object tag-set is copied from the source object or replaced with tag-set provided in the request.
124
+ * Valid values: 'COPY' | 'REPLACE'
125
+ */
126
+ taggingDirective?: 'COPY' | 'REPLACE';
127
+
128
+ /**
129
+ * If the bucket is configured as a website, redirects requests for this object to another object or URL.
130
+ */
131
+ websiteRedirectLocation?: string;
132
+
133
+ /**
134
+ * Server-Side Encryption with Customer-Provided Keys headers for the source object.
135
+ * Should include:
136
+ * - x-amz-copy-source-server-side-encryption-customer-algorithm
137
+ * - x-amz-copy-source-server-side-encryption-customer-key
138
+ * - x-amz-copy-source-server-side-encryption-customer-key-MD5
139
+ */
140
+ sourceSSECHeaders?: Record<string, string | number>;
141
+ destinationSSECHeaders?: SSECHeaders;
142
+ additionalHeaders?: Record<string, string | number>;
143
+ }
144
+
145
+ export interface CopyObjectResult {
146
+ etag: string;
147
+ lastModified?: Date;
148
+ }
149
+
150
+ /**
151
+ * Where Buffer is available, e.g. when @types/node is loaded, we want to use it.
152
+ * But it should be excluded in other environments (e.g. Cloudflare).
153
+ */
154
+ export type MaybeBuffer = typeof globalThis extends { Buffer?: infer B }
155
+ ? B extends new (...a: unknown[]) => unknown
156
+ ? InstanceType<B>
157
+ : ArrayBuffer | Uint8Array
158
+ : ArrayBuffer | Uint8Array;
package/src/utils.ts CHANGED
@@ -1,36 +1,80 @@
1
1
  'use strict';
2
+ import type { XmlValue, XmlMap, ListBucketResponse, ErrorWithCode } from './types.js';
2
3
 
3
- import type { Crypto, XmlValue, XmlMap, ListBucketResponse, ErrorWithCode } from './types.js';
4
- declare const crypto: Crypto;
4
+ const ENCODR = new TextEncoder();
5
+ const chunkSize = 0x8000; // 32KB chunks
6
+ const HEXS = '0123456789abcdef';
5
7
 
6
- // Initialize crypto functions - this is needed for environments where `crypto` is not available globally
7
- // e.g., in Cloudflare Workers or other non-Node.js environments with nodejs_flags enabled.
8
- const _createHmac: Crypto['createHmac'] = crypto.createHmac || (await import('node:crypto')).createHmac;
9
- const _createHash: Crypto['createHash'] = crypto.createHash || (await import('node:crypto')).createHash;
8
+ export const getByteSize = (data: unknown): number => {
9
+ if (typeof data === 'string') {
10
+ return ENCODR.encode(data).byteLength;
11
+ }
12
+ if (data instanceof ArrayBuffer || data instanceof Uint8Array) {
13
+ return data.byteLength;
14
+ }
15
+ if (data instanceof Blob) {
16
+ return data.size;
17
+ }
18
+ throw new Error('Unsupported data type');
19
+ };
20
+
21
+ /**
22
+ * Turn a raw ArrayBuffer into its hexadecimal representation.
23
+ * @param {ArrayBuffer} buffer The raw bytes.
24
+ * @returns {string} Hexadecimal string
25
+ */
26
+ export const hexFromBuffer = (buffer: ArrayBuffer): string => {
27
+ const bytes = new Uint8Array(buffer);
28
+ let hex = '';
29
+ for (const byte of bytes) {
30
+ hex += HEXS[byte >> 4]! + HEXS[byte & 0x0f]!;
31
+ }
32
+ return hex;
33
+ };
10
34
 
11
35
  /**
12
- * Hash content using SHA-256
13
- * @param {string|Buffer} content – data to hash
14
- * @returns {string} Hex encoded hash
36
+ * Turn a raw ArrayBuffer into its base64 representation.
37
+ * @param {ArrayBuffer} buffer The raw bytes.
38
+ * @returns {string} Base64 string
15
39
  */
16
- export const hash = (content: string | Buffer): string => {
17
- return _createHash('sha256').update(content).digest('hex') as string;
40
+ export const base64FromBuffer = (buffer: ArrayBuffer): string => {
41
+ const bytes = new Uint8Array(buffer);
42
+ let result = '';
43
+ for (let i = 0; i < bytes.length; i += chunkSize) {
44
+ const chunk = bytes.subarray(i, i + chunkSize);
45
+ result += btoa(String.fromCharCode.apply(null, chunk as unknown as number[]));
46
+ }
47
+ return result;
18
48
  };
19
49
 
20
- export const md5base64 = (data: string | Buffer): string => {
21
- return _createHash('md5').update(data).digest('base64') as string;
50
+ /**
51
+ * Compute SHA-256 hash of arbitrary string data.
52
+ * @param {string} content The content to be hashed.
53
+ * @returns {ArrayBuffer} The raw hash
54
+ */
55
+ export const sha256 = async (content: string): Promise<ArrayBuffer> => {
56
+ const data = ENCODR.encode(content);
57
+
58
+ return await globalThis.crypto.subtle.digest('SHA-256', data);
22
59
  };
23
60
 
24
61
  /**
25
- * Compute HMAC-SHA-256 of arbitrary data and return a hex string.
26
- * @param {string|Buffer} key secret key
27
- * @param {string|Buffer} content data to authenticate
28
- * @param {BufferEncoding} [encoding='hex'] hex | base64 | …
29
- * @returns {string | Buffer} hex encoded HMAC
62
+ * Compute HMAC-SHA-256 of arbitrary data.
63
+ * @param {string|ArrayBuffer} key The key used to sign the content.
64
+ * @param {string} content The content to be signed.
65
+ * @returns {ArrayBuffer} The raw signature
30
66
  */
31
- export const hmac = (key: string | Buffer, content: string | Buffer, encoding?: 'hex' | 'base64'): string | Buffer => {
32
- const mac = _createHmac('sha256', key).update(content);
33
- return encoding ? mac.digest(encoding) : mac.digest();
67
+ export const hmac = async (key: string | ArrayBuffer, content: string): Promise<ArrayBuffer> => {
68
+ const secret = await globalThis.crypto.subtle.importKey(
69
+ 'raw',
70
+ typeof key === 'string' ? ENCODR.encode(key) : key,
71
+ { name: 'HMAC', hash: 'SHA-256' },
72
+ false,
73
+ ['sign'],
74
+ );
75
+ const data = ENCODR.encode(content);
76
+
77
+ return await globalThis.crypto.subtle.sign('HMAC', secret, data);
34
78
  };
35
79
 
36
80
  /**
@@ -46,7 +90,7 @@ export const sanitizeETag = (etag: string): string => {
46
90
  '&QUOT;': '',
47
91
  '&#x00022': '',
48
92
  };
49
- return etag.replace(/^("|&quot;|&#34;)|("|&quot;|&#34;)$/g, m => replaceChars[m] as string);
93
+ return etag.replace(/(^("|&quot;|&#34;))|(("|&quot;|&#34;)$)/g, m => replaceChars[m] as string);
50
94
  };
51
95
 
52
96
  const entityMap = {