7. Afbeeldingen uploaden
7. Afbeeldingen uploaden
In dit hoofdstuk bespreken we twee verschillende manieren om afbeeldingen (of andere bestanden) te uploaden naar cloud storage. De eerste optie maakt gebruik van Vercel Blob en is enkel te gebruiken als je ook je eigen backend schrijft, de tweede optie maakt gebruik van Supabase Storage en kan zowel gebruikt worden als je een eigen backend schrijft of als stand-alone oplossing.
Vercel Blob
Om Vercel blob storage te gebruiken moet je eerst een Vercel project aanmaken zoals beschreven in appendix 4. Het configureren van de Postgres database kan je overslaan als je het Next project niet wilt gebruiken in combinatie met mobile development. Je kan Vercel blob (met enkele limitaties) aanspreken vanaf localhost.
Vervolgens activeer je Blob storage in de projectinstellingen, hiervoor navigeer je naar settings en klik je op 'Create database' en selecteer je 'Blob'.

Doorloop het aanmaakproces, kies een naam voor je blob database en selecteer de environments waarin de blob database beschikbaar moet zijn.

Nu dat je blob storage geactiveerd is, moet je de environment key toevoegen aan de .env file van je Next project. Commit deze wijzigingen op Git.
Waarschuwing
Dit is een gevoelige key en je plaatst deze normaal niet mee in git, maar omdat de docenten jouw project moeten kunnen bekijken en compileren moet de key mee in git gezet worden.

Info
Backend Next.js applicatie
BLOB_READ_WRITE_TOKEN="vercel_blob_rw_***********":::
Image uploaden
Hieronder vind je een voorbeeld van hoe je een afbeelding kan uploaden naar Vercel Blob via een API route. De client verstuurd een afbeelding naar de server en de server upload deze vervolgens naar de blob storage. Alhoewel deze aanpak veilig en eenvoudig is, heeft deze een belangrijk nadeel, de afbeeldingen kunnen maximaal 4.5MB groot zijn.
Als je met grotere bestanden wilt werken (wat we afraden omdat een gebruiker normaal nooit zulke grote bestanden moet uploaden en omdat de kost om grotere bestanden te uploaden snel kan doortellen), kan je een client-upload gebruiken. Op deze manier kan je bestanden tot 5TB uploaden. Voor meer informatie verwijzen we door naar de Vercel documentatie.
Installeer alvast de @vercel/blob library.
Image upload action
Vie de put methode uit de @vercel/blob library kan je een bestand uploaden naar de blob storage. Het resultaat deze actie kan gebruikt worden om de URL uit te lezen, de URL kan je vervolgens bewaren in een database.
Info
Backend Next.js applicatie
import {formAction} from '@mediators'
import {ActionResponse} from '@models'
import {Schemas} from '@/lib/server/schemas'
import {put} from '@vercel/blob'
import {revalidatePath} from 'next/cache'
export async function uploadImage(_prevData: ActionResponse, unvalidatedData: FormData): Promise<ActionResponse> {
return formAction(Schemas.imageUpload, unvalidatedData, async (data) => {
const blob = await put(data.image.name, data.image, {
access: 'public',
});
console.log('Image URL:', blob.url)
revalidatePath('/')
})
}import {z} from 'zod'
export const imageUpload = z.object({
image: z
.instanceof(File)
.refine(data => data.size > 0, {message: 'You must select an image'})
.refine(data => data.size < 4.5 * 1024 * 1024, {message: 'The size of the image must be less than 4.5MB'}),
}):::
Upload formulier aanmaken
Om een afbeelding te uploaden vanuit een formulier moet je een input element van het type file gebruiken. Vervolgens kan je het formulier gewoon inzenden zoals in les 4 besproken.
Als je preview wilt tonen van de afbeelding die de gebruiker geselecteerd heeft, kan je, via URL.createObjectURL, een URL genereren waarmee de afbeelding uitgelezen kan worden.
Info
Backend Next.js applicatie
'use client'
import {FunctionComponent, useActionState, useRef, useState} from 'react'
import {Actions} from '@actions'
import {useForm} from 'react-hook-form'
import Form from '@/component/custom/form'
import {Input} from '@/components/ui/input'
import FormError from '@/component/custom/formError'
import {Button} from '@/components/ui/button'
const UploadImageForm: FunctionComponent = () => {
const inputFileRef = useRef<HTMLInputElement>(null)
const [serverErrors, uploadImage, isPending] = useActionState(Actions.uploadImage, {success: false})
const [imageUrl, setImageURL] = useState<string>("https://placehold.co/600x400")
const form = useForm()
return (
<div className="m-4">
<Form hookForm={form} action={uploadImage} className="flex gap-4 flex-col mt-4">
<img src={imageUrl} className="w-[250px] h-auto aspect-auto border-2 border-muted self-center" onClick={() => inputFileRef?.current?.click()}/>
<Input name="image" ref={inputFileRef} type="file" className="hidden" onChange={evt => {
const file = evt.target.files?.[0]
if (file) {
setImageURL(URL.createObjectURL(file))
}
}}/>
<FormError path="image" serverErrors={serverErrors} formErrors={form.formState.errors}/>
<Button type="submit" disabled={isPending}>Upload</Button>
</Form>
</div>
)
}
export default UploadImageForm:::
Afbeeldingen uitlezen uit vercel blob
Via de list methode uit de @vercel/blob library kan je alle bestanden in de blob storage uitlezen als een gepagineerde lijst. Je moet deze methode natuurlijk niet gebruiken, in de meeste situaties bewaar je de URL van de afbeelding in een database, als kolom of property van de rij of het document waarbij de afbeelding hoort. Soms is het handig om alle afbeeldingen uit te lezen met de list methode.
Info
Backend Next.js applicatie
import {list} from '@vercel/blob'
const HomePage: FunctionComponent = async () => {
const uploadedImages = await list()
return (
<>
<h1 className="text-6xl">Uploaded images</h1>
{uploadedImages.blobs.length === 0 && <p className="text-xl my-4">No images uploaded yet</p>}
<div className="flex gap-4 my-4">
{uploadedImages.blobs.map(blob => (
<img src={blob.url} key={blob.url} className="w-[250px] h-auto aspect-auto border-2 border-muted"/>
))}
</div>
<h1 className="text-6xl">Upload an image</h1>
<UploadImageForm/>
</>
)
}:::
Afbeeldingen uploaden vanuit een mobile applicatie
Om een afbeelding te uploaden vanuit een mobiele applicatie moet je eerste een route handler toevoegen die de afbeelding kan ontvangen en uploaden naar de blob storage.
De route krijgt binaire data binnen, daarom moet je de blob() methode gebruiken om de body uit te lezen. Vervolgens gebruik je de put methode opnieuw om de afbeelding te uploaden naar de blob storage. Merk op dat deze methode een pathnaam verwacht als eerste parameter, hier kan je eigenlijk eender wat gebruiken omdat Vercel automatisch een unieke suffix toevoegt aan elk geupload bestand. Hieronder gebruiken we de randomUUID methode uit de crypto module om een unieke naam te genereren. Tenslotte kan de URL teruggegeven worden.
Info
Backend Next.js applicatie
import {NextRequest, NextResponse} from 'next/server'
import {put} from '@vercel/blob'
import {randomUUID} from 'crypto'
export async function POST(request: NextRequest) {
const image = await request.blob()
const blob = await put(randomUUID(), image, {
access: 'public',
})
return new NextResponse(
JSON.stringify({uri: blob.url}),
{status: 200},
)
}:::
Vervolgens kunnen we deze methode oproepen vanuit de mobiele applicatie. Hiervoor is een nieuwe API methode nodig waar je een TanStack wrapper rond schrijft.
Onderstaande code verwacht één parameter, fileUri. Deze URI kan uit de galerij komen, gegenereerd worden via React Native Vision camera of uitgelezen worden uit de assets (zie het uitgewerkte voorbeeld). De URI verwijst hoe dan ook naar een locatie op het bestandssysteem en moet dus eerst ingelezen worden voordat je deze kunnen versturen naar de API route in je backend applicatie. Deze conversie gebeurd het eenvoudigst via fetch, als je arrayBuffer oproept op de response van fetch krijg je een array met binaire data (bytes) die je kan meegeven als body van een post.
Merk op dat onderstaande code de afbeelding post naar 10.0.2.2, dit is het IP adres van de host machine als je met een Android emulator werkt. Dit betekent dus dat je de code lokaal kan testen zonder dat je de backend applicatie moet deployen. Dit neemt niet weg dat je de backend wel degelijk moet deployen als je jouw mobiele applicatie inlevert. Natuurlijk moet je in productie het IP adres dan ook vervangen met het domein van je backend.
Info
Mobiele applicatie
interface UploadImageParams {
fileUri: string
}
const uploadImageToVercel = async ({fileUri}: UploadImageParams): Promise<string> => {
const file = await fetch(fileUri).then(res => res.arrayBuffer())
const result = await fetch('http://10.0.2.2:3000/api/images', {
method: 'post',
body: file,
headers: {
'Content-Type': `image/${fileUri.split('.').pop()}`,
},
})
}:::
Supabase Storage
Als je gebruik wilt maken van Supabase Storage moet je eerst een Supabase projecten aanmaken via de Supabase website. In dit nieuwe project moet je vervolgens de nodige settings aanpassen.
Voor we deze besproken moet er met volgende zaken rekening gehouden worden:
- Een gratis Supabase project wordt automatisch gepauzeerd als er 7 dagen geen requests geweest zijn. Als je project gepauzeerd wordt, kunnen de docenten je project niet testen. Zorg dus dat dit actief blijft tot na je examen, dit doe je door ofwel minstens elke 7 dagen een request te sturen of door je project terug te activeren als je een mail krijgt dat dit gepauzeerd is.
- Je zal de storage onbeveiligd moeten gebruiken tenzij je ook Supabase Auth en de Supabase Database gebruikt. Aangezien iedereen eender wat kan uploaden, gebruik je deze techniek best niet in een echt project. Omdat het aantal gratis blob storage services heel beperkt is, kiezen we er in deze cursus toch voor om deze techniek toe te lichten.
Storage activeren
Navigeer in de side-bar naar storage en klik op 'New bucket'.

Kies vervolgens voor een publieke bucket waar iedereen afbeeldingen uit kan lezen, stel best ook een uploadlimiet en de toegestane bestandstypes in.

Vervolgens moeten de access-policies ingesteld worden zodat anonieme gebruikers data kunnen wegschrijven naar de storage bucket.
Waarschuwing
Dit is onveilig en wordt hier enkel aangeraden omdat dit een school project is waar je moet bewijzen dat je iets kan, maar waar je de tijd niet hebt om aan security te denken.

Kies vervolgens voor "Full Customization" en geef onderstaande settings in. De naam van je image bucket kan natuurlijk verschillen als je hierboven iets anders gekozen hebt dan "image-demo".

Druk tenslotte op "Review" en en "Save policy".
Supabase environment variabelen toevoegen
Voordat je Supabase kan gebruiken in je mobiele applicatie moet je de nodige environment variables toevoegen aan .env. Deze variabelen zijn bedoeld om op de client gebruikt te worden en mogen dus mee op git gezet worden.
Om de nodige data op te zoeken navigeer je naar de project settings.

In de project settings moet je naar het API tabblad navigeren waar je de site-url en anon key kunt kopiëren.

Voeg deze keys toe aan de .env file van je mobiele applicatie.
Info
Mobiele applicatie
# TER ILLUSTRATIE, GEBRUIK JE EIGEN KEYS
EXPO_PUBLIC_SUPABASE_URL=https://ckxolxtahstdeaytcxdm.supabase.co
EXPO_PUBLIC_SUPABASE_ANON=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImNreG9seHRhaHN0ZGVheXRjeGRtIiwicm9sZSI6ImFub24iLCJpYXQiOjE2ODM1Njg0MTcsImV4cCI6MTk5OTE0NDQxN30.nTpo8bP5JPbTdS-mWEAH4dp2UtRUJd7SVA71Ervphes:::
Supabase client aanmaken
Installeer eerst de Supabase client library.
Vervolgens moet de verbinding met Supabase aangemaakt worden. Dit doe je door de createClient methode aan te roepen met de site-url en de anon key.
Info
Mobiele applicatie
import {createClient} from '@supabase/supabase-js'
const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL
const supabaseKey = process.env.EXPO_PUBLIC_SUPABASE_ANON
if (!supabaseUrl || !supabaseKey) {
throw new Error('Missing Supabase URL and/or key')
}
export const supabaseClient = createClient(supabaseUrl, supabaseKey):::
Afbeeldingen uploaden
De laatste stap is het uploaden van de afbeelding naar Supabase. Hiervoor schrijven je een nieuwe API functie die vervolgens aangeroepen wordt via een TanStack wrapper.
Onderstaande code verwacht één parameter, fileUri. Deze URI kan uit de galerij komen, gegenereerd worden via React Native Vision camera of uitgelezen worden uit de assets (zie het uitgewerkte voorbeeld). De URI verwijst hoe dan ook naar een locatie op het bestandssysteem en moet dus eerst ingelezen worden voordat je deze kunnen versturen naar de API route in je backend applicatie. Deze conversie gebeurd het eenvoudigst via fetch, als je arrayBuffer oproept op de response van fetch krijg je een array met binaire data (bytes) die je kan meegeven als body van een post.
Info
Mobiele applicatie
interface UploadImageParams {
fileUri: string
}
const uploadImageToSupabase = async ({fileUri}: UploadImageParams): Promise<string> => {
// De Supabase SDK heeft een Array Buffer nodig, via Fetch kunnen we de file uitlezen uit het bestandsysteem en
// deze vervolgens omvormen naar een array buffer.
const file = await fetch(fileUri).then(res => res.arrayBuffer())
const extension = fileUri.split('.').pop()
const fileName = `${randomUUID()}.${extension}`
const {error} = await supabaseClient.storage
// Pas de naam aan naar je eigen bucket.
.from('image-demo')
.upload(fileName, file, {contentType: `image/${extension}`})
if (error) {
throw error
}
// Bewaar het pad van de afbeelding in de DB.
const {data: {publicUrl}} = supabaseClient.storage
// Pas de naam aan naar je eigen bucket.
.from('image-demo')
.getPublicUrl(fileName)
return publicUrl
}:::