@croco/storage-r2 0.0.1
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/README.md +54 -0
- package/dist/index.d.mts +97 -0
- package/dist/index.d.ts +97 -0
- package/dist/index.js +1 -0
- package/dist/index.mjs +1 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# @croco/storage-r2
|
|
2
|
+
|
|
3
|
+
Cloudflare R2를 `@croco/storage-core` 인터페이스에 연결하는 스토리지 구현체입니다.
|
|
4
|
+
|
|
5
|
+
## 설치
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @croco/storage-r2 @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 사용법
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { R2StorageProvider } from "@croco/storage-r2";
|
|
15
|
+
|
|
16
|
+
const storage = new R2StorageProvider(configService, logger);
|
|
17
|
+
|
|
18
|
+
await storage.put("images/logo.png", Buffer.from("content"), {
|
|
19
|
+
contentType: "image/png",
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const file = await storage.get("images/logo.png");
|
|
23
|
+
const signedUrl = await storage.getSignedUrl("images/logo.png", { expiresIn: 3600 });
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 설정
|
|
27
|
+
|
|
28
|
+
필수 설정 키:
|
|
29
|
+
|
|
30
|
+
- `R2_ACCOUNT_ID`
|
|
31
|
+
- `R2_ACCESS_KEY_ID`
|
|
32
|
+
- `R2_SECRET_ACCESS_KEY`
|
|
33
|
+
- `R2_BUCKET`
|
|
34
|
+
|
|
35
|
+
선택 설정 키:
|
|
36
|
+
|
|
37
|
+
- `R2_PUBLIC_URL_BASE`
|
|
38
|
+
|
|
39
|
+
## API 레퍼런스
|
|
40
|
+
|
|
41
|
+
| API | 설명 |
|
|
42
|
+
| ------------------------- | ------------------------------------------------------------------ |
|
|
43
|
+
| `R2StorageProvider` | 업로드, 다운로드, 스트림 조회, 삭제, 메타데이터 조회를 제공합니다. |
|
|
44
|
+
| `R2_OPTIONS` | DI 등록용 토큰입니다. |
|
|
45
|
+
| `R2Options` | 계정, 버킷, 공개 URL 설정 타입입니다. |
|
|
46
|
+
| `MissingR2ConfigProblem` | 필수 설정이 없을 때 발생합니다. |
|
|
47
|
+
| `EmptyR2BodyProblem` | 다운로드 응답 본문이 비었을 때 발생합니다. |
|
|
48
|
+
| `R2ObjectTooLargeProblem` | `get()`으로 10MB 초과 객체를 읽으려 할 때 발생합니다. |
|
|
49
|
+
|
|
50
|
+
## 동작 메모
|
|
51
|
+
|
|
52
|
+
- 버퍼 업로드와 다운로드는 최대 3회 재시도합니다.
|
|
53
|
+
- `get()`은 메모리 사용량 보호를 위해 10MB까지만 허용합니다.
|
|
54
|
+
- 큰 파일은 `getStream()`을 사용하세요.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { StorageProblem, BaseStorageProvider, PutOptions, SignedUrlOptions, ObjectMetadata } from '@croco/storage-core';
|
|
2
|
+
import { ProblemCategory } from '@croco/problems-core';
|
|
3
|
+
import { Readable } from 'node:stream';
|
|
4
|
+
import { ConfigService } from '@croco/framework-config';
|
|
5
|
+
import { Logger } from '@croco/framework-logger';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* R2 다운로드 응답에 본문이 없을 때 발생하는 문제입니다.
|
|
9
|
+
*/
|
|
10
|
+
declare class EmptyR2BodyProblem extends StorageProblem {
|
|
11
|
+
constructor(key: string);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 필수 R2 설정이 누락되었을 때 발생하는 문제입니다.
|
|
16
|
+
*/
|
|
17
|
+
declare class MissingR2ConfigProblem extends StorageProblem {
|
|
18
|
+
readonly code = "STORAGE_R2_MISSING_CONFIG";
|
|
19
|
+
readonly category = ProblemCategory.InternalServerError;
|
|
20
|
+
constructor(configKey: string);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 메모리 기반 다운로드 한도를 초과한 객체를 읽으려 할 때 발생하는 문제입니다.
|
|
25
|
+
*/
|
|
26
|
+
declare class R2ObjectTooLargeProblem extends StorageProblem {
|
|
27
|
+
readonly code = "STORAGE_R2_OBJECT_TOO_LARGE";
|
|
28
|
+
readonly category = ProblemCategory.InternalServerError;
|
|
29
|
+
constructor(key: string, maxBytes: number);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Cloudflare R2 스토리지 제공자
|
|
34
|
+
*
|
|
35
|
+
* AWS S3 SDK를 사용하여 R2와 통신합니다.
|
|
36
|
+
*/
|
|
37
|
+
declare class R2StorageProvider extends BaseStorageProvider {
|
|
38
|
+
private readonly config;
|
|
39
|
+
readonly _logger: Logger;
|
|
40
|
+
private static readonly MAX_BUFFERED_GET_BYTES;
|
|
41
|
+
private readonly client;
|
|
42
|
+
private readonly options;
|
|
43
|
+
private readonly retryTemplate;
|
|
44
|
+
private static readonly REQUIRED_CONFIG_KEYS;
|
|
45
|
+
constructor(config: ConfigService, _logger: Logger);
|
|
46
|
+
private validateRequiredConfig;
|
|
47
|
+
put(key: string, data: Buffer | Readable, options?: PutOptions): Promise<void>;
|
|
48
|
+
getStream(key: string): Promise<Readable>;
|
|
49
|
+
get(key: string): Promise<Buffer>;
|
|
50
|
+
delete(key: string): Promise<void>;
|
|
51
|
+
getPublicUrl(key: string): string;
|
|
52
|
+
getSignedUrl(key: string, options: SignedUrlOptions): Promise<string>;
|
|
53
|
+
getMetadata(key: string): Promise<ObjectMetadata>;
|
|
54
|
+
private isNotFoundError;
|
|
55
|
+
private handleNotFoundError;
|
|
56
|
+
private executeWithRetry;
|
|
57
|
+
private executeR2Operation;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Cloudflare R2 제공자 생성에 필요한 설정입니다.
|
|
62
|
+
*/
|
|
63
|
+
type R2Options = {
|
|
64
|
+
/**
|
|
65
|
+
* Cloudflare Account ID
|
|
66
|
+
*/
|
|
67
|
+
accountId: string;
|
|
68
|
+
/**
|
|
69
|
+
* R2 Access Key ID
|
|
70
|
+
*/
|
|
71
|
+
accessKeyId: string;
|
|
72
|
+
/**
|
|
73
|
+
* R2 Secret Access Key
|
|
74
|
+
*/
|
|
75
|
+
secretAccessKey: string;
|
|
76
|
+
/**
|
|
77
|
+
* R2 버킷 이름
|
|
78
|
+
*/
|
|
79
|
+
bucket: string;
|
|
80
|
+
/**
|
|
81
|
+
* 공개 URL 기본 경로 (선택)
|
|
82
|
+
*
|
|
83
|
+
* Custom domain을 사용하는 경우 설정합니다.
|
|
84
|
+
* 예: 'https://cdn.example.com'
|
|
85
|
+
*
|
|
86
|
+
* 설정하지 않으면 R2의 기본 퍼블릭 URL을 사용합니다:
|
|
87
|
+
* `https://{bucket}.{accountId}.r2.dev`
|
|
88
|
+
*/
|
|
89
|
+
publicUrlBase?: string;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* R2 설정 옵션 DI 토큰
|
|
94
|
+
*/
|
|
95
|
+
declare const R2_OPTIONS: unique symbol;
|
|
96
|
+
|
|
97
|
+
export { EmptyR2BodyProblem, MissingR2ConfigProblem, R2ObjectTooLargeProblem, type R2Options, R2StorageProvider, R2_OPTIONS };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { StorageProblem, BaseStorageProvider, PutOptions, SignedUrlOptions, ObjectMetadata } from '@croco/storage-core';
|
|
2
|
+
import { ProblemCategory } from '@croco/problems-core';
|
|
3
|
+
import { Readable } from 'node:stream';
|
|
4
|
+
import { ConfigService } from '@croco/framework-config';
|
|
5
|
+
import { Logger } from '@croco/framework-logger';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* R2 다운로드 응답에 본문이 없을 때 발생하는 문제입니다.
|
|
9
|
+
*/
|
|
10
|
+
declare class EmptyR2BodyProblem extends StorageProblem {
|
|
11
|
+
constructor(key: string);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 필수 R2 설정이 누락되었을 때 발생하는 문제입니다.
|
|
16
|
+
*/
|
|
17
|
+
declare class MissingR2ConfigProblem extends StorageProblem {
|
|
18
|
+
readonly code = "STORAGE_R2_MISSING_CONFIG";
|
|
19
|
+
readonly category = ProblemCategory.InternalServerError;
|
|
20
|
+
constructor(configKey: string);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 메모리 기반 다운로드 한도를 초과한 객체를 읽으려 할 때 발생하는 문제입니다.
|
|
25
|
+
*/
|
|
26
|
+
declare class R2ObjectTooLargeProblem extends StorageProblem {
|
|
27
|
+
readonly code = "STORAGE_R2_OBJECT_TOO_LARGE";
|
|
28
|
+
readonly category = ProblemCategory.InternalServerError;
|
|
29
|
+
constructor(key: string, maxBytes: number);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Cloudflare R2 스토리지 제공자
|
|
34
|
+
*
|
|
35
|
+
* AWS S3 SDK를 사용하여 R2와 통신합니다.
|
|
36
|
+
*/
|
|
37
|
+
declare class R2StorageProvider extends BaseStorageProvider {
|
|
38
|
+
private readonly config;
|
|
39
|
+
readonly _logger: Logger;
|
|
40
|
+
private static readonly MAX_BUFFERED_GET_BYTES;
|
|
41
|
+
private readonly client;
|
|
42
|
+
private readonly options;
|
|
43
|
+
private readonly retryTemplate;
|
|
44
|
+
private static readonly REQUIRED_CONFIG_KEYS;
|
|
45
|
+
constructor(config: ConfigService, _logger: Logger);
|
|
46
|
+
private validateRequiredConfig;
|
|
47
|
+
put(key: string, data: Buffer | Readable, options?: PutOptions): Promise<void>;
|
|
48
|
+
getStream(key: string): Promise<Readable>;
|
|
49
|
+
get(key: string): Promise<Buffer>;
|
|
50
|
+
delete(key: string): Promise<void>;
|
|
51
|
+
getPublicUrl(key: string): string;
|
|
52
|
+
getSignedUrl(key: string, options: SignedUrlOptions): Promise<string>;
|
|
53
|
+
getMetadata(key: string): Promise<ObjectMetadata>;
|
|
54
|
+
private isNotFoundError;
|
|
55
|
+
private handleNotFoundError;
|
|
56
|
+
private executeWithRetry;
|
|
57
|
+
private executeR2Operation;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Cloudflare R2 제공자 생성에 필요한 설정입니다.
|
|
62
|
+
*/
|
|
63
|
+
type R2Options = {
|
|
64
|
+
/**
|
|
65
|
+
* Cloudflare Account ID
|
|
66
|
+
*/
|
|
67
|
+
accountId: string;
|
|
68
|
+
/**
|
|
69
|
+
* R2 Access Key ID
|
|
70
|
+
*/
|
|
71
|
+
accessKeyId: string;
|
|
72
|
+
/**
|
|
73
|
+
* R2 Secret Access Key
|
|
74
|
+
*/
|
|
75
|
+
secretAccessKey: string;
|
|
76
|
+
/**
|
|
77
|
+
* R2 버킷 이름
|
|
78
|
+
*/
|
|
79
|
+
bucket: string;
|
|
80
|
+
/**
|
|
81
|
+
* 공개 URL 기본 경로 (선택)
|
|
82
|
+
*
|
|
83
|
+
* Custom domain을 사용하는 경우 설정합니다.
|
|
84
|
+
* 예: 'https://cdn.example.com'
|
|
85
|
+
*
|
|
86
|
+
* 설정하지 않으면 R2의 기본 퍼블릭 URL을 사용합니다:
|
|
87
|
+
* `https://{bucket}.{accountId}.r2.dev`
|
|
88
|
+
*/
|
|
89
|
+
publicUrlBase?: string;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* R2 설정 옵션 DI 토큰
|
|
94
|
+
*/
|
|
95
|
+
declare const R2_OPTIONS: unique symbol;
|
|
96
|
+
|
|
97
|
+
export { EmptyR2BodyProblem, MissingR2ConfigProblem, R2ObjectTooLargeProblem, type R2Options, R2StorageProvider, R2_OPTIONS };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var F=Object.create;var y=Object.defineProperty;var b=Object.getOwnPropertyDescriptor;var $=Object.getOwnPropertyNames;var j=Object.getPrototypeOf,M=Object.prototype.hasOwnProperty;var R=(t,n)=>(n=Symbol[t])?n:Symbol.for("Symbol."+t);var k=(t,n)=>{for(var e in n)y(t,e,{get:n[e],enumerable:!0})},C=(t,n,e,o)=>{if(n&&typeof n=="object"||typeof n=="function")for(let a of $(n))!M.call(t,a)&&a!==e&&y(t,a,{get:()=>n[a],enumerable:!(o=b(n,a))||o.enumerable});return t};var p=(t,n,e)=>(e=t!=null?F(j(t)):{},C(n||!t||!t.__esModule?y(e,"default",{value:t,enumerable:!0}):e,t)),G=t=>C(y({},"__esModule",{value:!0}),t),w=(t,n,e,o)=>{for(var a=o>1?void 0:o?b(n,e):n,i=t.length-1,r;i>=0;i--)(r=t[i])&&(a=(o?r(n,e,a):r(a))||a);return o&&a&&y(n,e,a),a};var T=(t,n,e)=>(n=t[R("asyncIterator")])?n.call(t):(t=t[R("iterator")](),n={},e=(o,a)=>(a=t[o])&&(n[o]=i=>new Promise((r,c,m)=>(i=a.call(t,i),m=i.done,Promise.resolve(i.value).then(u=>r({value:u,done:m}),c)))),e("next"),e("return"),n);var J={};k(J,{EmptyR2BodyProblem:()=>d,MissingR2ConfigProblem:()=>l,R2ObjectTooLargeProblem:()=>f,R2StorageProvider:()=>s,R2_OPTIONS:()=>A});module.exports=G(J);var S=require("@croco/problems-core"),O=require("@croco/storage-core"),d=class extends O.StorageProblem{constructor(n){super("STORAGE_R2_EMPTY_BODY",S.ProblemCategory.InternalServerError,"Empty response body",{extensions:{key:n}})}};var _=require("@croco/problems-core"),x=require("@croco/storage-core"),l=class extends x.StorageProblem{constructor(e){super(void 0,void 0,`Missing required R2 configuration: ${e}`);this.code="STORAGE_R2_MISSING_CONFIG";this.category=_.ProblemCategory.InternalServerError}};var B=require("@croco/problems-core"),N=require("@croco/storage-core"),f=class extends N.StorageProblem{constructor(e,o){super(void 0,void 0,`R2 object '${e}' exceeds the in-memory download limit of ${o} bytes`);this.code="STORAGE_R2_OBJECT_TOO_LARGE";this.category=B.ProblemCategory.InternalServerError}};var I=require("@aws-sdk/client-s3"),U=require("@aws-sdk/s3-request-presigner"),v=require("@croco/framework-context"),P=require("@croco/retry-core"),K=require("@croco/storage-core");var Y=new Set([408,425,429,500,502,503,504]),L=new Set(["ECONNABORTED","ECONNRESET","ENETDOWN","ENETRESET","ENETUNREACH","ENOTFOUND","ETIMEDOUT","RequestTimeout","RequestTimeoutException","SlowDown","Throttling","ThrottlingException","TimeoutError","TooManyRequestsException"]),q=t=>{if(!(!t||typeof t!="object")){if("$metadata"in t){let n=t.$metadata;if(typeof n.httpStatusCode=="number")return n.httpStatusCode}if("statusCode"in t&&typeof t.statusCode=="number")return t.statusCode;if("status"in t&&typeof t.status=="number")return t.status}},W=t=>{if(!(!t||typeof t!="object")){if("code"in t&&typeof t.code=="string")return t.code;if("name"in t&&typeof t.name=="string")return t.name}},z=(t,n)=>t instanceof Error||t&&typeof t=="object"&&"message"in t&&typeof t.message=="string"?t.message:typeof t=="string"?t:n,H=(t,n)=>{if(t instanceof Error)return t;let e=new Error(z(t,n));return t&&typeof t=="object"&&("code"in t&&typeof t.code=="string"&&(e.code=t.code),"name"in t&&typeof t.name=="string"&&(e.name=t.name),"status"in t&&typeof t.status=="number"&&(e.status=t.status),"statusCode"in t&&typeof t.statusCode=="number"&&(e.statusCode=t.statusCode),"$metadata"in t&&t.$metadata&&typeof t.$metadata=="object"&&(e.$metadata=t.$metadata)),e},X=t=>{let n=q(t);if(typeof n=="number"&&Y.has(n))return!0;let e=W(t);return typeof e=="string"&&L.has(e)},Q={shouldRetry(t,n,e){return n<e&&X(t)}},s=class extends K.BaseStorageProvider{constructor(e,o){super();this.config=e;this._logger=o;this.retryTemplate=new P.RetryTemplate({maxAttempts:3,backoff:{delay:10,multiplier:2,maxDelay:50,jitter:!1},retryPolicy:Q});this.options={accountId:this.validateRequiredConfig("R2_ACCOUNT_ID"),accessKeyId:this.validateRequiredConfig("R2_ACCESS_KEY_ID"),secretAccessKey:this.validateRequiredConfig("R2_SECRET_ACCESS_KEY"),bucket:this.validateRequiredConfig("R2_BUCKET"),publicUrlBase:this.config.get("R2_PUBLIC_URL_BASE")},this.client=new I.S3Client({region:"auto",endpoint:`https://${this.options.accountId}.r2.cloudflarestorage.com`,credentials:{accessKeyId:this.options.accessKeyId,secretAccessKey:this.options.secretAccessKey}})}validateRequiredConfig(e){let o=this.config.get(e);if(!o)throw new l(e);return o}async put(e,o,a){this.validateKey(e);try{await(async r=>{let{PutObjectCommand:c}=await import("@aws-sdk/client-s3"),m=new c({Bucket:this.options.bucket,Key:e,Body:o,ContentType:a==null?void 0:a.contentType,CacheControl:a==null?void 0:a.cacheControl,Metadata:a==null?void 0:a.metadata}),u=async()=>await this.executeR2Operation(()=>this.client.send(m),"Unknown upload error");if(r){await this.executeWithRetry(u);return}await u()})(Buffer.isBuffer(o))}catch(i){this.throwUploadFailed(e,i)}}async getStream(e){this.validateKey(e);let{GetObjectCommand:o}=await import("@aws-sdk/client-s3"),a=new o({Bucket:this.options.bucket,Key:e});try{let i=await this.executeWithRetry(async()=>await this.executeR2Operation(()=>this.client.send(a),"Unknown download error"));if(!i.Body)throw new d(e);return i.Body}catch(i){return this.handleNotFoundError(e,i)}}async get(e){this.validateKey(e);try{let{GetObjectCommand:c}=await import("@aws-sdk/client-s3"),m=new c({Bucket:this.options.bucket,Key:e}),u=await this.executeWithRetry(async()=>await this.executeR2Operation(()=>this.client.send(m),"Unknown download error"));if(!u.Body)throw new d(e);let h=[],D=u.Body,E=0;try{for(var o=T(D),a,i,r;a=!(i=await o.next()).done;a=!1){let g=i.value;if(E+=g.byteLength,E>s.MAX_BUFFERED_GET_BYTES)throw new f(e,s.MAX_BUFFERED_GET_BYTES);h.push(g)}}catch(i){r=[i]}finally{try{a&&(i=o.return)&&await i.call(o)}finally{if(r)throw r[0]}}return Buffer.concat(h)}catch(c){return this.handleNotFoundError(e,c)}}async delete(e){this.validateKey(e);try{await this.executeWithRetry(async()=>{let{DeleteObjectCommand:o}=await import("@aws-sdk/client-s3"),a=new o({Bucket:this.options.bucket,Key:e});await this.executeR2Operation(()=>this.client.send(a),"Unknown delete error")})}catch(o){this.throwDeleteFailed(e,o)}}getPublicUrl(e){return this.validateKey(e),this.options.publicUrlBase?`${this.options.publicUrlBase.replace(/\/+$/,"")}/${e}`:`https://${this.options.bucket}.${this.options.accountId}.r2.dev/${e}`}async getSignedUrl(e,o){this.validateKey(e);let{GetObjectCommand:a}=await import("@aws-sdk/client-s3"),i=new a({Bucket:this.options.bucket,Key:e});return(0,U.getSignedUrl)(this.client,i,{expiresIn:o.expiresIn})}async getMetadata(e){var o,a;this.validateKey(e);try{let i=await this.executeWithRetry(async()=>{let{HeadObjectCommand:r}=await import("@aws-sdk/client-s3"),c=new r({Bucket:this.options.bucket,Key:e});return await this.executeR2Operation(()=>this.client.send(c),"Unknown metadata error")});return{size:(o=i.ContentLength)!=null?o:0,contentType:i.ContentType,lastModified:(a=i.LastModified)!=null?a:new Date,etag:i.ETag,metadata:i.Metadata}}catch(i){return this.handleNotFoundError(e,i)}}isNotFoundError(e){return e&&typeof e=="object"&&"$metadata"in e?e.$metadata.httpStatusCode===404:e instanceof Error&&"name"in e?e.name==="NotFound":!1}handleNotFoundError(e,o){throw this.isNotFoundError(o)&&this.throwNotFound(e,o),o}async executeWithRetry(e){return await this.retryTemplate.execute(async()=>await e())}async executeR2Operation(e,o){try{return await e()}catch(a){throw H(a,o)}}};s.MAX_BUFFERED_GET_BYTES=10*1024*1024,s.REQUIRED_CONFIG_KEYS=["R2_ACCOUNT_ID","R2_ACCESS_KEY_ID","R2_SECRET_ACCESS_KEY","R2_BUCKET"],s=w([(0,v.Component)()],s);var A=Symbol("R2_OPTIONS");0&&(module.exports={EmptyR2BodyProblem,MissingR2ConfigProblem,R2ObjectTooLargeProblem,R2StorageProvider,R2_OPTIONS});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var C=Object.defineProperty;var w=Object.getOwnPropertyDescriptor;var E=(t,a)=>(a=Symbol[t])?a:Symbol.for("Symbol."+t);var g=(t,a,e,n)=>{for(var o=n>1?void 0:n?w(a,e):a,i=t.length-1,r;i>=0;i--)(r=t[i])&&(o=(n?r(a,e,o):r(o))||o);return n&&o&&C(a,e,o),o};var R=(t,a,e)=>(a=t[E("asyncIterator")])?a.call(t):(t=t[E("iterator")](),a={},e=(n,o)=>(o=t[n])&&(a[n]=i=>new Promise((r,c,d)=>(i=o.call(t,i),d=i.done,Promise.resolve(i.value).then(u=>r({value:u,done:d}),c)))),e("next"),e("return"),a);import{ProblemCategory as T}from"@croco/problems-core";import{StorageProblem as S}from"@croco/storage-core";var m=class extends S{constructor(a){super("STORAGE_R2_EMPTY_BODY",T.InternalServerError,"Empty response body",{extensions:{key:a}})}};import{ProblemCategory as O}from"@croco/problems-core";import{StorageProblem as _}from"@croco/storage-core";var p=class extends _{constructor(e){super(void 0,void 0,`Missing required R2 configuration: ${e}`);this.code="STORAGE_R2_MISSING_CONFIG";this.category=O.InternalServerError}};import{ProblemCategory as x}from"@croco/problems-core";import{StorageProblem as B}from"@croco/storage-core";var l=class extends B{constructor(e,n){super(void 0,void 0,`R2 object '${e}' exceeds the in-memory download limit of ${n} bytes`);this.code="STORAGE_R2_OBJECT_TOO_LARGE";this.category=x.InternalServerError}};import{S3Client as N}from"@aws-sdk/client-s3";import{getSignedUrl as I}from"@aws-sdk/s3-request-presigner";import{Component as U}from"@croco/framework-context";import{RetryTemplate as v}from"@croco/retry-core";import{BaseStorageProvider as P}from"@croco/storage-core";var K=new Set([408,425,429,500,502,503,504]),A=new Set(["ECONNABORTED","ECONNRESET","ENETDOWN","ENETRESET","ENETUNREACH","ENOTFOUND","ETIMEDOUT","RequestTimeout","RequestTimeoutException","SlowDown","Throttling","ThrottlingException","TimeoutError","TooManyRequestsException"]),D=t=>{if(!(!t||typeof t!="object")){if("$metadata"in t){let a=t.$metadata;if(typeof a.httpStatusCode=="number")return a.httpStatusCode}if("statusCode"in t&&typeof t.statusCode=="number")return t.statusCode;if("status"in t&&typeof t.status=="number")return t.status}},F=t=>{if(!(!t||typeof t!="object")){if("code"in t&&typeof t.code=="string")return t.code;if("name"in t&&typeof t.name=="string")return t.name}},$=(t,a)=>t instanceof Error||t&&typeof t=="object"&&"message"in t&&typeof t.message=="string"?t.message:typeof t=="string"?t:a,j=(t,a)=>{if(t instanceof Error)return t;let e=new Error($(t,a));return t&&typeof t=="object"&&("code"in t&&typeof t.code=="string"&&(e.code=t.code),"name"in t&&typeof t.name=="string"&&(e.name=t.name),"status"in t&&typeof t.status=="number"&&(e.status=t.status),"statusCode"in t&&typeof t.statusCode=="number"&&(e.statusCode=t.statusCode),"$metadata"in t&&t.$metadata&&typeof t.$metadata=="object"&&(e.$metadata=t.$metadata)),e},M=t=>{let a=D(t);if(typeof a=="number"&&K.has(a))return!0;let e=F(t);return typeof e=="string"&&A.has(e)},k={shouldRetry(t,a,e){return a<e&&M(t)}},s=class extends P{constructor(e,n){super();this.config=e;this._logger=n;this.retryTemplate=new v({maxAttempts:3,backoff:{delay:10,multiplier:2,maxDelay:50,jitter:!1},retryPolicy:k});this.options={accountId:this.validateRequiredConfig("R2_ACCOUNT_ID"),accessKeyId:this.validateRequiredConfig("R2_ACCESS_KEY_ID"),secretAccessKey:this.validateRequiredConfig("R2_SECRET_ACCESS_KEY"),bucket:this.validateRequiredConfig("R2_BUCKET"),publicUrlBase:this.config.get("R2_PUBLIC_URL_BASE")},this.client=new N({region:"auto",endpoint:`https://${this.options.accountId}.r2.cloudflarestorage.com`,credentials:{accessKeyId:this.options.accessKeyId,secretAccessKey:this.options.secretAccessKey}})}validateRequiredConfig(e){let n=this.config.get(e);if(!n)throw new p(e);return n}async put(e,n,o){this.validateKey(e);try{await(async r=>{let{PutObjectCommand:c}=await import("@aws-sdk/client-s3"),d=new c({Bucket:this.options.bucket,Key:e,Body:n,ContentType:o==null?void 0:o.contentType,CacheControl:o==null?void 0:o.cacheControl,Metadata:o==null?void 0:o.metadata}),u=async()=>await this.executeR2Operation(()=>this.client.send(d),"Unknown upload error");if(r){await this.executeWithRetry(u);return}await u()})(Buffer.isBuffer(n))}catch(i){this.throwUploadFailed(e,i)}}async getStream(e){this.validateKey(e);let{GetObjectCommand:n}=await import("@aws-sdk/client-s3"),o=new n({Bucket:this.options.bucket,Key:e});try{let i=await this.executeWithRetry(async()=>await this.executeR2Operation(()=>this.client.send(o),"Unknown download error"));if(!i.Body)throw new m(e);return i.Body}catch(i){return this.handleNotFoundError(e,i)}}async get(e){this.validateKey(e);try{let{GetObjectCommand:c}=await import("@aws-sdk/client-s3"),d=new c({Bucket:this.options.bucket,Key:e}),u=await this.executeWithRetry(async()=>await this.executeR2Operation(()=>this.client.send(d),"Unknown download error"));if(!u.Body)throw new m(e);let f=[],b=u.Body,y=0;try{for(var n=R(b),o,i,r;o=!(i=await n.next()).done;o=!1){let h=i.value;if(y+=h.byteLength,y>s.MAX_BUFFERED_GET_BYTES)throw new l(e,s.MAX_BUFFERED_GET_BYTES);f.push(h)}}catch(i){r=[i]}finally{try{o&&(i=n.return)&&await i.call(n)}finally{if(r)throw r[0]}}return Buffer.concat(f)}catch(c){return this.handleNotFoundError(e,c)}}async delete(e){this.validateKey(e);try{await this.executeWithRetry(async()=>{let{DeleteObjectCommand:n}=await import("@aws-sdk/client-s3"),o=new n({Bucket:this.options.bucket,Key:e});await this.executeR2Operation(()=>this.client.send(o),"Unknown delete error")})}catch(n){this.throwDeleteFailed(e,n)}}getPublicUrl(e){return this.validateKey(e),this.options.publicUrlBase?`${this.options.publicUrlBase.replace(/\/+$/,"")}/${e}`:`https://${this.options.bucket}.${this.options.accountId}.r2.dev/${e}`}async getSignedUrl(e,n){this.validateKey(e);let{GetObjectCommand:o}=await import("@aws-sdk/client-s3"),i=new o({Bucket:this.options.bucket,Key:e});return I(this.client,i,{expiresIn:n.expiresIn})}async getMetadata(e){var n,o;this.validateKey(e);try{let i=await this.executeWithRetry(async()=>{let{HeadObjectCommand:r}=await import("@aws-sdk/client-s3"),c=new r({Bucket:this.options.bucket,Key:e});return await this.executeR2Operation(()=>this.client.send(c),"Unknown metadata error")});return{size:(n=i.ContentLength)!=null?n:0,contentType:i.ContentType,lastModified:(o=i.LastModified)!=null?o:new Date,etag:i.ETag,metadata:i.Metadata}}catch(i){return this.handleNotFoundError(e,i)}}isNotFoundError(e){return e&&typeof e=="object"&&"$metadata"in e?e.$metadata.httpStatusCode===404:e instanceof Error&&"name"in e?e.name==="NotFound":!1}handleNotFoundError(e,n){throw this.isNotFoundError(n)&&this.throwNotFound(e,n),n}async executeWithRetry(e){return await this.retryTemplate.execute(async()=>await e())}async executeR2Operation(e,n){try{return await e()}catch(o){throw j(o,n)}}};s.MAX_BUFFERED_GET_BYTES=10*1024*1024,s.REQUIRED_CONFIG_KEYS=["R2_ACCOUNT_ID","R2_ACCESS_KEY_ID","R2_SECRET_ACCESS_KEY","R2_BUCKET"],s=g([U()],s);var G=Symbol("R2_OPTIONS");export{m as EmptyR2BodyProblem,p as MissingR2ConfigProblem,l as R2ObjectTooLargeProblem,s as R2StorageProvider,G as R2_OPTIONS};
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@croco/storage-r2",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"files": [
|
|
5
|
+
"dist"
|
|
6
|
+
],
|
|
7
|
+
"type": "commonjs",
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@aws-sdk/client-s3": "^3.1039.0",
|
|
15
|
+
"@aws-sdk/s3-request-presigner": "^3.1039.0",
|
|
16
|
+
"@croco/framework-config": "0.0.1",
|
|
17
|
+
"@croco/framework-context": "0.0.1",
|
|
18
|
+
"@croco/framework-logger": "0.0.1",
|
|
19
|
+
"@croco/problems-core": "0.0.1",
|
|
20
|
+
"@croco/retry-core": "0.0.1",
|
|
21
|
+
"@croco/storage-core": "0.0.1"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^22.10.5"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsup src/index.ts --format esm,cjs --minify --clean --dts",
|
|
28
|
+
"deploy": "pnpm run build && pnpm publish --no-git-checks",
|
|
29
|
+
"lint": "oxlint .",
|
|
30
|
+
"test": "vitest run",
|
|
31
|
+
"typecheck": "tsc --noEmit"
|
|
32
|
+
},
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"import": "./dist/index.js",
|
|
36
|
+
"require": "./dist/index.cjs",
|
|
37
|
+
"types": "./dist/index.d.ts"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|