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/README.md +6 -3
- package/client/css/app.css +304 -3
- package/client/index.html +3 -0
- package/client/js/app.js +500 -225
- package/package.json +1 -1
- package/server/cleanup.js +3 -4
- package/server/config.js +1 -0
- package/server/db.js +4 -0
- package/server/server.js +108 -3
package/package.json
CHANGED
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
|
-
|
|
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
|
-
|
|
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
|
|