Improved drawing sync: MIME detection, WebP/JPEG support, sync log + frontend updates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-06-21 13:15:25 +02:00
1 parent a8779d7be1
commit d97591b242
7 files changed
+544 -144

No files matched your search

+47 -5
View File
@@ -136,6 +136,18 @@ function decrypt(data) {
return decrypted;
}
function detectImageMime(buffer, filePath = "") {
if (buffer && buffer.length >= 12) {
if (buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47) return "image/png";
if (buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) return "image/jpeg";
if (buffer.toString("ascii", 0, 4) === "RIFF" && buffer.toString("ascii", 8, 12) === "WEBP") return "image/webp";
}
const ext = path.extname(filePath).toLowerCase();
if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
if (ext === ".webp") return "image/webp";
return "image/png";
}
/* ─────────── Vector clock conflict resolution ─────────── */
function mergeVectorClocks(clockA, clockB) {
const merged = { ...clockA };
@@ -313,6 +325,20 @@ app.get("/api/notes/:id", authMiddleware, (req, res) => {
}
}
delete note.content_encrypted;
// Read drawing data from file if exists
if (note.drawing_data_path && fs.existsSync(note.drawing_data_path)) {
try {
const raw = fs.readFileSync(note.drawing_data_path);
const mime = detectImageMime(raw, note.drawing_data_path);
note.drawingData = `data:${mime};base64,` + raw.toString("base64");
} catch {
note.drawingData = null;
}
} else {
note.drawingData = null;
}
res.json(note);
});
@@ -404,12 +430,28 @@ app.put("/api/notes/:id/drawing", authMiddleware, (req, res) => {
const note = db.prepare("SELECT id FROM notes WHERE id = ? AND user_id = ?").get(req.params.id, req.userId);
if (!note) return res.status(404).json({ error: "Not found" });
const filePath = path.join(UPLOAD_DIR, `drawing_${req.params.id}.png`);
const base64Data = drawingData.replace(/^data:image\/\w+;base64,/, "");
if (!drawingData || typeof drawingData !== "string") return res.status(400).json({ error: "drawingData required" });
const m = drawingData.match(/^data:(image\/(?:png|jpeg|jpg|webp));base64,(.+)$/);
if (!m) return res.status(400).json({ error: "Invalid drawing format" });
const mime = m[1];
const ext = mime === "image/png" ? "png" : mime === "image/webp" ? "webp" : "jpg";
const base64Data = m[2];
const filePath = path.join(UPLOAD_DIR, `drawing_${req.params.id}.${ext}`);
// Remove older file variants from previous format writes.
for (const oldExt of ["png", "jpg", "jpeg", "webp"]) {
const oldPath = path.join(UPLOAD_DIR, `drawing_${req.params.id}.${oldExt}`);
if (oldPath !== filePath && fs.existsSync(oldPath)) {
try { fs.unlinkSync(oldPath); } catch {}
}
}
fs.writeFileSync(filePath, Buffer.from(base64Data, "base64"));
db.prepare("UPDATE notes SET drawing_data_path = ?, updated_at = datetime('now'), version = version + 1 WHERE id = ?")
.run(filePath, req.params.id);
db.prepare("UPDATE notes SET drawing_data_path = ?, updated_at = datetime('now'), version = version + 1, device_id = ? WHERE id = ?")
.run(filePath, req.deviceId, req.params.id);
db.prepare("INSERT INTO sync_log (user_id, entity_type, entity_id, action, payload, device_id) VALUES (?, 'note', ?, 'update', ?, ?)")
.run(req.userId, req.params.id, JSON.stringify({ drawingUpdated: true }), req.deviceId);
broadcastToUser(req.userId, { type: "note_updated", data: { id: req.params.id, drawingUpdated: true } }, req.deviceId);
res.json({ success: true });
});
@@ -537,4 +579,4 @@ server.listen(PORT, () => {
console.log(` 📡 WebSocket available at ws://localhost:${PORT}/ws`);
console.log(` 🔒 JWT Secret: ${JWT_SECRET.slice(0, 12)}...`);
console.log(` 💾 Database: ${path.join(DATA_DIR, "inknote.db")}\n`);
});
});