# Back up photos in a peer-to-peer app





This guide shows you how to **back up photos peer-to-peer** by adapting the [`hello-pear-electron`](https://github.com/holepunchto/hello-pear-electron) scaffold to decode local images with [`bare-ffmpeg`](https://www.npmjs.com/package/bare-ffmpeg)/[`bare-media`](https://www.npmjs.com/package/bare-media) and push them into a [Hyperblobs](https://www.npmjs.com/package/hyperblobs) store. The reference implementation is [`pear-photo-backup`](https://github.com/holepunchto/pear-docs/tree/preview/examples/how-to/stream-and-share-media/photo-backup).

<Callout type="info">
  **Start from the desktop boilerplate.** This guide has a frontend, so it builds on [`hello-pear-electron`](/p2p/getting-started/from-a-template/start-from-hello-pear-electron)—the renderer, preload bridge, and Bare [worker](/pear/explanation/workers) it extends. Read [Start from the hello-pear-electron template](/p2p/getting-started/from-a-template/start-from-hello-pear-electron) first. The Pear-end logic itself is portable—only the UI is desktop-specific (see [Runtime and languages](/pear/explanation/runtime-and-languages)); the terminal template [`hello-pear-bare`](/p2p/getting-started/from-a-template/start-from-hello-pear-bare) has no UI layer. See all starting points in [Start from a template](/p2p/getting-started/from-a-template).
</Callout>

* [Stream a live camera in a peer-to-peer app](/p2p/how-to/stream-and-share-media/stream-a-live-camera-in-a-peer-to-peer-app)—the sibling how-to that establishes the same Hyperblobs + blob-server pattern for live frames.

## Before you begin [#before-you-begin]

* A working clone of `hello-pear-electron` (or your own app built from the getting-started path).
* Comfort with [Hyperblobs](https://www.npmjs.com/package/hyperblobs) and the [Bare](/pear/explanation/runtime-and-languages) native module set.

## What changes [#what-changes]

| Layer            | Change                                                                                                                                                                                                                                                                                                                                                                                                              |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Dependencies     | Add [`bare-ffmpeg`](https://www.npmjs.com/package/bare-ffmpeg), [`bare-media`](https://www.npmjs.com/package/bare-media), [`get-mime-type`](https://www.npmjs.com/package/get-mime-type), [`hyperblobs`](https://www.npmjs.com/package/hyperblobs), [`hypercore-blob-server`](https://www.npmjs.com/package/hypercore-blob-server), [`hypercore-id-encoding`](https://www.npmjs.com/package/hypercore-id-encoding). |
| Worker           | Store the full file as one Hyperblob and generate a small inline **preview** (a `data:` URL)—`bare-media` for images, `bare-ffmpeg` for video—recorded alongside the blob id in the view.                                                                                                                                                                                                                           |
| Worker transport | Use a JSON-over-pipe control surface: the renderer sends `{ type: 'add-video', path }` and the worker emits `{ type: 'videos', videos }` events whose entries carry a blob-server link plus the inline preview.                                                                                                                                                                                                     |
| Renderer         | Show a grid that renders each entry's inline preview and opens the full blob via its link.                                                                                                                                                                                                                                                                                                                          |

## Steps [#steps]

<Steps>
  <Step>
    ### Add the dependencies [#add-the-dependencies]

    ```bash skip="desktop-gui"
    npm install bare-ffmpeg bare-media get-mime-type hyperblobs hypercore-blob-server hypercore-id-encoding
    ```

    `bare-ffmpeg` and `bare-media` are **Bare** native modules—they ship prebuilt binaries via [`bare-sidecar`](https://www.npmjs.com/package/bare-sidecar) and only work inside the Bare worker, never in Electron's main or renderer.
  </Step>

  <Step>
    ### Store the file and generate a preview inside the worker [#store-the-file-and-generate-a-preview-inside-the-worker]

    `workers/video-room.js` (`VideoRoom`, shared with the video-stream how-to but extended for images) defines `addVideo` (L181), which checks the MIME type with [`get-mime-type`](https://www.npmjs.com/package/get-mime-type) and rejects anything that is not an image or video (L183–L186). It then streams the full file bytes into a Hyperblobs write stream (L188–L194) and captures the resulting blob id (L195). Only then does it generate a small inline preview—`bare-media` for images, `bare-ffmpeg` for video (L197)—and append the record to the base (L200–L202):

    ```js file=<rootDir>/examples/how-to/stream-and-share-media/photo-backup/workers/video-room.js#L181-L203 title="workers/video-room.js" lineNumbers {1,3-6,8-14,15,17,20-22} skip="example-import"
      async addVideo (filePath, info) {
        const name = path.basename(filePath)
        const type = getMimeType(name)
        if (!(type.startsWith('image/') || type.startsWith('video/'))) {
          throw new Error('Only image/video files are allowed')
        }

        const rs = fs.createReadStream(filePath)
        const ws = this.blobs.createWriteStream()
        await new Promise((resolve, reject) => {
          ws.on('error', reject)
          ws.on('close', resolve)
          rs.pipe(ws)
        })
        const blob = { key: idEnc.normalize(this.blobs.core.key), ...ws.id }

        const preview = type.startsWith('image/') ? await createPreviewImage(filePath) : await createPreviewVideo(filePath)

        const id = Math.random().toString(16).slice(2)
        await this.base.append(
          VideoDispatch.encode('@pear-photo-backup/add-video', { id, name, type, blob, info: { ...info, preview } })
        )
      }
    ```

    The preview is a base64 `data:` URL kept inline in the record's `info`, so the grid can paint immediately without fetching the full blob. Images are resized with `bare-media`: `createPreviewImage` decodes the file, resizes it to a 256×256 bound, and re-encodes it as WebP (L6–L9), then returns the bytes as a base64 `data:` URL (L10):

    ```js file=<rootDir>/examples/how-to/stream-and-share-media/photo-backup/workers/create-preview-image.js title="workers/create-preview-image.js" lineNumbers {6-9,10} skip="example-import"
    const { image } = require('bare-media')

    const MIMETYPE = 'image/webp'

    async function createPreviewImage (filePath) {
      const buffer = await image(filePath)
        .decode()
        .resize({ maxWidth: 256, maxHeight: 256 })
        .encode({ mimetype: MIMETYPE })
      return `data:${MIMETYPE};base64,${buffer.toString('base64')}`
    }

    module.exports = createPreviewImage
    ```

    `workers/create-preview-video.js` is the `bare-ffmpeg` counterpart for video files (a stub in the reference app—wire up `bare-ffmpeg` frame extraction here to generate a video thumbnail). Both preview helpers are worker-side modules (they call `bare-media`/`bare-ffmpeg`), so they live alongside the worker in `workers/`, not in the renderer. Each record stored in the view is `{ id, name, type, blob, info: { preview, ... } }`—small metadata plus a blob id pointing at the full bytes.
  </Step>

  <Step>
    ### Reuse the standard close order [#reuse-the-standard-close-order]

    Photo backup does not add an interval or any other timer, so `WorkerTask._close` keeps the same chain as hello-pear-electron: room → swarm → store. The room itself closes its blob server and blobs core first (`VideoRoom._close`). The [`graceful-goodbye`](https://www.npmjs.com/package/graceful-goodbye) handler in `workers/index.js` fires it.
  </Step>

  <Step>
    ### Render a photo grid [#render-a-photo-grid]

    In the renderer, render `<img src={entry.info.preview} loading="lazy">` straight from the inline preview, and open the full-size image (or video) via `entry.info.link`—the blob-server URL the worker attaches in `getVideos`. Use the renderer's drag-and-drop to call `getPathForFile(file)` on each dropped file (exposed by the preload bridge) and write `{ type: 'add-video', path }` over the worker pipe, which the worker forwards to its `addVideo` handler.
  </Step>
</Steps>

## Run it [#run-it]

```bash skip="desktop-gui"
npm run build

# host
npm start -- --storage /tmp/photos-host --name host
```

Drag and drop photos into the window. The thumbnails appear in the grid immediately. Quit the host, restart it with the same `--storage` path, and the grid replays from disk.

```bash skip="desktop-gui"
# friend backing up the same album
npm start -- --storage /tmp/photos-friend --name friend --invite <invite>
```

The friend's app replicates the room's Hyperblobs and shows the same grid. Originals are downloaded lazily—only the thumbnails are pulled eagerly.

## Where to go next [#where-to-go-next]

* [Store and serve large media with Hyperblobs](/p2p/how-to/stream-and-share-media/store-and-serve-large-media-with-hyperblobs)—the Pear/Bare blob primitive behind this app, with no UI.
* [Stream a live camera in a peer-to-peer app](/p2p/how-to/stream-and-share-media/stream-a-live-camera-in-a-peer-to-peer-app)—the same Hyperblobs + blob-server pattern for live frames.
* [Stream stored video in a peer-to-peer app](/p2p/how-to/stream-and-share-media/stream-stored-video-in-a-peer-to-peer-app)—the same blob mechanics for stored video.
* [Bare modules](/bare/reference/bare/modules)—what `bare-ffmpeg` and `bare-media` ship and why they live in the worker.
