docs(storage): signed resumable uploads docs and example (#40793)
This commit is contained in:
@@ -302,6 +302,12 @@ Uppy has integrations with different frameworks:
|
||||
- [Vue](https://uppy.io/docs/vue/)
|
||||
- [Angular](https://uppy.io/docs/angular/)
|
||||
|
||||
### Presigned uploads
|
||||
|
||||
Resumable uploads also supports using signed upload tokens to created time-limited URLs that you can share to your users by invoking the `createSignedUploadUrl` method on the SDK and including the returned token in the `x-signature` header of the resumable upload.
|
||||
|
||||
See this [full example using Uppy with signed URLs](https://github.com/supabase/supabase/tree/master/examples/storage/resumable-upload-signed-uppy) for more context.
|
||||
|
||||
## Overwriting files
|
||||
|
||||
When uploading a file to a path that already exists, the default behavior is to return a `400 Asset Already Exists` error.
|
||||
|
||||
23
examples/storage/resumable-upload-signed-uppy/README.md
Normal file
23
examples/storage/resumable-upload-signed-uppy/README.md
Normal file
@@ -0,0 +1,23 @@
|
||||
## Resumable Uploads with Supabase Storage and Uppy
|
||||
|
||||
This example shows how to use signed urls from [Supabase Storage](https://supabase.io/docs/reference/javascript/storage) with [Uppy](https://uppy.io/) to upload files to Supabase Storage using the TUS protocol (signed resumable uploads).
|
||||
|
||||
This works by calling `createSignedUploadUrl()` to get a token for each file, and passing that token via the `x-signature` header when uploading the files
|
||||
|
||||
### Running the example
|
||||
|
||||
- Start local supabase project `supabase start`
|
||||
- Open the index.html file and set `SUPABASE_PUBLISHABLE_KEY` to the value output when starting the supabase cli
|
||||
- Serve the index.html file locally (e.g. with Python Simple HTTP Server or http-server npm package) and start uploading:
|
||||
|
||||
```bash
|
||||
# python http server
|
||||
python3 -m http.server
|
||||
|
||||
# npm http-server
|
||||
npx http-server
|
||||
```
|
||||
|
||||
### How it works
|
||||
|
||||
In index.html the `uppy.on('file-added')` hook calls the [create-upload-token](supabase/functions/create-upload-token/index.ts) function which creates a token for each added file and attaches it to that file's header config as `x-signature`.
|
||||
113
examples/storage/resumable-upload-signed-uppy/index.html
Normal file
113
examples/storage/resumable-upload-signed-uppy/index.html
Normal file
@@ -0,0 +1,113 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Signed Resumable Upload Supabase + UppyJS</title>
|
||||
<link href="https://releases.transloadit.com/uppy/v3.6.1/uppy.min.css" rel="stylesheet" />
|
||||
|
||||
<style>
|
||||
html {
|
||||
background: #9e44ef;
|
||||
}
|
||||
body {
|
||||
height: 100vh;
|
||||
background: radial-gradient(72.03% 66.03% at 50% 69.72%, #dbb8bf 0, transparent 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
a {
|
||||
display: block;
|
||||
margin: 10px;
|
||||
text-decoration: none;
|
||||
}
|
||||
#logo {
|
||||
max-width: 150px;
|
||||
}
|
||||
#drag-drop-area {
|
||||
margin-top: 40px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<img id="logo" src="supabase-logo-wordmark--dark.png" />
|
||||
<div id="drag-drop-area"></div>
|
||||
<a href="https://supabase.com/docs/guides/storage/uploads/resumable-uploads" target="_blank"
|
||||
>Read the docs.</a
|
||||
>
|
||||
|
||||
<script type="module">
|
||||
import {
|
||||
Uppy,
|
||||
Dashboard,
|
||||
Tus,
|
||||
} from 'https://releases.transloadit.com/uppy/v3.6.1/uppy.min.mjs'
|
||||
|
||||
const SUPABASE_PUBLISHABLE_KEY = '' // your project's publishable (anon) key
|
||||
const PROJECT_URL = 'http://127.0.0.1:54321'
|
||||
const STORAGE_BUCKET = 'uploads'
|
||||
|
||||
async function getSignedUploadToken(filename) {
|
||||
const res = await fetch(`${PROJECT_URL}/functions/v1/create-upload-token`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', apikey: SUPABASE_PUBLISHABLE_KEY },
|
||||
body: JSON.stringify({ filename }),
|
||||
})
|
||||
|
||||
if (!res.ok) throw new Error('Failed to get upload token')
|
||||
|
||||
const data = await res.json()
|
||||
|
||||
return data.token
|
||||
}
|
||||
|
||||
var uppy = new Uppy()
|
||||
.use(Dashboard, {
|
||||
inline: true,
|
||||
limit: 10,
|
||||
target: '#drag-drop-area',
|
||||
showProgressDetails: true,
|
||||
})
|
||||
.use(Tus, {
|
||||
endpoint: `${PROJECT_URL}/storage/v1/upload/resumable/sign`,
|
||||
headers: {
|
||||
apikey: SUPABASE_PUBLISHABLE_KEY,
|
||||
},
|
||||
uploadDataDuringCreation: true,
|
||||
chunkSize: 6 * 1024 * 1024,
|
||||
allowedMetaFields: ['bucketName', 'objectName', 'contentType', 'cacheControl'],
|
||||
onError: function (error) {
|
||||
console.log('Failed because: ' + error)
|
||||
},
|
||||
})
|
||||
|
||||
uppy.on('file-added', async (file) => {
|
||||
const supabaseMetadata = {
|
||||
bucketName: STORAGE_BUCKET,
|
||||
objectName: file.name,
|
||||
contentType: file.type,
|
||||
}
|
||||
|
||||
file.meta = {
|
||||
...file.meta,
|
||||
...supabaseMetadata,
|
||||
}
|
||||
|
||||
// Important - add signing token header
|
||||
const token = await getSignedUploadToken(file.name)
|
||||
uppy.setFileState(file.id, {
|
||||
tus: {
|
||||
headers: {
|
||||
'x-signature': token,
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
uppy.on('complete', (result) => {
|
||||
console.log('Upload complete! We’ve uploaded these files:', result.successful)
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 7.1 KiB |
4
examples/storage/resumable-upload-signed-uppy/supabase/.gitignore
vendored
Normal file
4
examples/storage/resumable-upload-signed-uppy/supabase/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# Supabase
|
||||
.branches
|
||||
.temp
|
||||
.env
|
||||
@@ -0,0 +1,32 @@
|
||||
# A string used to distinguish different Supabase projects on the same host. Defaults to the
|
||||
# working directory name when running `supabase init`.
|
||||
project_id = "resumable-upload-uppy"
|
||||
|
||||
[api]
|
||||
# Disable data API since we are not using the PostgREST client in this example.
|
||||
enabled = false
|
||||
|
||||
[storage]
|
||||
# The maximum file size allowed for all buckets in the project.
|
||||
file_size_limit = "50MiB"
|
||||
|
||||
[storage.image_transformation]
|
||||
enabled = false
|
||||
|
||||
[storage.buckets.uploads]
|
||||
public = true
|
||||
# file_size_limit = "50MiB"
|
||||
# allowed_mime_types = ["image/png", "image/jpeg"]
|
||||
# Uncomment to specify a local directory to upload objects to the bucket.
|
||||
# objects_path = "./buckets/uploads"
|
||||
|
||||
[functions.create-upload-token]
|
||||
enabled = true
|
||||
verify_jwt = true
|
||||
import_map = "./functions/create-upload-token/deno.json"
|
||||
# Uncomment to specify a custom file path to the entrypoint.
|
||||
# Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx
|
||||
entrypoint = "./functions/create-upload-token/index.ts"
|
||||
# Specifies static files to be bundled with the function. Supports glob patterns.
|
||||
# For example, if you want to serve static HTML pages in your function:
|
||||
# static_files = [ "./functions/create-upload-token/*.html" ]
|
||||
@@ -0,0 +1,3 @@
|
||||
# Configuration for private npm package dependencies
|
||||
# For more information on using private registries with Edge Functions, see:
|
||||
# https://supabase.com/docs/guides/functions/import-maps#importing-from-private-registries
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"imports": {}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import 'jsr:@supabase/functions-js/edge-runtime.d.ts'
|
||||
import { createClient } from 'jsr:@supabase/supabase-js'
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
const SUPABASE_URL = Deno.env.get('SUPABASE_URL') ?? ''
|
||||
const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
|
||||
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY)
|
||||
|
||||
try {
|
||||
const { filename } = await req.json()
|
||||
if (!filename) {
|
||||
return new Response('Missing filename', { status: 400 })
|
||||
}
|
||||
|
||||
const { data, error } = await supabase.storage.from('uploads').createSignedUploadUrl(filename)
|
||||
|
||||
if (error) {
|
||||
return new Response(error.message, { status: 500 })
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ token: data.token }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
} catch (error) {
|
||||
return new Response((error as Error).message, { status: 500 })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1 @@
|
||||
CREATE POLICY "allow uploads" ON storage.objects FOR INSERT TO public WITH CHECK (bucket_id = 'uploads');
|
||||
Reference in New Issue
Block a user