Back up photos in a peer-to-peer app
Decode local photos with bare-ffmpeg/bare-media and push them into a Hyperblobs store on top of the hello-pear-electron scaffold.
This guide shows you how to back up photos peer-to-peer by adapting the hello-pear-electron scaffold to decode local images with bare-ffmpeg/bare-media and push them into a Hyperblobs store. The reference implementation is pear-photo-backup.
This guide is about the Pear-end, not the shell. The code below lives in the Bare worker — the peer-to-peer logic, not the user interface. Because the Pear-end never imports DOM APIs and never assumes a UI framework, the same worker is portable across desktop (Electron), mobile (React Native via Bare iOS / Bare Android), and terminal. The example apps ship an Electron shell, but only the UI half changes per platform — the logic here stays the same. See Runtime and languages for the cross-platform model and current support.
This is a delta-only how-to. The shared scaffold is explained in the Start from the hello-pear-electron template tutorial — read it first.
- 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
- A working clone of
hello-pear-electron(or your own app built from the getting-started path). - Comfort with Hyperblobs and the Bare native module set.
What changes
| Layer | Change |
|---|---|
| Dependencies | Add bare-ffmpeg, bare-media, get-mime-type, hyperblobs, hypercore-blob-server, 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
Add the dependencies
npm install bare-ffmpeg bare-media get-mime-type hyperblobs hypercore-blob-server hypercore-id-encodingbare-ffmpeg and bare-media are Bare native modules — they ship prebuilt binaries via bare-sidecar and only work inside the Bare worker, never in Electron's main or renderer.
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 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):
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):
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 = createPreviewImageworkers/create-preview-video.js is the bare-ffmpeg counterpart for video files. 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.
Reuse the standard close order
Photo backup does not add an interval or any other timer, so Worker._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 handler in workers/index.js fires it.
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.
Run it
npm run build
# host
npm start -- --storage /tmp/photos-host --name hostDrag 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.
# 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
- 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 — the same Hyperblobs + blob-server pattern for live frames.
- Stream stored video in a peer-to-peer app — the same blob mechanics for stored video.
- Bare modules — what
bare-ffmpegandbare-mediaship and why they live in the worker.