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.
90 lines
2.4 KiB
TypeScript
90 lines
2.4 KiB
TypeScript
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)
|
|
})
|