Building a Censorship Resistant Image Uploader

Building a Censorship Resistant Image Uploader

·

8 min read

TL;DR: You can check out a repository with the complete code on GitHub, and also a styled version of the app on IPFS.

In the past, building applications that are censorship resistant but are still easy to use wasn't straightforward.

Meddling around with pinning services or running your own IPFS node usually isn't what the average frontend dev knows how to deal with. Luckily, someone understood that issue and created web3.storage, a service that works as a layer between your Web2 skills and the shiny new decentralized world.

I try to teach frontend developers skills that make them full stack in a way that allows them as much of their frontend skills as possible. web3.storage is a perfect service for this; with its new w3up API, it got even more flexible!

In this article, I'll show you how to build an image uploader that doesn't cost a dime!

D_D Newsletter CTA

What?

We will build a web app that lets people register an email and upload images they can then share, either with an HTTP URL or an IPFS Content ID (CID).

web3.storage handles the upload for free, and the account is owned by your user directly, so you don't have to pay for your user's uploads.

Prerequisites

You need a current version of Node.js because we use the Preact-CLI and some NPM packages.

Initializing the Project

First, run the following command to create a new Preact project:

$ npx preact-cli create default quicksave

This command will generate a basic Preact project. A simple PWA structure with a service worker, manifest, and everything.

Adding w3ui Packages

web3.storage is currently adding a new API called w3up. It comes with a few improvements over the previous API, but the biggest one is that users can register for it on their own accounts directly via the API.

They even created a UI library with a few React components out of the box.

Okay, "components" is a bit of a stretch here; the packages come with a few providers and hooks, but they are still a time saver. Plus, you can build a UI, but you don't need to provide your users with your own account.

Run this command to install the packages:

$ npm i @w3ui/react-keyring \ @w3ui/react-uploader \ @w3ui/react-uploads-list

  • The @w3ui/react-keyring package links every agent, in our case the browsers, to an account with an email address.
  • The @w3ui/react-uploader package takes care of the file uploads.
  • The @w3ui/react-uploads-list package lets you retrieve the CIDs of the uploaded files.

With these three packages, your users can signup for your app, upload their files and share them via an URL or a CID.

Integrating w3ui

As I mentioned, the w3ui libraries only come with provider components and hooks; this makes them flexible and requires us to build some actual UI around them.

So, let's start by adding the providers to our Preact app.

Adding Providers

In the src/components/app.js file, replace the code with this:

import { AuthProvider } from "@w3ui/react-keyring"
import { UploaderProvider } from "@w3ui/react-uploader"
import { UploadsListProvider } from "@w3ui/react-uploads-list"

import Home from "./home"

const App = () => (
  <AuthProvider>
    <UploaderProvider>
      <UploadsListProvider>
        <Home />
      </UploadsListProvider>
    </UploaderProvider>
  </AuthProvider>
)

export default App

Nothing exciting here; just wrapping all app components with the providers.

Creating the Home Component

Next is the Home component, simply a container for the components that will do all the work.

Create a src/component/home.js file and add this code:

import Auth from "./auth"
import Uploader from "./uploader"
import UploadList from "./uploadsList"

const Home = (props) => {
  return (
    <div>
      <button type="button" onClick={() => props.unloadAndRemoveIdentity()}>
        Logout
      </button>
      <br />
      <Uploader />
      <br />
      <UploadList />
    </div>
  )
}

export default Auth(Home)

The Home component is wrapped in a higher order Auth component, which blocks access to Home until a user registers. Auth also injects a unloadAndRemoveIdentity function to Home we use for a logout button.

Creating the Auth Component

The Auth component ensures that a user registers before using the app. Its a higher order component that wraps Home and displays the Register component when a user isn't registered.

Create it at src/component/auth with the following code:

import { useEffect } from "preact/hooks"
import { useAuth, AuthStatus } from "@w3ui/react-keyring"
import Register from "./register"

export default function (Wrappee) {
  return function AuthWrapper(props) {
    const { authStatus, loadDefaultIdentity, unloadAndRemoveIdentity } =
      useAuth()

    useEffect(() => {
      loadDefaultIdentity()
    }, [])

    if (authStatus === AuthStatus.SignedIn) {
      const authProps = { ...props, unloadAndRemoveIdentity }
      return <Wrappee {...authProps} />
    }

    return <Register />
  }
}

The loadDefaultIdentity is called to check if the current browser is already registered.

Creating the Register Component

The register component adds HTML elements around the w3ui hooks to make them usable.

Create it at src/component/register.js and fill it with this:

import { useReducer } from "preact/hooks"
import { useAuth, AuthStatus } from "@w3ui/react-keyring"

export default function Register() {
  const { authStatus, identity, registerAndStoreIdentity } = useAuth()
  const [state, setState] = useReducer(
    (state, update) => ({ ...state, ...update }),
    { email: "", loading: false }
  )

  const waitingForVerification = authStatus === AuthStatus.EmailVerification

  return (
    <>
      <h1>QuickSave</h1>
      <h2>Distribute your files via IPFS!</h2>

      <form
        onSubmit={(e) => {
          e.preventDefault()
          setState({ loading: true })
          registerAndStoreIdentity(state.email)
        }}
      >
        <div>
          <label>Email Address: </label>
          <input
            type="email"
            disabled={state.loading}
            value={state.email}
            onChange={(e) => setState({ email: e.target.value })}
          />
          <button type="submit" disabled={state.loading}>
            Register
          </button>
          {waitingForVerification && (
            <p>Waiting for verification of "{identity.email}" address...</p>
          )}
        </div>
      </form>
    </>
  )
}

Here is where the actual registration happens. We use a reducer to keep track of the state, in this case, an email variable for the registration and a loading variable to lock up the UI after we submit the email.

Creating the Uploader Component

Now that we have taken care of the registration, we need to implement the app's essential features.

Let's start with the Uploader component that lets users ... well ... upload images.

Create the file at src/components/uploader.js and add this code:

import { useReducer } from "preact/hooks"
import { useUploader } from "@w3ui/react-uploader"

export default function Uploader() {
  const [, uploader] = useUploader()
  const [state, setState] = useReducer(
    (state, update) => ({ ...state, ...update }),
    { file: null, loading: false }
  )

  const handleUploadSubmit = async (e) => {
    e.preventDefault()
    setState({ loading: true })
    await uploader.uploadFile(state.file)
    setState({ loading: false })
  }

  return (
    <form onSubmit={handleUploadSubmit}>
      <input
        type="file"
        accept="image/*"
        onChange={(e) => setState({ file: e.target.files[0] })}
        required
        disabled={state.loading}
      />
      <br />
      <button type="submit" disabled={state.loading}>
        Upload
      </button>
      <br />
      {state.loading && <p>Uploading file...</p>}
    </form>
  )
}

Again, we wrap a simple UI around the w3ui hooks. The <input> element is used to choose a file and filters the valid files for images.

Creating the UploadList Component

Now that our users can upload images, it would be nice if they could share them. For that, we have to use the w3up API for the CIDs of the images already uploaded and display them in a usable way.

Let's create a new file at src/components/uploadsList and add this code to it:

import { useEffect } from "preact/hooks"
import { useUploadsList } from "@w3ui/react-uploads-list"

export default function UploadsList() {
  const { data, error, reload } = useUploadsList()

  useEffect(() => {
    const intervalId = setInterval(() => reload(), 5000)
    return () => clearInterval(intervalId)
  }, [])

  if (error) return <p>{error.message}</p>

  console.log(data)

  return (
    <div>
      {data?.results?.length &&
        data.results.map(({ dataCid, carCids, uploadedAt }) => (
          <div>
            <hr />
            <img
              src={`https://w3s.link/ipfs/${dataCid}`}
              alt={`Image CID: ${dataCid}`}
            />
            <p>
              Uploaded At
              <br />
              {uploadedAt.toString()}
            </p>
            <p>
              Data CID
              <br />
              <code>{dataCid}</code>
            </p>
            <p>
              Image URL
              <br />
              <a href={`https://w3s.link/ipfs/${dataCid}`}>
                <code>https://w3s.link/ipfs/{dataCid}</code>
              </a>
            </p>
          </div>
        ))}
    </div>
  )
}

The list gets updated periodically every five seconds.

The exciting part is the dataCid's we get from the useUploadsList hook. It points directly to the image on IPFS. With an IPFS gateway, we can create a link we can put into an <img> tag and let the user share the file.

Testing the App

To test the app, we can use the Preact-CLI. The following command with run the system in dev mode:

$ npm run dev

The registration process requires you to enter an email. You have to verify it before you can start uploading.

Bonus: Deploying the App

Now that everything is up and running locally, it would be nice to deploy it somewhere.

We could build the project and upload it to GitHub Pages, but wouldn't it be cooler to go for something more decentralized?

The w3up tool suite has a CLI that lets you do just that. Give it a directory and get a CID that you can use to share with your peers.

Let's start by building the code:

$ npm run build

This will create a build directory, which we will tell w3up about, but first, we need to install it.

$ npm i -g @web3-storage/w3up-cli

We need to register here too:

$ w3up register

Now, we're ready to go!

$ w3up upload ./build

This command will respond with two CIDs and a gateway URL if everything goes correctly. We can use this URL to access the app online.

D_D Newsletter CTA

Summary

Building your own DApp with file hosting features has become easy with w3up and IPFS.

The w3up API is currently in beta, so uploads are free, but if you check out the parent project web3.storage, you see that they offer a free tier with a few gigabytes, and the other plans are pretty affordable too.

The best thing is that this isn't your subscription; your users register and eventually pay for it if they hit the free tier limits.

Plus, your app deployment and all the uploaded images are accessible via IPFS, which means anyone who thinks they shouldn't be deleted can pin the CIDs on their IPFS pinning service of choice or their own node and keep them available.