From b03e23b165b217355047bc8d57621e770d0d975e Mon Sep 17 00:00:00 2001 From: Murderlon Date: Thu, 7 Nov 2024 14:51:13 +0100 Subject: [PATCH 1/4] Add Next.js docs --- docs/framework-integrations/nextjs.mdx | 420 +++++++++++++++++++++++++ 1 file changed, 420 insertions(+) create mode 100644 docs/framework-integrations/nextjs.mdx diff --git a/docs/framework-integrations/nextjs.mdx b/docs/framework-integrations/nextjs.mdx new file mode 100644 index 0000000000..945981bce0 --- /dev/null +++ b/docs/framework-integrations/nextjs.mdx @@ -0,0 +1,420 @@ +--- +slug: /nextjs +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Next.js + +Integration guide for the [React][] components for the Uppy UI plugins and +hooks. + +:::tip + +Uppy also has hooks and more React examples in the [React docs](/docs/react). + +::: + +## Install + + + + +```shell +npm install @uppy/core @uppy/dashboard @uppy/react +``` + + + + + +```shell +yarn add @uppy/core @uppy/dashboard @uppy/react +``` + + + + +## Tus + +[Tus][tus] is an open protocol for resumable uploads built on HTTP. This means +accidentally closing your tab or losing connection let’s you continue, for +instance, your 10GB upload instead of starting all over. + +Tus supports any language, any platform, and any network. It requires a client +and server integration to work. We will be using [tus Node.js][]. + +Checkout the [`@uppy/tus` docs](/docs/tus) for more information. + +```tsx +'use client'; + +import Uppy from '@uppy/core'; +// For now, if you do not want to install UI components you +// are not using import from lib directly. +import Dashboard from '@uppy/react/lib/Dashboard'; +import Tus from '@uppy/tus'; +import { useState } from 'react'; + +import '@uppy/core/dist/style.min.css'; +import '@uppy/dashboard/dist/style.min.css'; + +function createUppy() { + return new Uppy().use(Tus, { endpoint: '/api/upload' }); +} + +export default function UppyDashboard() { + // Important: use an initializer function to prevent the state from recreating. + const [uppy] = useState(createUppy); + + return ; +} +``` + +[`@tus/server`][] does not not support the Next.js app router yet, which is +based on the fetch `Request` API instead of `http.IncomingMessage` and +`http.ServerResponse`. + +Even if you are fully comitting to the app router, there is no downside to still +having the pages router next to it for some Node.js style API routes. + +Attach the tus server handler to a Next.js route handler in an +[optional catch-all route file](https://nextjs.org/docs/pages/building-your-application/routing/dynamic-routes#optional-catch-all-routes). + +`/pages/api/upload/[[...file]].ts` + +```ts +import type { NextApiRequest, NextApiResponse } from 'next'; +import { Server, Upload } from '@tus/server'; +import { FileStore } from '@tus/file-store'; + +/** + * !Important. This will tell Next.js NOT Parse the body as tus requires + * @see https://nextjs.org/docs/api-routes/request-helpers + */ +export const config = { + api: { + bodyParser: false, + }, +}; + +const tusServer = new Server({ + // `path` needs to match the route declared by the next file router + path: '/api/upload', + datastore: new FileStore({ directory: './files' }), +}); + +export default function handler(req: NextApiRequest, res: NextApiResponse) { + return tusServer.handle(req, res); +} +``` + +## Transloadit + +:::note + +Before continuing you should have a [Transloadit](https://transloadit.com) +account and a +[template](https://transloadit.com/docs/getting-started/my-first-app/) setup. + +::: + +Transloadit’s strength is versatility. By doing video, audio, images, documents, +and more, you only need one vendor for [all your file processing +needs][transloadit-services]. The [`@uppy/transloadit`][] plugin directly +uploads to Transloadit so you only have to worry about creating a +[template][transloadit-concepts]. It uses +[Tus](#i-want-reliable-resumable-uploads) under the hood so you don’t have to +sacrifice reliable, resumable uploads for convenience. + +When you go to production always make sure to set the `signature`. **Not using +[Signature Authentication](https://transloadit.com/docs/topics/signature-authentication/) +can be a security risk**. Signature Authentication is a security measure that +can prevent outsiders from tampering with your Assembly Instructions. + +Generating a signature should be done on the server to avoid leaking secrets. + + + + +```ts +import { NextResponse, NextRequest } from 'next/server'; +import crypto from 'crypto'; + +function utcDateString(ms: number): string { + return new Date(ms) + .toISOString() + .replace(/-/g, '/') + .replace(/T/, ' ') + .replace(/\.\d+Z$/, '+00:00'); +} + +export async function POST(request: NextRequest) { + // expire 1 hour from now (this must be milliseconds) + const expires = utcDateString(Date.now() + 1 * 60 * 60 * 1000); + const authKey = process.env.TRANSLOADIT_KEY; + const authSecret = process.env.TRANSLOADIT_SECRET; + const templateId = process.env.TRANSLOADIT_TEMPLATE_ID; + + if (!authKey || !authSecret || !templateId) { + return NextResponse.json( + { error: 'Missing Transloadit credentials' }, + { status: 500 }, + ); + } + + const body = await request.json(); + const params = JSON.stringify({ + auth: { + key: authKey, + expires, + }, + template_id: templateId, + fields: { + // You can use this in your template. + userId: body.userId, + }, + // your other params like notify_url, etc. + }); + + const signatureBytes = crypto + .createHmac('sha384', authSecret) + .update(Buffer.from(params, 'utf-8')); + // The final signature needs the hash name in front, so + // the hashing algorithm can be updated in a backwards-compatible + // way when old algorithms become insecure. + const signature = `sha384:${signatureBytes.digest('hex')}`; + + return NextResponse.json({ expires, signature, params }); +} +``` + + + + + +```ts +import type { NextApiRequest, NextApiResponse } from 'next'; +import crypto from 'node:crypto'; + +function utcDateString(ms: number): string { + return new Date(ms) + .toISOString() + .replace(/-/g, '/') + .replace(/T/, ' ') + .replace(/\.\d+Z$/, '+00:00'); +} + +export default function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method Not Allowed' }); + } + + // expire 1 hour from now (this must be milliseconds) + const expires = utcDateString(Date.now() + 1 * 60 * 60 * 1000); + const authKey = process.env.TRANSLOADIT_KEY; + const authSecret = process.env.TRANSLOADIT_SECRET; + const templateId = process.env.TRANSLOADIT_TEMPLATE_ID; + + if (!authKey || !authSecret || !templateId) { + return res.status(500).json({ error: 'Missing Transloadit credentials' }); + } + + const params = JSON.stringify({ + auth: { + key: authKey, + expires, + }, + template_id: templateId, + fields: { + // You can use this in your template. + userId: req.body.userId, + }, + // your other params like notify_url, etc. + }); + + const signatureBytes = crypto + .createHmac('sha384', authSecret) + .update(Buffer.from(params, 'utf-8')); + // The final signature needs the hash name in front, so + // the hashing algorithm can be updated in a backwards-compatible + // way when old algorithms become insecure. + const signature = `sha384:${signatureBytes.digest('hex')}`; + + res.status(200).json({ expires, signature, params }); +} +``` + + + + +On the client we want to fetch the signature and params from the server. You may +want to send values from React state along to your endpoint, for instance to add +[`fields`](https://transloadit.com/docs/topics/assembly-variables/) which you +can use in your template as global variables. + +```js +// ... +function createUppy() { + const uppy = new Uppy(); + uppy.use(Transloadit, { + async assemblyOptions() { + // You can send meta data along for use in your template. + // https://transloadit.com/docs/topics/assembly-instructions/#form-fields-in-instructions + const { meta } = uppy.getState(); + const body = JSON.stringify({ userId: meta.userId }); + const res = await fetch('/transloadit-params', { method: 'POST', body }); + return response.json(); + }, + }); + return uppy; +} + +function Component({ userId }) { + // IMPORTANT: passing an initializer function to prevent the state from recreating. + const [uppy] = useState(createUppy); + + useEffect(() => { + if (userId) { + uppy.setOptions({ meta: { userId } }); + } + }, [uppy, userId]); +} +``` + +## HTTP uploads to your backend + +If you want to handle uploads yourself, in Next.js or another server in any +language, you can use [`@uppy/xhr-upload`](/docs/xhr-upload). + +:::warning + +The server-side examples are simplified for demostration purposes and assume a +regular file upload while `@uppy/xhr-upload` can also send `FormData` through +the `formData` or `bundle` options. + +::: + + + + +```ts +import { NextRequest, NextResponse } from 'next/server'; +import { writeFile } from 'node:fs/promises'; +import path from 'node:path'; + +export const config = { + api: { + bodyParser: false, + }, +}; + +export async function POST(request: NextRequest) { + const formData = await request.formData(); + const file = formData.get('file') as File | null; + + if (!file) { + return NextResponse.json({ error: 'No file uploaded' }, { status: 400 }); + } + + const buffer = Buffer.from(await file.arrayBuffer()); + const filename = file.name.replace(/\s/g, '-'); + const filepath = path.join(process.cwd(), 'public', 'uploads', filename); + + try { + await writeFile(filepath, buffer); + return NextResponse.json({ + message: 'File uploaded successfully', + filename, + }); + } catch (error) { + console.error('Error saving file:', error); + return NextResponse.json({ error: 'Error saving file' }, { status: 500 }); + } +} +``` + + + + + +```ts +import type { NextApiRequest, NextApiResponse } from 'next'; +import { createWriteStream } from 'fs'; +import { pipeline } from 'stream/promises'; +import path from 'path'; + +export const config = { + api: { + bodyParser: false, + }, +}; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse, +) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method Not Allowed' }); + } + + try { + const filename = `file-${Date.now()}.txt`; + const filepath = path.join(process.cwd(), 'public', 'uploads', filename); + const writeStream = createWriteStream(filepath); + + await pipeline(req, writeStream); + + res.status(200).json({ message: 'File uploaded successfully', filename }); + } catch (error) { + console.error('Error saving file:', error); + res.status(500).json({ error: 'Error saving file' }); + } +} +``` + + + + +```tsx +'use client'; + +import Uppy from '@uppy/core'; +// For now, if you do not want to install UI components you +// are not using import from lib directly. +import Dashboard from '@uppy/react/lib/Dashboard'; +import Xhr from '@uppy/xhr-upload'; +import { useState } from 'react'; + +import '@uppy/core/dist/style.min.css'; +import '@uppy/dashboard/dist/style.min.css'; + +function createUppy() { + return new Uppy().use(Xhr, { endpoint: '/api/upload' }); +} + +export default function UppyDashboard() { + // Important: use an initializer function to prevent the state from recreating. + const [uppy] = useState(createUppy); + + return ; +} +``` + +## Next steps + +- Add client-side file [restrictions](/docs/uppy/#restrictions). +- Upload files together with other form fields with [`@uppy/form`](/docs/form). +- Use your [language of choice](/docs/locales) instead of English. +- Add an [image editor](docs/image-editor) for cropping and resizing images. +- Download files from remote sources, such as [Google Drive](docs/google-drive) + and [Dropbox](docs/dropbox), with [Companion](/docs/companion). +- Add [Golden Retriever](/docs/golden-retriever) to save selected files in your + browser cache, so that if the browser crashes, or the user accidentally closes + the tab, Uppy can restore everything and continue uploading as if nothing + happened. + +[tus Node.js]: https://github.com/tus/tus-node-server +[`@tus/server`]: + https://github.com/tus/tus-node-server/tree/main/packages/server From 39377b77098976d6b9cc85f7dc66b02884f6b44c Mon Sep 17 00:00:00 2001 From: Murderlon Date: Thu, 7 Nov 2024 14:58:21 +0100 Subject: [PATCH 2/4] Fixes --- docs/framework-integrations/nextjs.mdx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/framework-integrations/nextjs.mdx b/docs/framework-integrations/nextjs.mdx index 945981bce0..8988bb9c69 100644 --- a/docs/framework-integrations/nextjs.mdx +++ b/docs/framework-integrations/nextjs.mdx @@ -122,9 +122,9 @@ account and a Transloadit’s strength is versatility. By doing video, audio, images, documents, and more, you only need one vendor for [all your file processing -needs][transloadit-services]. The [`@uppy/transloadit`][] plugin directly -uploads to Transloadit so you only have to worry about creating a -[template][transloadit-concepts]. It uses +needs][transloadit-services]. The [`@uppy/transloadit`](/docs/transloadit) +plugin directly uploads to Transloadit so you only have to worry about creating +a [template][transloadit-concepts]. It uses [Tus](#i-want-reliable-resumable-uploads) under the hood so you don’t have to sacrifice reliable, resumable uploads for convenience. @@ -415,6 +415,10 @@ export default function UppyDashboard() { the tab, Uppy can restore everything and continue uploading as if nothing happened. +[transloadit-concepts]: https://transloadit.com/docs/getting-started/concepts/ +[transloadit-services]: https://transloadit.com/services/ +[react]: https://facebook.github.io/react +[tus]: https://tus.io/ [tus Node.js]: https://github.com/tus/tus-node-server [`@tus/server`]: https://github.com/tus/tus-node-server/tree/main/packages/server From d69973926687a54bf0f0ea12bea50bf38f3113b1 Mon Sep 17 00:00:00 2001 From: Murderlon Date: Mon, 18 Nov 2024 11:34:16 +0100 Subject: [PATCH 3/4] Apply feedback --- docs/framework-integrations/nextjs.mdx | 37 +++++++++++++++++--------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/docs/framework-integrations/nextjs.mdx b/docs/framework-integrations/nextjs.mdx index 8988bb9c69..99bda0b021 100644 --- a/docs/framework-integrations/nextjs.mdx +++ b/docs/framework-integrations/nextjs.mdx @@ -7,8 +7,10 @@ import TabItem from '@theme/TabItem'; # Next.js -Integration guide for the [React][] components for the Uppy UI plugins and -hooks. +Integration guide for [Next.js][] featuring the [dashboard](/docs/dashboard), +the [tus](/docs/tus) uploader, [transloadit](/docs/transloadit), multipart +uploads to a Next.js route, the Uppy UI components, and the +[React hooks](/docs/react). :::tip @@ -116,7 +118,7 @@ export default function handler(req: NextApiRequest, res: NextApiResponse) { Before continuing you should have a [Transloadit](https://transloadit.com) account and a -[template](https://transloadit.com/docs/getting-started/my-first-app/) setup. +[Template](https://transloadit.com/docs/getting-started/my-first-app/) setup. ::: @@ -138,6 +140,8 @@ Generating a signature should be done on the server to avoid leaking secrets. +`/app/api/transloadit/route.ts` + ```ts import { NextResponse, NextRequest } from 'next/server'; import crypto from 'crypto'; @@ -157,6 +161,7 @@ export async function POST(request: NextRequest) { const authSecret = process.env.TRANSLOADIT_SECRET; const templateId = process.env.TRANSLOADIT_TEMPLATE_ID; + // Typically, here you would also deny generating a signature for improper use if (!authKey || !authSecret || !templateId) { return NextResponse.json( { error: 'Missing Transloadit credentials' }, @@ -172,8 +177,9 @@ export async function POST(request: NextRequest) { }, template_id: templateId, fields: { - // You can use this in your template. - userId: body.userId, + // This becomes available in your Template as `${fields.customValue}` + // and could be used to have a storage directory per user for example + customValue: body.customValue, }, // your other params like notify_url, etc. }); @@ -194,6 +200,8 @@ export async function POST(request: NextRequest) { +`/pages/api/transloadit/params.ts` + ```ts import type { NextApiRequest, NextApiResponse } from 'next'; import crypto from 'node:crypto'; @@ -207,6 +215,7 @@ function utcDateString(ms: number): string { } export default function handler(req: NextApiRequest, res: NextApiResponse) { + // Typically, here you would also deny generating a signature for improper use if (req.method !== 'POST') { return res.status(405).json({ error: 'Method Not Allowed' }); } @@ -228,8 +237,9 @@ export default function handler(req: NextApiRequest, res: NextApiResponse) { }, template_id: templateId, fields: { - // You can use this in your template. - userId: req.body.userId, + // This becomes available in your Template as `${fields.customValue}` + // and could be used to have a storage directory per user for example + customValue: req.body.customValue, }, // your other params like notify_url, etc. }); @@ -263,7 +273,7 @@ function createUppy() { // You can send meta data along for use in your template. // https://transloadit.com/docs/topics/assembly-instructions/#form-fields-in-instructions const { meta } = uppy.getState(); - const body = JSON.stringify({ userId: meta.userId }); + const body = JSON.stringify({ customValue: meta.customValue }); const res = await fetch('/transloadit-params', { method: 'POST', body }); return response.json(); }, @@ -271,15 +281,15 @@ function createUppy() { return uppy; } -function Component({ userId }) { +function Component({ customValue }) { // IMPORTANT: passing an initializer function to prevent the state from recreating. const [uppy] = useState(createUppy); useEffect(() => { - if (userId) { - uppy.setOptions({ meta: { userId } }); + if (customValue) { + uppy.setOptions({ meta: { customValue } }); } - }, [uppy, userId]); + }, [uppy, customValue]); } ``` @@ -290,7 +300,7 @@ language, you can use [`@uppy/xhr-upload`](/docs/xhr-upload). :::warning -The server-side examples are simplified for demostration purposes and assume a +The server-side examples are simplified for demonstration purposes and assume a regular file upload while `@uppy/xhr-upload` can also send `FormData` through the `formData` or `bundle` options. @@ -418,6 +428,7 @@ export default function UppyDashboard() { [transloadit-concepts]: https://transloadit.com/docs/getting-started/concepts/ [transloadit-services]: https://transloadit.com/services/ [react]: https://facebook.github.io/react +[Next.js]: https://nextjs.org/ [tus]: https://tus.io/ [tus Node.js]: https://github.com/tus/tus-node-server [`@tus/server`]: From 50e68db289972479263b0b95edef76e3c57e4ed6 Mon Sep 17 00:00:00 2001 From: Murderlon Date: Mon, 18 Nov 2024 11:40:02 +0100 Subject: [PATCH 4/4] Unused definition --- docs/framework-integrations/nextjs.mdx | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/framework-integrations/nextjs.mdx b/docs/framework-integrations/nextjs.mdx index 99bda0b021..1aa9040fec 100644 --- a/docs/framework-integrations/nextjs.mdx +++ b/docs/framework-integrations/nextjs.mdx @@ -427,7 +427,6 @@ export default function UppyDashboard() { [transloadit-concepts]: https://transloadit.com/docs/getting-started/concepts/ [transloadit-services]: https://transloadit.com/services/ -[react]: https://facebook.github.io/react [Next.js]: https://nextjs.org/ [tus]: https://tus.io/ [tus Node.js]: https://github.com/tus/tus-node-server