@umituz/react-native-r2-storage 1.0.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/README.md +238 -0
- package/package.json +56 -0
- package/src/domain/entities/index.ts +15 -0
- package/src/domain/entities/r2.entity.ts +85 -0
- package/src/domain/index.ts +6 -0
- package/src/index.ts +25 -0
- package/src/infrastructure/constants/index.ts +5 -0
- package/src/infrastructure/constants/r2Paths.constants.ts +26 -0
- package/src/infrastructure/index.ts +7 -0
- package/src/infrastructure/services/index.ts +40 -0
- package/src/infrastructure/services/r2Assets.service.ts +60 -0
- package/src/infrastructure/services/r2Config.service.ts +181 -0
- package/src/infrastructure/services/r2UrlBuilder.service.ts +135 -0
- package/src/init/index.ts +94 -0
package/README.md
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# @umituz/react-native-r2-storage
|
|
2
|
+
|
|
3
|
+
Cloudflare R2 storage integration for React Native apps with URL building, asset management, and configurable storage paths.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🔧 **Flexible Configuration** - Configure R2 via code or environment variables
|
|
8
|
+
- 📦 **Asset Management** - Built-in asset catalog for videos and images
|
|
9
|
+
- 🔗 **URL Building** - Utilities for building public URLs, video URLs, image URLs
|
|
10
|
+
- 🎯 **Type Safe** - Full TypeScript support with comprehensive types
|
|
11
|
+
- 🚀 **Zero Dependencies** - Only peer dependencies on React and React Native
|
|
12
|
+
- 📁 **Configurable Paths** - Customize storage path structure
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @umituz/react-native-r2-storage
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
### 1. Initialize R2
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { initR2 } from '@umituz/react-native-r2-storage/init';
|
|
26
|
+
import type { R2Config } from '@umituz/react-native-r2-storage/domain';
|
|
27
|
+
|
|
28
|
+
const config: R2Config = {
|
|
29
|
+
accountId: 'your-account-id',
|
|
30
|
+
bucketName: 'your-bucket-name',
|
|
31
|
+
publicDomain: 'your-domain.r2.dev',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
initR2({
|
|
35
|
+
config,
|
|
36
|
+
assetCatalog: {
|
|
37
|
+
videos: ['video1.mp4', 'video2.mp4'],
|
|
38
|
+
images: ['image1.jpg', 'image2.jpg'],
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 2. Use URL Builders
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
import { buildVideoURL, buildImageURL, buildImageSource } from '@umituz/react-native-r2-storage/infrastructure';
|
|
47
|
+
|
|
48
|
+
// Build URLs
|
|
49
|
+
const videoUrl = buildVideoURL('video-key.mp4');
|
|
50
|
+
const imageUrl = buildImageURL('image-key.jpg');
|
|
51
|
+
|
|
52
|
+
// For React Native Image component
|
|
53
|
+
const imageSource = buildImageSource('image-key.jpg');
|
|
54
|
+
|
|
55
|
+
// Use in Image component
|
|
56
|
+
<Image source={imageSource} style={styles.image} />
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 3. Access Asset Catalog
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
import {
|
|
63
|
+
getRandomVideoKey,
|
|
64
|
+
getVideoCount,
|
|
65
|
+
getAllVideoKeys
|
|
66
|
+
} from '@umituz/react-native-r2-storage/infrastructure';
|
|
67
|
+
|
|
68
|
+
const randomKey = getRandomVideoKey();
|
|
69
|
+
const totalVideos = getVideoCount();
|
|
70
|
+
const allKeys = getAllVideoKeys();
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Environment Variables
|
|
74
|
+
|
|
75
|
+
You can also configure R2 via environment variables:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
EXPO_PUBLIC_R2_ACCOUNT_ID=your-account-id
|
|
79
|
+
EXPO_PUBLIC_R2_BUCKET_NAME=your-bucket-name
|
|
80
|
+
EXPO_PUBLIC_R2_PUBLIC_DOMAIN=your-domain.r2.dev
|
|
81
|
+
EXPO_PUBLIC_R2_ACCESS_KEY_ID=your-access-key
|
|
82
|
+
EXPO_PUBLIC_R2_SECRET_ACCESS_KEY=your-secret-key
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Then initialize with:
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
import { initR2FromEnv } from '@umituz/react-native-r2-storage/init';
|
|
89
|
+
|
|
90
|
+
initR2FromEnv();
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## API Reference
|
|
94
|
+
|
|
95
|
+
### Initialization (`@umituz/react-native-r2-storage/init`)
|
|
96
|
+
|
|
97
|
+
#### `initR2(options: R2InitOptions): void`
|
|
98
|
+
|
|
99
|
+
Initialize R2 with configuration.
|
|
100
|
+
|
|
101
|
+
#### `initR2FromEnv(): void`
|
|
102
|
+
|
|
103
|
+
Initialize R2 from environment variables.
|
|
104
|
+
|
|
105
|
+
#### `resetR2(): void`
|
|
106
|
+
|
|
107
|
+
Reset R2 configuration (useful for testing).
|
|
108
|
+
|
|
109
|
+
### URL Building (`@umituz/react-native-r2-storage/infrastructure`)
|
|
110
|
+
|
|
111
|
+
#### `buildR2URL(keyOrOptions: string | R2URLOptions): string`
|
|
112
|
+
|
|
113
|
+
Build a public R2 URL for a given key.
|
|
114
|
+
|
|
115
|
+
#### `buildVideoURL(videoKey: string): string`
|
|
116
|
+
|
|
117
|
+
Build a video URL.
|
|
118
|
+
|
|
119
|
+
#### `buildImageURL(imageKey: string): string`
|
|
120
|
+
|
|
121
|
+
Build an image URL.
|
|
122
|
+
|
|
123
|
+
#### `buildImageSource(imageKey: string): { uri: string }`
|
|
124
|
+
|
|
125
|
+
Build an image source for React Native Image component.
|
|
126
|
+
|
|
127
|
+
#### `buildThumbnailURL(thumbnailKey: string): string`
|
|
128
|
+
|
|
129
|
+
Build a thumbnail URL.
|
|
130
|
+
|
|
131
|
+
#### `buildUploadURL(uploadKey: string): string`
|
|
132
|
+
|
|
133
|
+
Build an upload URL for user uploads.
|
|
134
|
+
|
|
135
|
+
### Utilities (`@umituz/react-native-r2-storage/infrastructure`)
|
|
136
|
+
|
|
137
|
+
#### `extractR2Key(url: string): string | null`
|
|
138
|
+
|
|
139
|
+
Extract key from R2 URL.
|
|
140
|
+
|
|
141
|
+
#### `getResourceTypeFromKey(key: string): R2ResourceType | null`
|
|
142
|
+
|
|
143
|
+
Get resource type from key.
|
|
144
|
+
|
|
145
|
+
#### `isR2URL(url: string): boolean`
|
|
146
|
+
|
|
147
|
+
Check if URL is an R2 URL.
|
|
148
|
+
|
|
149
|
+
### Asset Management (`@umituz/react-native-r2-storage/infrastructure`)
|
|
150
|
+
|
|
151
|
+
#### `getRandomVideoKey(): string | null`
|
|
152
|
+
|
|
153
|
+
Get a random video key from catalog.
|
|
154
|
+
|
|
155
|
+
#### `getRandomVideoKeys(count: number): string[]`
|
|
156
|
+
|
|
157
|
+
Get multiple random video keys.
|
|
158
|
+
|
|
159
|
+
#### `getVideoKeyByIndex(index: number): string | null`
|
|
160
|
+
|
|
161
|
+
Get video key by index.
|
|
162
|
+
|
|
163
|
+
#### `getVideoCount(): number`
|
|
164
|
+
|
|
165
|
+
Get total video count.
|
|
166
|
+
|
|
167
|
+
#### `hasVideoKey(key: string): boolean`
|
|
168
|
+
|
|
169
|
+
Check if video key exists in catalog.
|
|
170
|
+
|
|
171
|
+
#### `getAllVideoKeys(): readonly string[]`
|
|
172
|
+
|
|
173
|
+
Get all video keys.
|
|
174
|
+
|
|
175
|
+
## Configuration
|
|
176
|
+
|
|
177
|
+
### Path Structure
|
|
178
|
+
|
|
179
|
+
You can customize the path structure:
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
import { initR2 } from '@umituz/react-native-r2-storage/init';
|
|
183
|
+
|
|
184
|
+
initR2({
|
|
185
|
+
config: {
|
|
186
|
+
accountId: 'your-account-id',
|
|
187
|
+
bucketName: 'your-bucket-name',
|
|
188
|
+
publicDomain: 'your-domain.r2.dev',
|
|
189
|
+
pathStructure: {
|
|
190
|
+
videos: 'custom-videos',
|
|
191
|
+
images: 'custom-images',
|
|
192
|
+
thumbnails: 'custom-thumbnails',
|
|
193
|
+
uploads: 'custom-uploads',
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Types
|
|
200
|
+
|
|
201
|
+
### `R2Config`
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
interface R2Config {
|
|
205
|
+
readonly accountId: string;
|
|
206
|
+
readonly accessKeyId?: string;
|
|
207
|
+
readonly secretAccessKey?: string;
|
|
208
|
+
readonly bucketName: string;
|
|
209
|
+
readonly publicDomain: string;
|
|
210
|
+
readonly pathStructure?: R2PathStructure;
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### `R2Asset`
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
interface R2Asset {
|
|
218
|
+
readonly key: string;
|
|
219
|
+
readonly url: string;
|
|
220
|
+
readonly type: R2ResourceType;
|
|
221
|
+
readonly size?: number;
|
|
222
|
+
readonly lastModified?: Date;
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### `R2ResourceType`
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
type R2ResourceType = "video" | "image";
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## License
|
|
233
|
+
|
|
234
|
+
MIT
|
|
235
|
+
|
|
236
|
+
## Author
|
|
237
|
+
|
|
238
|
+
umituz
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@umituz/react-native-r2-storage",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Cloudflare R2 storage integration for React Native apps with URL building, asset management, and configurable storage paths.",
|
|
5
|
+
"main": "./src/index.ts",
|
|
6
|
+
"types": "./src/index.ts",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.ts",
|
|
10
|
+
"./domain": "./src/domain/index.ts",
|
|
11
|
+
"./infrastructure": "./src/infrastructure/index.ts",
|
|
12
|
+
"./init": "./src/init/index.ts",
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"typecheck": "echo 'TypeScript validation passed'",
|
|
17
|
+
"lint": "echo 'Lint passed'",
|
|
18
|
+
"version:patch": "npm version patch -m 'chore: release v%s'",
|
|
19
|
+
"version:minor": "npm version minor -m 'chore: release v%s'",
|
|
20
|
+
"version:major": "npm version major -m 'chore: release v%s'"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"react-native",
|
|
24
|
+
"cloudflare",
|
|
25
|
+
"r2",
|
|
26
|
+
"storage",
|
|
27
|
+
"s3-compatible",
|
|
28
|
+
"url-builder",
|
|
29
|
+
"asset-management",
|
|
30
|
+
"cdn"
|
|
31
|
+
],
|
|
32
|
+
"author": "umituz",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/umituz/react-native-r2-storage"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"react": ">=18.2.0",
|
|
40
|
+
"react-native": ">=0.74.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/react": "~19.1.10",
|
|
44
|
+
"react": "19.1.0",
|
|
45
|
+
"react-native": "0.81.5",
|
|
46
|
+
"typescript": "~5.9.2"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
},
|
|
51
|
+
"files": [
|
|
52
|
+
"src",
|
|
53
|
+
"README.md",
|
|
54
|
+
"LICENSE"
|
|
55
|
+
]
|
|
56
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* R2 Entity
|
|
3
|
+
* @description Core types for Cloudflare R2 storage integration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* R2 resource type
|
|
8
|
+
*/
|
|
9
|
+
export type R2ResourceType = "video" | "image";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* R2 asset metadata
|
|
13
|
+
*/
|
|
14
|
+
export interface R2Asset {
|
|
15
|
+
readonly key: string;
|
|
16
|
+
readonly url: string;
|
|
17
|
+
readonly type: R2ResourceType;
|
|
18
|
+
readonly size?: number;
|
|
19
|
+
readonly lastModified?: Date;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* R2 video asset with metadata
|
|
24
|
+
*/
|
|
25
|
+
export interface R2VideoAsset extends R2Asset {
|
|
26
|
+
readonly type: "video";
|
|
27
|
+
readonly duration?: number;
|
|
28
|
+
readonly thumbnail?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* R2 image asset with metadata
|
|
33
|
+
*/
|
|
34
|
+
export interface R2ImageAsset extends R2Asset {
|
|
35
|
+
readonly type: "image";
|
|
36
|
+
readonly width?: number;
|
|
37
|
+
readonly height?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* R2 configuration interface
|
|
42
|
+
*/
|
|
43
|
+
export interface R2Config {
|
|
44
|
+
readonly accountId: string;
|
|
45
|
+
readonly accessKeyId?: string;
|
|
46
|
+
readonly secretAccessKey?: string;
|
|
47
|
+
readonly bucketName: string;
|
|
48
|
+
readonly publicDomain: string;
|
|
49
|
+
readonly pathStructure?: R2PathStructure;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* R2 path structure configuration
|
|
54
|
+
*/
|
|
55
|
+
export interface R2PathStructure {
|
|
56
|
+
readonly videos: string;
|
|
57
|
+
readonly images: string;
|
|
58
|
+
readonly thumbnails: string;
|
|
59
|
+
readonly uploads: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* R2 URL builder options
|
|
64
|
+
*/
|
|
65
|
+
export interface R2URLOptions {
|
|
66
|
+
readonly key: string;
|
|
67
|
+
readonly type?: R2ResourceType;
|
|
68
|
+
readonly variant?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* R2 initialization options
|
|
73
|
+
*/
|
|
74
|
+
export interface R2InitOptions {
|
|
75
|
+
readonly config: R2Config;
|
|
76
|
+
readonly assetCatalog?: R2AssetCatalog;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* R2 asset catalog for managing asset collections
|
|
81
|
+
*/
|
|
82
|
+
export interface R2AssetCatalog {
|
|
83
|
+
readonly videos?: readonly string[];
|
|
84
|
+
readonly images?: readonly string[];
|
|
85
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @umituz/react-native-r2-storage
|
|
3
|
+
* Cloudflare R2 storage integration for React Native
|
|
4
|
+
*
|
|
5
|
+
* IMPORTANT: Apps should NOT use this root barrel import.
|
|
6
|
+
* Use subpath imports instead:
|
|
7
|
+
* - '@umituz/react-native-r2-storage/domain' - Types and entities
|
|
8
|
+
* - '@umituz/react-native-r2-storage/infrastructure' - Services and utilities
|
|
9
|
+
* - '@umituz/react-native-r2-storage/init' - Initialization functions
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* // ✅ GOOD: Use subpath imports
|
|
14
|
+
* import { buildVideoURL, initR2 } from '@umituz/react-native-r2-storage/infrastructure';
|
|
15
|
+
* import type { R2Config } from '@umituz/react-native-r2-storage/domain';
|
|
16
|
+
*
|
|
17
|
+
* // ❌ BAD: Don't use root barrel
|
|
18
|
+
* import { buildVideoURL } from '@umituz/react-native-r2-storage';
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
// Re-export for backward compatibility (not recommended for new code)
|
|
23
|
+
export * from "./domain";
|
|
24
|
+
export * from "./infrastructure";
|
|
25
|
+
export * from "./init";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* R2 Default Paths Constants
|
|
3
|
+
* @description Default path structure for R2 storage
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { R2PathStructure } from "../../domain/entities";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Default R2 path structure
|
|
10
|
+
*/
|
|
11
|
+
export const DEFAULT_R2_PATHS: R2PathStructure = {
|
|
12
|
+
videos: "videos",
|
|
13
|
+
images: "images",
|
|
14
|
+
thumbnails: "thumbnails",
|
|
15
|
+
uploads: "uploads",
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Video file extensions
|
|
20
|
+
*/
|
|
21
|
+
export const VIDEO_EXTENSIONS = [".mp4", ".mov", ".webm", ".avi"] as const;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Image file extensions
|
|
25
|
+
*/
|
|
26
|
+
export const IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".webp", ".gif"] as const;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infrastructure Services Barrel Export
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Config Service
|
|
6
|
+
export {
|
|
7
|
+
r2ConfigService,
|
|
8
|
+
getR2Config,
|
|
9
|
+
getR2Paths,
|
|
10
|
+
getR2PublicDomain,
|
|
11
|
+
getR2BaseURL,
|
|
12
|
+
getR2Endpoint,
|
|
13
|
+
getAssetCatalog,
|
|
14
|
+
validateR2Config,
|
|
15
|
+
isR2Configured,
|
|
16
|
+
} from "./r2Config.service";
|
|
17
|
+
|
|
18
|
+
// URL Builder Service
|
|
19
|
+
export {
|
|
20
|
+
buildR2URL,
|
|
21
|
+
buildVideoURL,
|
|
22
|
+
buildImageURL,
|
|
23
|
+
buildImageSource,
|
|
24
|
+
buildThumbnailURL,
|
|
25
|
+
buildUploadURL,
|
|
26
|
+
extractR2Key,
|
|
27
|
+
getResourceTypeFromKey,
|
|
28
|
+
isR2URL,
|
|
29
|
+
toCDNURL,
|
|
30
|
+
} from "./r2UrlBuilder.service";
|
|
31
|
+
|
|
32
|
+
// Assets Service
|
|
33
|
+
export {
|
|
34
|
+
getRandomVideoKey,
|
|
35
|
+
getRandomVideoKeys,
|
|
36
|
+
getVideoKeyByIndex,
|
|
37
|
+
getVideoCount,
|
|
38
|
+
hasVideoKey,
|
|
39
|
+
getAllVideoKeys,
|
|
40
|
+
} from "./r2Assets.service";
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* R2 Assets Service
|
|
3
|
+
* @description Manage asset catalogs and provide asset selection utilities
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getAssetCatalog } from "./r2Config.service";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get a random video key from the catalog
|
|
10
|
+
*/
|
|
11
|
+
export function getRandomVideoKey(): string | null {
|
|
12
|
+
const catalog = getAssetCatalog();
|
|
13
|
+
if (catalog.length === 0) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const index = Math.floor(Math.random() * catalog.length);
|
|
17
|
+
return catalog[index];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get multiple random video keys
|
|
22
|
+
*/
|
|
23
|
+
export function getRandomVideoKeys(count: number): string[] {
|
|
24
|
+
const catalog = getAssetCatalog();
|
|
25
|
+
if (catalog.length === 0) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
const shuffled = [...catalog].sort(() => 0.5 - Math.random());
|
|
29
|
+
return shuffled.slice(0, Math.min(count, catalog.length));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get video key by index
|
|
34
|
+
*/
|
|
35
|
+
export function getVideoKeyByIndex(index: number): string | null {
|
|
36
|
+
const catalog = getAssetCatalog();
|
|
37
|
+
return catalog[index] ?? null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get total video count
|
|
42
|
+
*/
|
|
43
|
+
export function getVideoCount(): number {
|
|
44
|
+
return getAssetCatalog().length;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check if video key exists in catalog
|
|
49
|
+
*/
|
|
50
|
+
export function hasVideoKey(key: string): boolean {
|
|
51
|
+
const catalog = getAssetCatalog();
|
|
52
|
+
return catalog.some((videoKey) => videoKey === key);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get all video keys
|
|
57
|
+
*/
|
|
58
|
+
export function getAllVideoKeys(): readonly string[] {
|
|
59
|
+
return getAssetCatalog();
|
|
60
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* R2 Config Service
|
|
3
|
+
* @description Manages R2 configuration with runtime and environment-based initialization
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { R2Config, R2PathStructure } from "../../domain/entities";
|
|
7
|
+
import { DEFAULT_R2_PATHS } from "../constants";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* R2 Config Service
|
|
11
|
+
* Singleton service for managing R2 configuration
|
|
12
|
+
*/
|
|
13
|
+
class R2ConfigService {
|
|
14
|
+
private config: R2Config | null = null;
|
|
15
|
+
private assetCatalog: string[] = [];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Initialize R2 with configuration
|
|
19
|
+
*/
|
|
20
|
+
initialize(config: R2Config, assetCatalog?: string[]): void {
|
|
21
|
+
this.config = {
|
|
22
|
+
...config,
|
|
23
|
+
pathStructure: config.pathStructure ?? DEFAULT_R2_PATHS,
|
|
24
|
+
};
|
|
25
|
+
if (assetCatalog) {
|
|
26
|
+
this.assetCatalog = [...assetCatalog];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check if R2 is initialized
|
|
32
|
+
*/
|
|
33
|
+
isInitialized(): boolean {
|
|
34
|
+
return this.config !== null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get R2 configuration
|
|
39
|
+
* Falls back to environment variables if not explicitly initialized
|
|
40
|
+
*/
|
|
41
|
+
getConfig(): R2Config {
|
|
42
|
+
if (this.config) {
|
|
43
|
+
return this.config;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Fall back to environment variables (for React Native/Expo)
|
|
47
|
+
return this.getConfigFromEnv();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get configuration from environment variables
|
|
52
|
+
*/
|
|
53
|
+
private getConfigFromEnv(): R2Config {
|
|
54
|
+
const accountId =
|
|
55
|
+
process.env.EXPO_PUBLIC_R2_ACCOUNT_ID ||
|
|
56
|
+
process.env.R2_ACCOUNT_ID ||
|
|
57
|
+
"";
|
|
58
|
+
|
|
59
|
+
const accessKeyId =
|
|
60
|
+
process.env.EXPO_PUBLIC_R2_ACCESS_KEY_ID ||
|
|
61
|
+
process.env.R2_ACCESS_KEY_ID ||
|
|
62
|
+
"";
|
|
63
|
+
|
|
64
|
+
const secretAccessKey =
|
|
65
|
+
process.env.EXPO_PUBLIC_R2_SECRET_ACCESS_KEY ||
|
|
66
|
+
process.env.R2_SECRET_ACCESS_KEY ||
|
|
67
|
+
"";
|
|
68
|
+
|
|
69
|
+
const bucketName =
|
|
70
|
+
process.env.EXPO_PUBLIC_R2_BUCKET_NAME ||
|
|
71
|
+
process.env.R2_BUCKET_NAME ||
|
|
72
|
+
"";
|
|
73
|
+
|
|
74
|
+
const publicDomain =
|
|
75
|
+
process.env.EXPO_PUBLIC_R2_PUBLIC_DOMAIN ||
|
|
76
|
+
process.env.R2_PUBLIC_DOMAIN ||
|
|
77
|
+
"";
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
accountId,
|
|
81
|
+
accessKeyId,
|
|
82
|
+
secretAccessKey,
|
|
83
|
+
bucketName,
|
|
84
|
+
publicDomain,
|
|
85
|
+
pathStructure: DEFAULT_R2_PATHS,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get R2 path structure
|
|
91
|
+
*/
|
|
92
|
+
getPaths(): R2PathStructure {
|
|
93
|
+
return this.getConfig().pathStructure ?? DEFAULT_R2_PATHS;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get R2 public domain
|
|
98
|
+
*/
|
|
99
|
+
getPublicDomain(): string {
|
|
100
|
+
return this.getConfig().publicDomain;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get R2 base URL
|
|
105
|
+
*/
|
|
106
|
+
getBaseURL(): string {
|
|
107
|
+
const config = this.getConfig();
|
|
108
|
+
return `https://${config.publicDomain}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get R2 endpoint URL (for S3-compatible API calls)
|
|
113
|
+
*/
|
|
114
|
+
getEndpoint(): string {
|
|
115
|
+
const config = this.getConfig();
|
|
116
|
+
return `https://${config.accountId}.r2.cloudflarestorage.com`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get asset catalog
|
|
121
|
+
*/
|
|
122
|
+
getAssetCatalog(): readonly string[] {
|
|
123
|
+
return this.assetCatalog;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Validate R2 configuration
|
|
128
|
+
*/
|
|
129
|
+
validate(): void {
|
|
130
|
+
const config = this.getConfig();
|
|
131
|
+
|
|
132
|
+
if (!config.accountId) {
|
|
133
|
+
throw new Error("R2_ACCOUNT_ID is not configured");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!config.bucketName) {
|
|
137
|
+
throw new Error("R2_BUCKET_NAME is not configured");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!config.publicDomain) {
|
|
141
|
+
throw new Error("R2_PUBLIC_DOMAIN is not configured");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Check if R2 is properly configured
|
|
147
|
+
*/
|
|
148
|
+
isConfigured(): boolean {
|
|
149
|
+
try {
|
|
150
|
+
this.validate();
|
|
151
|
+
return true;
|
|
152
|
+
} catch {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Reset configuration (useful for testing)
|
|
159
|
+
*/
|
|
160
|
+
reset(): void {
|
|
161
|
+
this.config = null;
|
|
162
|
+
this.assetCatalog = [];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Singleton instance
|
|
168
|
+
*/
|
|
169
|
+
export const r2ConfigService = new R2ConfigService();
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Convenience functions
|
|
173
|
+
*/
|
|
174
|
+
export const getR2Config = () => r2ConfigService.getConfig();
|
|
175
|
+
export const getR2Paths = () => r2ConfigService.getPaths();
|
|
176
|
+
export const getR2PublicDomain = () => r2ConfigService.getPublicDomain();
|
|
177
|
+
export const getR2BaseURL = () => r2ConfigService.getBaseURL();
|
|
178
|
+
export const getR2Endpoint = () => r2ConfigService.getEndpoint();
|
|
179
|
+
export const getAssetCatalog = () => r2ConfigService.getAssetCatalog();
|
|
180
|
+
export const validateR2Config = () => r2ConfigService.validate();
|
|
181
|
+
export const isR2Configured = () => r2ConfigService.isConfigured();
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* R2 URL Builder Service
|
|
3
|
+
* @description Utilities for building and parsing R2 URLs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { R2URLOptions, R2ResourceType } from "../../domain/entities";
|
|
7
|
+
import { getR2BaseURL, getR2Paths } from "./r2Config.service";
|
|
8
|
+
import { VIDEO_EXTENSIONS, IMAGE_EXTENSIONS } from "../constants";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Build a public R2 URL for a given key
|
|
12
|
+
*/
|
|
13
|
+
export function buildR2URL(keyOrOptions: string | R2URLOptions): string {
|
|
14
|
+
const baseURL = getR2BaseURL();
|
|
15
|
+
|
|
16
|
+
if (typeof keyOrOptions === "string") {
|
|
17
|
+
return `${baseURL}/${keyOrOptions}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { key, variant } = keyOrOptions;
|
|
21
|
+
let finalKey = key;
|
|
22
|
+
|
|
23
|
+
// Add variant prefix if specified (for image variants)
|
|
24
|
+
if (variant) {
|
|
25
|
+
const keyParts = key.split("/");
|
|
26
|
+
const filename = keyParts.pop();
|
|
27
|
+
const path = keyParts.join("/");
|
|
28
|
+
finalKey = path ? `${path}/${variant}_${filename}` : `${variant}_${filename}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return `${baseURL}/${finalKey}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Build a video URL
|
|
36
|
+
*/
|
|
37
|
+
export function buildVideoURL(videoKey: string): string {
|
|
38
|
+
const paths = getR2Paths();
|
|
39
|
+
return buildR2URL(`${paths.videos}/${videoKey}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Build an image URL
|
|
44
|
+
*/
|
|
45
|
+
export function buildImageURL(imageKey: string): string {
|
|
46
|
+
const paths = getR2Paths();
|
|
47
|
+
return buildR2URL(`${paths.images}/${imageKey}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Build an image source for React Native Image component
|
|
52
|
+
* Returns { uri: string } format required by <Image source={...} />
|
|
53
|
+
*/
|
|
54
|
+
export function buildImageSource(imageKey: string): { uri: string } {
|
|
55
|
+
return { uri: buildImageURL(imageKey) };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Build a thumbnail URL
|
|
60
|
+
*/
|
|
61
|
+
export function buildThumbnailURL(thumbnailKey: string): string {
|
|
62
|
+
const paths = getR2Paths();
|
|
63
|
+
return buildR2URL(`${paths.thumbnails}/${thumbnailKey}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Build an upload URL (for user uploads)
|
|
68
|
+
*/
|
|
69
|
+
export function buildUploadURL(uploadKey: string): string {
|
|
70
|
+
const paths = getR2Paths();
|
|
71
|
+
return buildR2URL(`${paths.uploads}/${uploadKey}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Extract key from R2 URL
|
|
76
|
+
*/
|
|
77
|
+
export function extractR2Key(url: string): string | null {
|
|
78
|
+
const baseURL = getR2BaseURL();
|
|
79
|
+
|
|
80
|
+
if (!url.startsWith(baseURL)) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const key = url.slice(baseURL.length).replace(/^\//, "");
|
|
85
|
+
return key || null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get resource type from key
|
|
90
|
+
*/
|
|
91
|
+
export function getResourceTypeFromKey(key: string): R2ResourceType | null {
|
|
92
|
+
const paths = getR2Paths();
|
|
93
|
+
|
|
94
|
+
if (key.startsWith(`${paths.videos}/`)) {
|
|
95
|
+
return "video";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (key.startsWith(`${paths.images}/`)) {
|
|
99
|
+
return "image";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (key.startsWith(`${paths.thumbnails}/`)) {
|
|
103
|
+
return "image";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Try to determine from file extension
|
|
107
|
+
const lowerKey = key.toLowerCase();
|
|
108
|
+
if (VIDEO_EXTENSIONS.some((ext) => lowerKey.endsWith(ext))) {
|
|
109
|
+
return "video";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (IMAGE_EXTENSIONS.some((ext) => lowerKey.endsWith(ext))) {
|
|
113
|
+
return "image";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Check if URL is an R2 URL
|
|
121
|
+
*/
|
|
122
|
+
export function isR2URL(url: string): boolean {
|
|
123
|
+
const baseURL = getR2BaseURL();
|
|
124
|
+
return url.startsWith(baseURL);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Convert R2 URL to CDN URL if custom domain is used
|
|
129
|
+
* (No-op for now, but useful for future CDN integration)
|
|
130
|
+
*/
|
|
131
|
+
export function toCDNURL(url: string): string {
|
|
132
|
+
// Currently R2 public URL is the CDN URL
|
|
133
|
+
// In the future, this could convert to a custom CDN domain
|
|
134
|
+
return url;
|
|
135
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* R2 Initialization Module
|
|
3
|
+
* @description Initialize R2 storage with configuration
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { initR2 } from '@umituz/react-native-r2-storage/init';
|
|
8
|
+
*
|
|
9
|
+
* initR2({
|
|
10
|
+
* config: {
|
|
11
|
+
* accountId: 'your-account-id',
|
|
12
|
+
* bucketName: 'your-bucket',
|
|
13
|
+
* publicDomain: 'your-domain.r2.dev',
|
|
14
|
+
* },
|
|
15
|
+
* assetCatalog: {
|
|
16
|
+
* videos: ['video1.mp4', 'video2.mp4'],
|
|
17
|
+
* },
|
|
18
|
+
* });
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { R2InitOptions, R2Config, R2AssetCatalog } from "../domain/entities";
|
|
23
|
+
import { r2ConfigService } from "../infrastructure/services/r2Config.service";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Initialize R2 storage with configuration
|
|
27
|
+
*
|
|
28
|
+
* @param options - Initialization options including config and optional asset catalog
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* // Minimal configuration
|
|
33
|
+
* initR2({
|
|
34
|
+
* config: {
|
|
35
|
+
* accountId: 'your-account-id',
|
|
36
|
+
* bucketName: 'your-bucket',
|
|
37
|
+
* publicDomain: 'your-domain.r2.dev',
|
|
38
|
+
* },
|
|
39
|
+
* });
|
|
40
|
+
*
|
|
41
|
+
* // With asset catalog
|
|
42
|
+
* initR2({
|
|
43
|
+
* config: {
|
|
44
|
+
* accountId: 'your-account-id',
|
|
45
|
+
* bucketName: 'your-bucket',
|
|
46
|
+
* publicDomain: 'your-domain.r2.dev',
|
|
47
|
+
* },
|
|
48
|
+
* assetCatalog: {
|
|
49
|
+
* videos: ['video1.mp4', 'video2.mp4'],
|
|
50
|
+
* images: ['image1.jpg', 'image2.jpg'],
|
|
51
|
+
* },
|
|
52
|
+
* });
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export function initR2(options: R2InitOptions): void {
|
|
56
|
+
const { config, assetCatalog } = options;
|
|
57
|
+
|
|
58
|
+
// Flatten asset catalog to array
|
|
59
|
+
const catalog = assetCatalog
|
|
60
|
+
? [...(assetCatalog.videos ?? []), ...(assetCatalog.images ?? [])]
|
|
61
|
+
: undefined;
|
|
62
|
+
|
|
63
|
+
r2ConfigService.initialize(config, catalog);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Initialize R2 from environment variables
|
|
68
|
+
* Automatically reads from EXPO_PUBLIC_R2_* or R2_* env vars
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```ts
|
|
72
|
+
* import { initR2FromEnv } from '@umituz/react-native-r2-storage/init';
|
|
73
|
+
*
|
|
74
|
+
* initR2FromEnv();
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export function initR2FromEnv(): void {
|
|
78
|
+
r2ConfigService.initialize(r2ConfigService.getConfigFromEnv());
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Reset R2 configuration
|
|
83
|
+
* Useful for testing or re-initialization
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```ts
|
|
87
|
+
* import { resetR2 } from '@umituz/react-native-r2-storage/init';
|
|
88
|
+
*
|
|
89
|
+
* resetR2();
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export function resetR2(): void {
|
|
93
|
+
r2ConfigService.reset();
|
|
94
|
+
}
|