# 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.

Animated screen capture of the example app, showing a user uploading an image and viewing it in their gallery.

You can play with the app in your browser (opens new window), 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 (opens new window). All the code we'll look at in this guide is contained in src/js/storage.js (opens new window), 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.

validateToken(token)
/**
 * 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.

storeImage(imageFile, caption)

// 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 (opens new window)

# 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.

listImageMetadata()
/**
 * 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.

getImageMetadata(cid)
/**
 * 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!