Skip to content

Commit 6bd161a

Browse files
committed
make project public
1 parent 1c2444f commit 6bd161a

File tree

10 files changed

+779
-0
lines changed

10 files changed

+779
-0
lines changed

.gitignore

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
2+
3+
# Logs
4+
5+
logs
6+
_.log
7+
npm-debug.log_
8+
yarn-debug.log*
9+
yarn-error.log*
10+
lerna-debug.log*
11+
.pnpm-debug.log*
12+
13+
# Caches
14+
15+
.cache
16+
17+
# Diagnostic reports (https://nodejs.org/api/report.html)
18+
19+
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
20+
21+
# Runtime data
22+
23+
pids
24+
_.pid
25+
_.seed
26+
*.pid.lock
27+
28+
# Directory for instrumented libs generated by jscoverage/JSCover
29+
30+
lib-cov
31+
32+
# Coverage directory used by tools like istanbul
33+
34+
coverage
35+
*.lcov
36+
37+
# nyc test coverage
38+
39+
.nyc_output
40+
41+
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
42+
43+
.grunt
44+
45+
# Bower dependency directory (https://bower.io/)
46+
47+
bower_components
48+
49+
# node-waf configuration
50+
51+
.lock-wscript
52+
53+
# Compiled binary addons (https://nodejs.org/api/addons.html)
54+
55+
build/Release
56+
57+
# Dependency directories
58+
59+
node_modules/
60+
jspm_packages/
61+
62+
# Snowpack dependency directory (https://snowpack.dev/)
63+
64+
web_modules/
65+
66+
# TypeScript cache
67+
68+
*.tsbuildinfo
69+
70+
# Optional npm cache directory
71+
72+
.npm
73+
74+
# Optional eslint cache
75+
76+
.eslintcache
77+
78+
# Optional stylelint cache
79+
80+
.stylelintcache
81+
82+
# Microbundle cache
83+
84+
.rpt2_cache/
85+
.rts2_cache_cjs/
86+
.rts2_cache_es/
87+
.rts2_cache_umd/
88+
89+
# Optional REPL history
90+
91+
.node_repl_history
92+
93+
# Output of 'npm pack'
94+
95+
*.tgz
96+
97+
# Yarn Integrity file
98+
99+
.yarn-integrity
100+
101+
# dotenv environment variable files
102+
103+
.env
104+
.env.development.local
105+
.env.test.local
106+
.env.production.local
107+
.env.local
108+
109+
# parcel-bundler cache (https://parceljs.org/)
110+
111+
.parcel-cache
112+
113+
# Next.js build output
114+
115+
.next
116+
out
117+
118+
# Nuxt.js build / generate output
119+
120+
.nuxt
121+
dist
122+
123+
# Gatsby files
124+
125+
# Comment in the public line in if your project uses Gatsby and not Next.js
126+
127+
# https://nextjs.org/blog/next-9-1#public-directory-support
128+
129+
# public
130+
131+
# vuepress build output
132+
133+
.vuepress/dist
134+
135+
# vuepress v2.x temp and cache directory
136+
137+
.temp
138+
139+
# Docusaurus cache and generated files
140+
141+
.docusaurus
142+
143+
# Serverless directories
144+
145+
.serverless/
146+
147+
# FuseBox cache
148+
149+
.fusebox/
150+
151+
# DynamoDB Local files
152+
153+
.dynamodb/
154+
155+
# TernJS port file
156+
157+
.tern-port
158+
159+
# Stores VSCode versions used for testing VSCode extensions
160+
161+
.vscode-test
162+
163+
# yarn v2
164+
165+
.yarn/cache
166+
.yarn/unplugged
167+
.yarn/build-state.yml
168+
.yarn/install-state.gz
169+
.pnp.*
170+
171+
# IntelliJ based IDEs
172+
.idea
173+
174+
# Finder (MacOS) folder config
175+
.DS_Store

README.md

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
# 🍙 @coin-mirror/blob
2+
3+
A simple and efficient solution for uploading files to S3/R2 from Next.js and similar applications. This package serves as a lightwight alternative to [`@vercel/blob`](https://github.com/vercel/storage/tree/main/packages/blob).
4+
5+
## Why and how?
6+
7+
Uploading files to S3/R2 Buckets from a Next.js App (or any other app) can be hard and comes with a lot of boiler plate code. Especially on serverless environments. _With this package we tried to make to uploading process really simple._
8+
9+
There are two ways of uploading files:
10+
11+
- **Server-side Upload**: Uploading from the server is easy and straight forward: Just use call the `put` function with your files `Buffer`, string or `ReadableStream` - and you're done!
12+
- **Client upload**: Serverless environments like Vercel limit how much you can upload from client directly to your server (Limited to 4.5 MBs). For this case, you just need to provide an endpoint which calls `uploadHandler` function and put in the `upload` function on the client into your form. (Examples below...)
13+
14+
Uploading from files with up to 5 TB (on Cloudflare R2) will be easily possible on these ways. Let's have a look on the API.
15+
16+
## Example: Next.js Client Upload
17+
18+
In this example we will upload to an R2 Bucket where we store the avatar of the user. This example can also be applied for non-Next.js projects. Also, uploading to other S3-compatible object storages are pretty fine.
19+
20+
Let's start by installing the
21+
22+
```bash
23+
npm install @coin-mirror/blob
24+
# OR
25+
yarn add @coin-mirror/blob
26+
# OR
27+
pnpm add @coin-mirror/blob
28+
# OR
29+
bun add @coin-mirror/blob
30+
```
31+
32+
Then we need to define the bucket, for actually connecting to our R2 bucket:
33+
34+
```ts
35+
// bucket.ts (on Server!)
36+
37+
import { type Bucket } from "@coin-mirror/blob";
38+
39+
export const myBucket: Bucket = {
40+
// The credentials for R2 can be access via the Cloudflare Dashboard.
41+
// Have a look here for other connection options: https://www.npmjs.com/package/@aws-sdk/client-s3
42+
connection: {
43+
region: "auto",
44+
endpoint: process.env.R2_ENDPOINT!,
45+
credentials: {
46+
accessKeyId: process.env.R2_ACCESS_KEY!,
47+
secretAccessKey: process.env.R2_SECRET_KEY!,
48+
},
49+
},
50+
name: "public-avatars", // The name of the bucket
51+
publicUrl: "public-avatars.cdn.example.com", // Required to access the file later in the process
52+
};
53+
```
54+
55+
Now we can implement the route. This is required so that the client can request an upload url. We put the endpoint under `/app/api/upload/avatar/route.ts`.
56+
57+
```ts
58+
// route.ts (App Router Next.js Implementation)
59+
60+
import { uploadHandler } from "@coin-mirror/blob/upload/server";
61+
import { NextRequest, NextResponse } from "next/server";
62+
63+
// Needs to be a POST Request, since we transmitting some data in body + we don't want any cache.
64+
export const POST = async (req: NextRequest) => {
65+
// Don't forget to authenticate and authorize the upload request! Otherwise everyone could upload.
66+
const session = await auth();
67+
if (!session)
68+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
69+
70+
// Some pseudo-code, may you want to look up some data:
71+
const user = await getUserById(session.userId);
72+
if (!user)
73+
return NextResponse.json({ error: "User not found" }, { status: 404 });
74+
75+
// Here comes the "magic": The upload handler returns a Response with a signed URL
76+
// where the client can upload his file.
77+
return uploadHandler(req, {
78+
bucket: myBucket,
79+
pathPrefix: `users/${user.id}`,
80+
maxSizeInBytes: 5 * 1024 * 1024, // 5MB
81+
allowedContentTypes: ["image/png", "image/jpeg", "image/jpg", "image/webp"],
82+
});
83+
};
84+
```
85+
86+
Now there should be an endpoint on `POST /api/upload/avatar` or similar. With this endpoint we can implement the client component (in this case a React input component).
87+
88+
```ts
89+
// upload.tsx (React Component)
90+
91+
"use client";
92+
93+
import { upload } from "@coin-mirror/blob/upload/client";
94+
95+
function UploadComponent({
96+
onUploaded,
97+
}: {
98+
onUploaded: (url: string) => void;
99+
}) {
100+
return (
101+
<input
102+
type="file"
103+
accept="image/png, image/jpeg, image/jpg, image/webp"
104+
className="sr-only"
105+
aria-hidden
106+
id="avatarUpload"
107+
onChange={async (event) => {
108+
event.preventDefault();
109+
const file = event.target.files?.[0];
110+
if (!file) return console.log("No file selected");
111+
112+
// Check file size is useful, to prevent errors from server response
113+
if (file.size > 4 * 1024 * 1024)
114+
return window.alert(
115+
"This file is too large. Please keep it under 4MB.",
116+
);
117+
118+
try {
119+
// The upload function takes the file, requests the upload url
120+
// and does the upload via signed URL.
121+
const newBlob = await upload(file, {
122+
handleUploadUrl: `/api/upload/avatar`,
123+
});
124+
125+
// Handle the successful upload
126+
onUploaded(newBlob.url);
127+
} catch (err) {
128+
// Handle the errors
129+
console.error("Failed to upload avatar image", err);
130+
}
131+
}}
132+
name="avatar"
133+
/>
134+
);
135+
}
136+
```
137+
138+
This component should now successfully upload any avatar. Internally, the `upload` function calls the endpoint which we just implemented to request a signed url from the server for uploading the file. Then the upload will happen with an additional PUT / Multi-Part upload request.
139+
140+
## Example: Server Upload & Get Files
141+
142+
The more easy way is the directly upload files from your on-server workflows. In this example we connecting to R2 buckets, but this can also be done with any other S3-compatible object storage.
143+
144+
The first step is to install the package:
145+
146+
```bash
147+
npm install @coin-mirror/blob
148+
# OR
149+
yarn install @coin-mirror/blob
150+
# OR
151+
pnpm add @coin-mirror/blob
152+
# OR
153+
bun add @coin-mirror/blob
154+
```
155+
156+
Then we need to define the bucket, for actually connecting to our R2 bucket:
157+
158+
```ts
159+
// bucket.ts (on Server!)
160+
161+
import { type Bucket } from "@coin-mirror/blob";
162+
163+
export const pdfBucket: Bucket = {
164+
// The credentials for R2 can be access via the Cloudflare Dashboard.
165+
// Have a look here for other connection options: https://www.npmjs.com/package/@aws-sdk/client-s3
166+
connection: {
167+
region: "auto",
168+
endpoint: process.env.R2_ENDPOINT!,
169+
credentials: {
170+
accessKeyId: process.env.R2_ACCESS_KEY!,
171+
secretAccessKey: process.env.R2_SECRET_KEY!,
172+
},
173+
},
174+
name: "my-big-pdf-blob-store", // The name of the bucket
175+
publicUrl: "pdf.cdn.example.com", // Required to access the file later in the process
176+
};
177+
```
178+
179+
Let's say you have a PDF in memory, in your `Buffer` and want to upload this file to your bucket:
180+
181+
```ts
182+
// upload.ts (on server)
183+
184+
import { put } from "@coin-mirror/blob";
185+
186+
const buffer = new Buffer(/* Some buffer with your data in it. */);
187+
const path = "/bug-reports/very-important.pdf";
188+
189+
await put(pdfBucket, path, buffer);
190+
```
191+
192+
Please note, you can also use a string, ReadableStream or UIntArray as an input.
193+
194+
The same easy way, you can also get or delete the PDF file:
195+
196+
```ts
197+
// get.ts (on server)
198+
199+
import { get, deleteFile } from "@coin-mirror/blob";
200+
201+
const path = "/bug-reports/very-important.pdf";
202+
203+
const pdfBuffer = await get(pdfBucket, path);
204+
205+
await deleteFile(pdfBucket, path);
206+
```
207+
208+
## Development
209+
210+
=> Using Bun for this project.
211+
212+
## Contributing
213+
214+
1. Fork the repository
215+
2. Create your feature branch
216+
3. Test your changes.
217+
4. Submit a pull request with detailed explainations.

bun.lockb

55.5 KB
Binary file not shown.

0 commit comments

Comments
 (0)