Initial scaffold: blob store server, CLI scripts, and PWA share target
Clipboard/screenshot sync across own devices via a single DEVICE_TOKEN-gated blob store, plus a small installable web app so a phone can push/pull clips and share screenshots via the OS share sheet.
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
import { Router } from 'express'
|
||||
import fs from 'node:fs'
|
||||
import { config } from './config.js'
|
||||
import { db, blobPath } from './db.js'
|
||||
import { newBlobId } from './ids.js'
|
||||
import { checkDeviceToken } from './auth.js'
|
||||
import { insertBlobRecord } from './blobs.js'
|
||||
|
||||
export const router = Router()
|
||||
|
||||
interface BlobRow {
|
||||
id: string
|
||||
kind: 'clip' | 'shot'
|
||||
content_type: string
|
||||
size_bytes: number
|
||||
created_at: number
|
||||
expires_at: number
|
||||
}
|
||||
|
||||
router.post('/blobs', (req, res) => {
|
||||
if (!checkDeviceToken(req)) {
|
||||
res.status(401).json({ error: 'bad device token' })
|
||||
return
|
||||
}
|
||||
|
||||
const kind = req.get('X-Blob-Kind')
|
||||
if (kind !== 'clip' && kind !== 'shot') {
|
||||
res.status(400).json({ error: 'X-Blob-Kind must be "clip" or "shot"' })
|
||||
return
|
||||
}
|
||||
|
||||
const contentLength = Number(req.get('Content-Length') ?? 0)
|
||||
if (contentLength > config.maxUploadBytes) {
|
||||
res.status(413).json({ error: 'blob too large' })
|
||||
return
|
||||
}
|
||||
|
||||
const id = newBlobId()
|
||||
const dest = blobPath(id)
|
||||
const writeStream = fs.createWriteStream(dest, { flags: 'wx' })
|
||||
|
||||
let bytesReceived = 0
|
||||
req.on('data', (chunk: Buffer) => {
|
||||
bytesReceived += chunk.length
|
||||
if (bytesReceived > config.maxUploadBytes) {
|
||||
req.destroy()
|
||||
writeStream.destroy()
|
||||
fs.rm(dest, { force: true }, () => {})
|
||||
}
|
||||
})
|
||||
|
||||
req.pipe(writeStream)
|
||||
|
||||
writeStream.on('finish', () => {
|
||||
insertBlobRecord(id, kind, req.get('Content-Type') ?? 'application/octet-stream', bytesReceived)
|
||||
res.json({ id, url: kind === 'shot' ? `/s/${id}` : null })
|
||||
})
|
||||
|
||||
writeStream.on('error', () => {
|
||||
fs.rm(dest, { force: true }, () => {})
|
||||
if (!res.headersSent) res.status(500).json({ error: 'write failed' })
|
||||
})
|
||||
})
|
||||
|
||||
// Latest non-expired clip, for the pull side of clipboard sync — no
|
||||
// history, no listing, just "what's the newest thing I copied elsewhere."
|
||||
router.get('/clip/latest', (req, res) => {
|
||||
if (!checkDeviceToken(req)) {
|
||||
res.status(401).json({ error: 'bad device token' })
|
||||
return
|
||||
}
|
||||
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const row = db
|
||||
.prepare(
|
||||
`SELECT * FROM blobs WHERE kind = 'clip' AND expires_at > ? ORDER BY created_at DESC LIMIT 1`,
|
||||
)
|
||||
.get(now) as BlobRow | undefined
|
||||
|
||||
if (!row) {
|
||||
res.status(204).end()
|
||||
return
|
||||
}
|
||||
|
||||
res.set('Content-Type', row.content_type)
|
||||
res.set('X-Blob-Id', row.id)
|
||||
res.set('X-Blob-Created-At', String(row.created_at))
|
||||
fs.createReadStream(blobPath(row.id)).pipe(res)
|
||||
})
|
||||
Reference in New Issue
Block a user