instbyte 1.8.0 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instbyte",
3
- "version": "1.8.0",
3
+ "version": "1.9.1",
4
4
  "description": "A self-hosted LAN sharing utility for fast, frictionless file, link, and snippet exchange across devices — no cloud required.",
5
5
  "main": "server/server.js",
6
6
  "bin": {
package/server/cleanup.js CHANGED
@@ -3,13 +3,12 @@ const fs = require("fs");
3
3
  const path = require("path");
4
4
  const config = require("./config");
5
5
 
6
- const DAY = config.storage.retention;
7
-
8
6
  setInterval(() => {
9
- const cutoff = Date.now() - DAY;
7
+ if (config.storage.retention === null) return;
8
+ const cutoff = Date.now() - config.storage.retention;
10
9
 
11
10
  db.all(
12
- `SELECT * FROM items WHERE created_at < ?`,
11
+ `SELECT * FROM items WHERE created_at < ? AND pinned = 0`,
13
12
  [cutoff],
14
13
  (err, rows) => {
15
14
  rows.forEach((item) => {
package/server/config.js CHANGED
@@ -30,6 +30,7 @@ function parseFileSize(val) {
30
30
  }
31
31
 
32
32
  function parseRetention(val) {
33
+ if (String(val).toLowerCase() === "never") return null;
33
34
  if (typeof val === "number") return val;
34
35
  const units = { h: 3600000, d: 86400000 };
35
36
  const match = String(val).match(/^(\d+)(h|d)$/i);
package/server/db.js CHANGED
@@ -6,6 +6,7 @@ const dbPath = process.env.INSTBYTE_DATA
6
6
  : path.join(__dirname, "../db.sqlite");
7
7
 
8
8
  const db = new sqlite3.Database(dbPath);
9
+ db.configure("busyTimeout", 5000);
9
10
 
10
11
  db.serialize(() => {
11
12
 
@@ -36,6 +37,9 @@ db.serialize(() => {
36
37
  db.run(`ALTER TABLE items ADD COLUMN size INTEGER DEFAULT 0`, () => { });
37
38
  db.run(`ALTER TABLE channels ADD COLUMN pinned INTEGER DEFAULT 0`, () => { });
38
39
 
40
+ db.run(`ALTER TABLE items ADD COLUMN title TEXT DEFAULT ''`, () => { });
41
+ db.run(`ALTER TABLE items ADD COLUMN edited_at INTEGER DEFAULT NULL`, () => { });
42
+
39
43
  // Insert default channels if empty
40
44
  db.get("SELECT COUNT(*) as count FROM channels", (err, row) => {
41
45
  if (err) {
package/server/server.js CHANGED
@@ -21,6 +21,29 @@ const config = require("./config");
21
21
  const UPLOADS_DIR = process.env.INSTBYTE_UPLOADS
22
22
  || path.join(__dirname, "../uploads");
23
23
 
24
+ /* STARTUP ORPHAN SCAN
25
+ Deletes any files in uploads dir that have no matching DB record.
26
+ Catches ghost files left by aborted uploads before fix in v1.9.1 */
27
+ function scanOrphans() {
28
+ fs.readdir(UPLOADS_DIR, (err, files) => {
29
+ if (err || !files || !files.length) return;
30
+
31
+ db.all("SELECT filename FROM items WHERE filename IS NOT NULL", (err, rows) => {
32
+ if (err) return;
33
+
34
+ const known = new Set(rows.map(r => r.filename));
35
+ files.forEach(file => {
36
+ if (!known.has(file)) {
37
+ const orphan = path.join(UPLOADS_DIR, file);
38
+ fs.unlink(orphan, err => {
39
+ if (!err) console.log("Orphan removed:", file);
40
+ });
41
+ }
42
+ });
43
+ });
44
+ });
45
+ }
46
+
24
47
  const CLIENT_DIR = path.join(__dirname, "../client");
25
48
 
26
49
  const app = express();
@@ -276,12 +299,35 @@ app.post("/logout", (req, res) => {
276
299
  app.post("/upload", (req, res) => {
277
300
  upload.single("file")(req, res, (err) => {
278
301
  if (err && err.code === "LIMIT_FILE_SIZE") {
279
- return res.status(413).json({ error: "File exceeds 2GB limit" });
302
+ // Clean up in case Multer may have written a partial file before hitting limit
303
+ if (req.file) {
304
+ const partial = path.join(UPLOADS_DIR, req.file.filename);
305
+ if (fs.existsSync(partial)) fs.unlinkSync(partial);
306
+ }
307
+ return res.status(413).json({ error: "File exceeds limit" });
280
308
  }
281
309
  if (err) {
310
+ if (req.file) {
311
+ const partial = path.join(UPLOADS_DIR, req.file.filename);
312
+ if (fs.existsSync(partial)) fs.unlinkSync(partial);
313
+ }
282
314
  return res.status(500).json({ error: "Upload failed" });
283
315
  }
284
316
 
317
+ // req.file missing means the request was aborted before Multer
318
+ // finished. No register or clean up required
319
+ if (!req.file) {
320
+ return res.status(400).json({ error: "No file received" });
321
+ }
322
+
323
+ // Detect client disconnect that happened after Multer finished writing
324
+ // but before we could respond. Clean up the orphaned file.
325
+ if (req.destroyed || res.destroyed) {
326
+ const partial = path.join(UPLOADS_DIR, req.file.filename);
327
+ if (fs.existsSync(partial)) fs.unlinkSync(partial);
328
+ return;
329
+ }
330
+
285
331
  const { channel, uploader } = req.body;
286
332
 
287
333
  const item = {
@@ -297,7 +343,13 @@ app.post("/upload", (req, res) => {
297
343
  `INSERT INTO items (type, filename, size, channel, uploader, created_at)
298
344
  VALUES (?, ?, ?, ?, ?, ?)`,
299
345
  ["file", item.filename, item.size, channel, uploader, item.created_at],
300
- function () {
346
+ function (dbErr) {
347
+ if (dbErr) {
348
+ // DB insert failed — don't leave the file on disk orphaned
349
+ const orphan = path.join(UPLOADS_DIR, item.filename);
350
+ if (fs.existsSync(orphan)) fs.unlinkSync(orphan);
351
+ return res.status(500).json({ error: "Failed to save item" });
352
+ }
301
353
  item.id = this.lastID;
302
354
  io.emit("new-item", item);
303
355
  res.json(item);
@@ -525,6 +577,45 @@ app.patch("/item/:id/move", (req, res) => {
525
577
  });
526
578
  });
527
579
 
580
+ /* UPDATE ITEM TITLE */
581
+ app.patch("/item/:id/title", (req, res) => {
582
+ const { id } = req.params;
583
+ const { title } = req.body;
584
+
585
+ if (title === undefined) return res.status(400).json({ error: "Title required" });
586
+
587
+ db.run(
588
+ "UPDATE items SET title=? WHERE id=?",
589
+ [title.trim(), id],
590
+ function (err) {
591
+ if (err) return res.status(500).json({ error: "Update failed" });
592
+ if (this.changes === 0) return res.status(404).json({ error: "Item not found" });
593
+ io.emit("item-updated", { id: parseInt(id), title: title.trim() });
594
+ res.json({ id, title: title.trim() });
595
+ }
596
+ );
597
+ });
598
+
599
+ /* UPDATE ITEM CONTENT */
600
+ app.patch("/item/:id/content", (req, res) => {
601
+ const { id } = req.params;
602
+ const { content } = req.body;
603
+
604
+ if (content === undefined) return res.status(400).json({ error: "Content required" });
605
+ if (content.trim() === "") return res.status(400).json({ error: "Content cannot be empty" });
606
+
607
+ db.run(
608
+ "UPDATE items SET content=?, edited_at=? WHERE id=? AND type='text'",
609
+ [content.trim(), Date.now(), id],
610
+ function (err) {
611
+ if (err) return res.status(500).json({ error: "Update failed" });
612
+ if (this.changes === 0) return res.status(404).json({ error: "Item not found or not editable" });
613
+ io.emit("item-updated", { id: parseInt(id), content: content.trim(), edited_at: Date.now() });
614
+ res.json({ id, content: content.trim() });
615
+ }
616
+ );
617
+ });
618
+
528
619
  /* RENAME CHANNEL */
529
620
  app.patch("/channels/:name", (req, res) => {
530
621
  const oldName = req.params.name;
@@ -573,7 +664,8 @@ app.post("/channels/:name/pin", (req, res) => {
573
664
  app.get("/info", (req, res) => {
574
665
  res.json({
575
666
  url: `http://${localIP}:${PORT}`,
576
- hasAuth: !!config.auth.passphrase
667
+ hasAuth: !!config.auth.passphrase,
668
+ retention: config.storage.retention // null means "never"
577
669
  });
578
670
  });
579
671
 
@@ -650,6 +742,9 @@ app.get("/logo-dynamic.png", (req, res) => {
650
742
  /* ============================
651
743
  SOCKET CONNECTION LOGGING
652
744
  ============================ */
745
+ // in-memory seen tracking — item id → Set of socket ids
746
+ // resets on server restart, no DB needed
747
+ const seenBy = new Map();
653
748
 
654
749
  let connectedUsers = 0;
655
750
 
@@ -664,6 +759,15 @@ io.on("connection", (socket) => {
664
759
  console.log(username + " connected | total:", connectedUsers);
665
760
  });
666
761
 
762
+ socket.on("seen", ({ id, name }) => {
763
+ if (!id || !name) return;
764
+ if (!seenBy.has(id)) seenBy.set(id, new Set());
765
+ seenBy.get(id).add(name); // name instead of socket.id
766
+ const count = seenBy.get(id).size;
767
+ console.log(`seen: item ${id} | count: ${count}`);
768
+ io.emit("seen-update", { id, count });
769
+ });
770
+
667
771
  socket.on("disconnect", () => {
668
772
  connectedUsers--;
669
773
  console.log(username + " disconnected | total:", connectedUsers);
@@ -727,6 +831,7 @@ findFreePort(PREFERRED).then(p => {
727
831
  console.log(`(port ${PREFERRED} was busy, switched to ${PORT})`);
728
832
  }
729
833
  console.log("");
834
+ scanOrphans(); // clean up any pre-v1.9.1 ghost files
730
835
  });
731
836
  });
732
837