Building a Censorship Resistant Image Uploader
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!
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.
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.