cloudflare-r2-uploader 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 +214 -0
- package/package.json +25 -0
- package/src/index.js +74 -0
package/README.md
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# Cloudflare R2 Uploader
|
|
2
|
+
|
|
3
|
+
A lightweight and simple uploader for **Cloudflare R2** that works with **Next.js**, **Node.js**, and any environment that supports the Web `File` API.
|
|
4
|
+
|
|
5
|
+
Upload a `File` object to your Cloudflare R2 bucket and instantly get its public URL.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- 🚀 Simple API
|
|
12
|
+
- 📁 Upload to custom folders
|
|
13
|
+
- 🖼️ File type validation
|
|
14
|
+
- 📏 File size validation
|
|
15
|
+
- 🔑 Automatic UUID file names
|
|
16
|
+
- 🌐 Returns the public file URL
|
|
17
|
+
- ⚡ Built on the official AWS S3 SDK
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install cloudflare-r2-uploader
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Environment Variables
|
|
30
|
+
|
|
31
|
+
Create a `.env` file and add the following variables:
|
|
32
|
+
|
|
33
|
+
```env
|
|
34
|
+
R2_BUCKET=idhomesolutions
|
|
35
|
+
R2_PUBLIC_URL=https://pub-xxxxxxxxxxxxxxxx.r2.dev
|
|
36
|
+
R2_ACCOUNT_ID=xxxxxxxxxxxxxxxx
|
|
37
|
+
R2_ACCESS_ID=xxxxxxxxxxxxxxxx
|
|
38
|
+
R2_SECRET_KEY=xxxxxxxxxxxxxxxx
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
| Variable | Description |
|
|
42
|
+
|----------|-------------|
|
|
43
|
+
| `R2_BUCKET` | Your Cloudflare R2 bucket name |
|
|
44
|
+
| `R2_PUBLIC_URL` | Your bucket's public URL |
|
|
45
|
+
| `R2_ACCOUNT_ID` | Cloudflare Account ID |
|
|
46
|
+
| `R2_ACCESS_ID` | R2 Access Key ID |
|
|
47
|
+
| `R2_SECRET_KEY` | R2 Secret Access Key |
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Basic Usage
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
import upload from "cloudflare-r2-uploader"
|
|
55
|
+
|
|
56
|
+
const url = await upload(file)
|
|
57
|
+
|
|
58
|
+
console.log(url)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Returns
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
https://pub-xxxxxxxxxxxxxxxx.r2.dev/f87dbb66-4e88-4c47-8fc3-6b74dd4d2b9a.jpg
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Upload to a Folder
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
const url = await upload(file, {
|
|
73
|
+
folder: "avatars"
|
|
74
|
+
})
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Returns
|
|
78
|
+
|
|
79
|
+
```text
|
|
80
|
+
https://pub-xxxxxxxxxxxxxxxx.r2.dev/avatars/f87dbb66-4e88-4c47-8fc3-6b74dd4d2b9a.jpg
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Custom Validation
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
await upload(file, {
|
|
89
|
+
folder: "products",
|
|
90
|
+
maxSize: 10 * 1024 * 1024,
|
|
91
|
+
allowedTypes: [
|
|
92
|
+
"image/",
|
|
93
|
+
"application/pdf"
|
|
94
|
+
]
|
|
95
|
+
})
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## API
|
|
101
|
+
|
|
102
|
+
### upload(file, options)
|
|
103
|
+
|
|
104
|
+
### Parameters
|
|
105
|
+
|
|
106
|
+
| Name | Type | Required | Description |
|
|
107
|
+
|------|------|----------|-------------|
|
|
108
|
+
| `file` | `File` | ✅ | File to upload |
|
|
109
|
+
| `options.folder` | `string` | No | Upload inside a folder |
|
|
110
|
+
| `options.maxSize` | `number` | No | Maximum file size in bytes |
|
|
111
|
+
| `options.allowedTypes` | `string[]` | No | Allowed MIME types |
|
|
112
|
+
|
|
113
|
+
### Default Options
|
|
114
|
+
|
|
115
|
+
```js
|
|
116
|
+
{
|
|
117
|
+
folder: "",
|
|
118
|
+
maxSize: 5 * 1024 * 1024,
|
|
119
|
+
allowedTypes: [
|
|
120
|
+
"image/"
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Allow Multiple File Types
|
|
128
|
+
|
|
129
|
+
```js
|
|
130
|
+
await upload(file, {
|
|
131
|
+
allowedTypes: [
|
|
132
|
+
"image/",
|
|
133
|
+
"application/pdf",
|
|
134
|
+
"video/",
|
|
135
|
+
"audio/"
|
|
136
|
+
]
|
|
137
|
+
})
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Allow Only PDFs
|
|
143
|
+
|
|
144
|
+
```js
|
|
145
|
+
await upload(file, {
|
|
146
|
+
allowedTypes: [
|
|
147
|
+
"application/pdf"
|
|
148
|
+
]
|
|
149
|
+
})
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Increase Maximum Upload Size
|
|
155
|
+
|
|
156
|
+
```js
|
|
157
|
+
await upload(file, {
|
|
158
|
+
maxSize: 20 * 1024 * 1024
|
|
159
|
+
})
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Error Handling
|
|
165
|
+
|
|
166
|
+
```js
|
|
167
|
+
try {
|
|
168
|
+
const url = await upload(file)
|
|
169
|
+
} catch (error) {
|
|
170
|
+
console.error(error.message)
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Possible error messages:
|
|
175
|
+
|
|
176
|
+
| Error | Reason |
|
|
177
|
+
|-------|--------|
|
|
178
|
+
| `File is required` | No file was provided |
|
|
179
|
+
| `Invalid file` | Value is not a `File` object |
|
|
180
|
+
| `File size must be under X MB` | File exceeds the maximum allowed size |
|
|
181
|
+
| `Only image files are allowed` | Uploaded file type is not allowed |
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Example
|
|
186
|
+
|
|
187
|
+
```js
|
|
188
|
+
import upload from "cloudflare-r2-uploader"
|
|
189
|
+
|
|
190
|
+
const imageUrl = await upload(file, {
|
|
191
|
+
folder: "users",
|
|
192
|
+
maxSize: 8 * 1024 * 1024,
|
|
193
|
+
allowedTypes: [
|
|
194
|
+
"image/"
|
|
195
|
+
]
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
console.log(imageUrl)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Requirements
|
|
204
|
+
|
|
205
|
+
- Node.js 18+
|
|
206
|
+
- Cloudflare R2 Bucket
|
|
207
|
+
- Public Bucket URL
|
|
208
|
+
- Valid R2 API Keys
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## License
|
|
213
|
+
|
|
214
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cloudflare-r2-uploader",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Simple Cloudflare R2 uploader",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"cloudflare",
|
|
15
|
+
"r2",
|
|
16
|
+
"upload",
|
|
17
|
+
"storage",
|
|
18
|
+
"nextjs"
|
|
19
|
+
],
|
|
20
|
+
"author": "Muhammad Jazib",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@aws-sdk/client-s3": "^3.1090.0"
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"
|
|
2
|
+
import crypto from "crypto"
|
|
3
|
+
|
|
4
|
+
const required = [
|
|
5
|
+
"R2_BUCKET",
|
|
6
|
+
"R2_PUBLIC_URL",
|
|
7
|
+
"R2_ACCOUNT_ID",
|
|
8
|
+
"R2_ACCESS_ID",
|
|
9
|
+
"R2_SECRET_KEY"
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
for (const key of required) {
|
|
13
|
+
if (!process.env[key])
|
|
14
|
+
throw new Error(`Missing environment variable: ${key}`)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const client = new S3Client({
|
|
18
|
+
region: "auto",
|
|
19
|
+
endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
|
|
20
|
+
credentials: {
|
|
21
|
+
accessKeyId: process.env.R2_ACCESS_ID,
|
|
22
|
+
secretAccessKey: process.env.R2_SECRET_KEY
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export default async function upload(
|
|
27
|
+
file,
|
|
28
|
+
{
|
|
29
|
+
folder = "",
|
|
30
|
+
maxSize = 5 * 1024 * 1024,
|
|
31
|
+
allowedTypes = ["image/"]
|
|
32
|
+
} = {}
|
|
33
|
+
) {
|
|
34
|
+
if (!file)
|
|
35
|
+
throw new Error("File is required")
|
|
36
|
+
|
|
37
|
+
if (!(file instanceof File))
|
|
38
|
+
throw new Error("Invalid file. Expected a File object.")
|
|
39
|
+
|
|
40
|
+
if (file.size > maxSize)
|
|
41
|
+
throw new Error(`File size must be under ${maxSize / 1024 / 1024} MB`)
|
|
42
|
+
|
|
43
|
+
const isAllowed = allowedTypes.some(type => file.type.startsWith(type))
|
|
44
|
+
|
|
45
|
+
if (!isAllowed)
|
|
46
|
+
throw new Error(
|
|
47
|
+
`File type "${file.type}" is not allowed.`
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
const extension = file.name.includes(".")
|
|
51
|
+
? file.name.split(".").pop()
|
|
52
|
+
: ""
|
|
53
|
+
|
|
54
|
+
const fileName = extension
|
|
55
|
+
? `${crypto.randomUUID()}.${extension}`
|
|
56
|
+
: crypto.randomUUID()
|
|
57
|
+
|
|
58
|
+
const key = folder
|
|
59
|
+
? `${folder.replace(/^\/|\/$/g, "")}/${fileName}`
|
|
60
|
+
: fileName
|
|
61
|
+
|
|
62
|
+
const buffer = Buffer.from(await file.arrayBuffer())
|
|
63
|
+
|
|
64
|
+
await client.send(
|
|
65
|
+
new PutObjectCommand({
|
|
66
|
+
Bucket: process.env.R2_BUCKET,
|
|
67
|
+
Key: key,
|
|
68
|
+
Body: buffer,
|
|
69
|
+
ContentType: file.type
|
|
70
|
+
})
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
return `${process.env.R2_PUBLIC_URL.replace(/\/$/, "")}/${key}`
|
|
74
|
+
}
|