Building a Censorship Resistant Image Uploader

K - Nov 3 '22 - - Dev Community

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

Building applications that are censorship resistant but are still easy to use isn't accessible.

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, the good people from Protocol Labs understood that issue and built web3.storage, a service that works as a layer between your Web2 skills and the shiny new decentralized world.

With Fullstack Frontend, 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, and 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!

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 installed 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
Enter fullscreen mode Exit fullscreen mode

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; it's mainly a few providers and hooks, but still an excellent 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
Enter fullscreen mode Exit fullscreen mode
  • The @w3ui/react-keyring package links every agent (browser) to an account (email address).
  • The @w3ui/react-uploader package takes care of the actual 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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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 />
  }
}
Enter fullscreen mode Exit fullscreen mode

The loadDefaultIdentity is called to check if a user has registered.

Creating the Register Component

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

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"
            class="btn btn-primary"
            disabled={state.loading}
          >
            Register
          </button>
          {waitingForVerification && (
            <p>Waiting for verification of "{identity.email}" address...</p>
          )}
        </div>
      </form>
    </>
  )
}

Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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 class="card-text">
              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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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. Point it to 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 
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

We need to register here too:

$ w3up register <YOUR_EMAIL>
Enter fullscreen mode Exit fullscreen mode

Now, we're ready to go!

$ w3up upload ./build
Enter fullscreen mode Exit fullscreen mode

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.

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 cheap, also.

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.

While this isn't 100% decentralized because we're relying on w3up for central app features, it at least decentralized the burden of hosting and keeping files and apps available to the IPFS network.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .