Image gallery example
To demonstrate how to use the Web3.Storage JavaScript library to build an application, we've written a simple image gallery app for uploading your favorite memes and GIFs to the decentralized web.
You can play with the app in your browser, since it has been uploaded to Web3.Storage and is available using any IPFS HTTP gateway. All you need is an API token for Web3.Storage.
If you want to run locally, you just need git and a recent version of Node.js. Here's how to start a hot-reloading development server that will update the app as you play with the source code:
# Clone the repository.
git clone https://github.com/web3-storage/example-image-gallery
cd example-image-gallery
# Install dependencies. This may take a few minutes.
npm install
# Run the app in development mode.
npm run dev
Leave the last command running, and open your browser to the URL printed in your terminal, which is usually http://localhost:3000
.
This guide will walk through some of the code in the example app, focusing on the parts that interact with Web3.Storage.
To see the full code, head to the web3-storage/example-image-gallery repository on GitHub. All the code we'll look at in this guide is contained in src/js/storage.js, which handles the interactions with Web3.Storage.
Token management
When you first start the app, it will check your browser's local storage for a saved API token for Web3.Storage. If it doesn't find one, the app will redirect to /settings.html
, which displays a form to paste in a token.
Before saving the token, we call a validateToken
function that tries to create a new Web3.Storage client and call the list
method. This will throw an authorization error if the token is invalid, causing validateToken
to return false
. If validateToken
returns true
, we save the token to local storage and prompt the user to upload an image.
/**
* Checks if the given API token is valid by issuing a request.
* @param {string} token
* @returns {Promise<boolean>} resolves to true if the token is valid, false if invalid.
*/
export async function validateToken(token) {
console.log('validating token',token)
const web3storage = new Web3Storage({ token })
try {
for await (const _ of web3storage.list({ maxResults: 1})) {
// any non-error response means the token is legit
break
}
return true
} catch (e) {
// only return false for auth-related errors
if (e.message.includes('401') || e.message.includes('403')) {
console.log('invalid token', e.message)
return false
}
// propagate non-auth errors
throw e
}
}
Keep it safe, and keep it secret!
Your API token gives access to your Web3.Storage account, so you shouldn't include a token directly into your front-end source code. This example has the user paste in their own token, which allows the app to run completely from the browser without hard-coding any tokens into the source code.. Alternatively, you could run a small backend service that manages the token and proxies calls from your users to Web3.Storage.
Image upload
To upload images, we use the put
method to store a File
object containing image data. We also store a small metadata.json
file alongside each image, containing a user-provided caption and the filename of the original image file.
To identify our files for display in the image gallery, we use the name
parameter to tag our uploads with the prefix ImageGallery
. Later we'll filter out uploads that don't have the prefix when we're building the image gallery view.
// We use this to identify our uploads in the client.list response.
const namePrefix = 'ImageGallery'
/**
* Stores an image file on Web3.Storage, along with a small metadata.json that includes a caption & filename.
* @param {File} imageFile a File object containing image data
* @param {string} caption a string that describes the image
*
* @typedef StoreImageResult
* @property {string} cid the Content ID for an directory containing the image and metadata
* @property {string} imageURI an ipfs:// URI for the image file
* @property {string} metadataURI an ipfs:// URI for the metadata file
* @property {string} imageGatewayURL an HTTP gateway URL for the image
* @property {string} metadataGatewayURL an HTTP gateway URL for the metadata file
*
* @returns {Promise<StoreImageResult>} an object containing links to the uploaded content
*/
export async function storeImage(imageFile, caption) {
// The name for our upload includes a prefix we can use to identify our files later
const uploadName = [namePrefix, caption].join('|')
// We store some metadata about the image alongside the image file.
// The metadata includes the file path, which we can use to generate
// a URL to the full image.
const metadataFile = jsonFile('metadata.json', {
path: imageFile.name,
caption
})
const token = getSavedToken()
if (!token) {
showMessage('> ❗️ no API token found for Web3.Storage. You can add one in the settings page!')
showLink(`${location.protocol}//${location.host}/settings.html`)
return
}
const web3storage = new Web3Storage({ token })
showMessage(`> 🤖 calculating content ID for ${imageFile.name}`)
const cid = await web3storage.put([imageFile, metadataFile], {
// the name is viewable at https://web3.storage/files and is included in the status and list API responses
name: uploadName,
// onRootCidReady will be called as soon as we've calculated the Content ID locally, before uploading
onRootCidReady: (localCid) => {
showMessage(`> 🔑 locally calculated Content ID: ${localCid} `)
showMessage('> 📡 sending files to web3.storage ')
},
// onStoredChunk is called after each chunk of data is uploaded
onStoredChunk: (bytes) => showMessage(`> 🛰 sent ${bytes.toLocaleString()} bytes to web3.storage`)
})
const metadataGatewayURL = makeGatewayURL(cid, 'metadata.json')
const imageGatewayURL = makeGatewayURL(cid, imageFile.name)
const imageURI = `ipfs://${cid}/${imageFile.name}`
const metadataURI = `ipfs://${cid}/metadata.json`
return { cid, metadataGatewayURL, imageGatewayURL, imageURI, metadataURI }
}
Note that the storeImage
function uses a few utility functions that aren't included in this walkthrough. To see the details of the jsonFile
, getSavedToken
, showMessage
, showLink
, and makeGatewayURL
functions, see src/js/helpers.js
Viewing images
To build the image gallery UI, we use the Web3.Storage client's list
method to get metadata about each upload, filtering out any that don't have our ImageGallery
name prefix.
/**
* Get metadata objects for each image stored in the gallery.
*
* @returns {AsyncIterator<ImageMetadata>} an async iterator that will yield an ImageMetadata object for each stored image.
*/
export async function* listImageMetadata() {
const token = getSavedToken()
if (!token) {
console.error('No API token for Web3.Storage found.')
return
}
const web3storage = new Web3Storage({ token })
for await (const upload of web3storage.list()) {
if (!upload.name || !upload.name.startsWith(namePrefix)) {
continue
}
try {
const metadata = await getImageMetadata(upload.cid)
yield metadata
} catch (e) {
console.error('error getting image metadata:', e)
continue
}
}
}
For each matching upload, we call getImageMetadata
to fetch the metadata.json
file that was stored along with each image. The contents of metadata.json
are returned along with an IPFS gateway URL to the image file, which can be used to display the images in the UI.
The getImageMetadata
function simply requests the metadata.json
file from an IPFS HTTP gateway and parses the JSON content.
/**
* Fetches the metadata JSON from an image upload.
* @param {string} cid the CID for the IPFS directory containing the metadata & image
*
* @typedef {object} ImageMetadata
* @property {string} cid the root cid of the IPFS directory containing the image & metadata
* @property {string} path the path within the IPFS directory to the image file
* @property {string} caption a user-provided caption for the image
* @property {string} gatewayURL an IPFS gateway url for the image
* @property {string} uri an IPFS uri for the image
*
* @returns {Promise<ImageMetadata>} a promise that resolves to a metadata object for the image
*/
export async function getImageMetadata(cid) {
const url = makeGatewayURL(cid, 'metadata.json')
const res = await fetch(url)
if (!res.ok) {
throw new Error(`error fetching image metadata: [${res.status}] ${res.statusText}`)
}
const metadata = await res.json()
const gatewayURL = makeGatewayURL(cid, metadata.path)
const uri = `ipfs://${cid}/${metadata.path}`
return { ...metadata, cid, gatewayURL, uri }
}
State management at scale
Listing all the uploads and filtering out the ones we don't want works for a simple example like this, but this approach will degrade in performance once a lot of data has been uploaded. A real application should use a database or other state management solution instead.
Conclusion
The Web3.Storage service and client library make getting your data onto decentralized storage easier than ever. In this guide we saw how to use Web3.Storage to build a simple image gallery using vanilla JavaScript. We hope that this example will help you build amazing things, and we can't wait to see what you make!