Implement SQLite

This commit is contained in:
Reimar 2024-02-10 23:30:03 +01:00
parent ba3785df0c
commit 9fb560fc9a
Signed by: Reimar
GPG Key ID: 93549FA07F0AE268
10 changed files with 1085 additions and 43 deletions

BIN
database.sqlite3 Normal file

Binary file not shown.

103
index.js
View File

@ -6,11 +6,24 @@ import path from "path";
import childProcess from "child_process"; import childProcess from "child_process";
import levenshtein from "js-levenshtein"; import levenshtein from "js-levenshtein";
import cookieParser from "cookie-parser"; import cookieParser from "cookie-parser";
import { fileURLToPath } from 'url'; import { fileURLToPath } from "url";
import sqlite3 from "sqlite3";
const users = [];
let sessions = []; let sessions = [];
const videos = [];
const db = new sqlite3.Database("database.sqlite3");
function dbGet(query, ...parameters) {
return new Promise((resolve, reject) => db.get(query, parameters, (err, data) => err ? reject(err) : resolve(data)));
}
function dbAll(query, ...parameters) {
return new Promise((resolve, reject) => db.all(query, parameters, (err, data) => err ? reject(err) : resolve(data)));
}
function dbRun(query, ...parameters) {
return new Promise((resolve, reject) => db.run(query, parameters, (err, data) => err ? reject(err) : resolve(data)));
}
function randomString(length) { function randomString(length) {
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789"; const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789";
@ -36,38 +49,51 @@ app.use("/videos", express.static("videos/"));
app.post("/api/register", async (req, res) => { app.post("/api/register", async (req, res) => {
const { username, password } = req.body; const { username, password } = req.body;
if (typeof username !== "string" || typeof password !== "string") { if (typeof username !== "string" || typeof password !== "string") {
return res.status(400).json({ ok: false, error: "bad request" }); return res.status(400).json({ ok: false, error: "bad request" });
} }
const existingUser = users.find(user => user.username === username);
const existingUser = await dbGet("SELECT * FROM users WHERE username = ?", username);
if (existingUser !== undefined) { if (existingUser !== undefined) {
return res.status(400).json({ ok: false, error: "username taken" }); return res.status(400).json({ ok: false, error: "username taken" });
} }
const passwordHash = await bcrypt.hash(password, 10); const passwordHash = await bcrypt.hash(password, 10);
const id = users.length;
const user = { id, username, passwordHash }; await dbGet("INSERT INTO users (username, password) VALUES (?, ?)", username, passwordHash);
users.push(user);
return res.status(200).json({ ok: true, user }); return res.status(200).json({ ok: true });
}); });
app.post("/api/login", async (req, res) => { app.post("/api/login", async (req, res) => {
const { username, password } = req.body; const { username, password } = req.body;
if (typeof username !== "string" || typeof password !== "string") { if (typeof username !== "string" || typeof password !== "string") {
return res.status(400).json({ ok: false, error: "bad request" }); return res.status(400).json({ ok: false, error: "bad request" });
} }
const user = users.find(user => user.username === username);
const user = await dbGet("SELECT * FROM users WHERE username = ?", username);
if (user === undefined) { if (user === undefined) {
return res.status(400).json({ ok: false, error: "wrong username/password" }); return res.status(400).json({ ok: false, error: "wrong username/password" });
} }
if (!await bcrypt.compare(password, user.passwordHash)) {
if (!await bcrypt.compare(password, user.password)) {
return res.status(400).json({ ok: false, error: "wrong username/password" }); return res.status(400).json({ ok: false, error: "wrong username/password" });
} }
sessions = sessions.filter(session => session.userId !== user.id); sessions = sessions.filter(session => session.userId !== user.id);
const token = randomString(64); const token = randomString(64);
const session = { userId: user.id, token }; const session = { userId: user.id, token };
sessions.push(session); sessions.push(session);
res.clearCookie("token"); res.clearCookie("token");
res.cookie("token", token); res.cookie("token", token);
return res.status(200).json({ ok: true, session }); return res.status(200).json({ ok: true, session });
}); });
@ -75,27 +101,30 @@ app.post("/api/login", async (req, res) => {
app.get("/api/search", async (req, res) => { app.get("/api/search", async (req, res) => {
const page = +req.query.page || 0; const page = +req.query.page || 0;
const search = req.query.query; const search = req.query.query;
if (!search) { if (!search) {
return res.status(400).json({ ok: false, error: "bad request" }); return res.status(400).json({ ok: false, error: "bad request" });
} }
const [start, end] = [20 * page, 20 * (page + 1)];
const withDistance = videos const [start, end] = [20 * page, 20];
.map(video => ({ dist: levenshtein(search, video.title), ...video })); const videos = await dbAll(`
withDistance.sort((a, b) => a.dist - b.dist); SELECT videos.*, users.username AS author
const returnedVideos = withDistance FROM videos
.slice(start, end) JOIN users ON users.id = videos.user_id
.map(video => { WHERE title LIKE CONCAT('%', ?, '%')
const user = users.find(user => user.id === video.userId); LIMIT ?
if (!user) { OFFSET ?
return { ...video, author: "[Liberal]" }; `, search, end, start);
}
return { ...video, author: user.username }; console.log(videos);
});
return res.status(200).json({ ok: true, videos: returnedVideos, total: videos.length }); const { total } = await dbGet("SELECT COUNT(*) AS total FROM videos");
return res.status(200).json({ ok: true, videos, total });
}); });
function authorized() { function authorized() {
return (req, res, next) => { return async (req, res, next) => {
const token = (() => { const token = (() => {
if (req.cookies && "token" in req.cookies) { if (req.cookies && "token" in req.cookies) {
return req.cookies["token"]; return req.cookies["token"];
@ -114,7 +143,7 @@ function authorized() {
if (session === undefined) { if (session === undefined) {
return res.status(400).json({ ok: false, error: "unauthorized" }); return res.status(400).json({ ok: false, error: "unauthorized" });
} }
const user = users.find(user => user.id === session.userId); const user = await dbGet("SELECT * FROM users WHERE id = ?", session.userId);
if (user === undefined) { if (user === undefined) {
throw new Error("error: session with invalid userId"); throw new Error("error: session with invalid userId");
} }
@ -130,39 +159,39 @@ app.get("/api/logout", authorized(), (req, res) => {
app.post("/api/upload_video", authorized(), fileUpload({ limits: { fileSize: 2 ** 26 }, useTempFiles: true }), async (req, res) => { app.post("/api/upload_video", authorized(), fileUpload({ limits: { fileSize: 2 ** 26 }, useTempFiles: true }), async (req, res) => {
const { title } = req.body; const { title } = req.body;
if (!req.files || !req.files.video) { if (!req.files || !req.files.video) {
return res.status(400).json({ ok: false, error: "bad request" }); return res.status(400).json({ ok: false, error: "bad request" });
} }
if (req.files.video.mimetype !== "video/mp4") { if (req.files.video.mimetype !== "video/mp4") {
return res.status(400).json({ ok: false, error: "bad mimetype" }); return res.status(400).json({ ok: false, error: "bad mimetype" });
} }
const userId = req.user.id; const userId = req.user.id;
const id = randomString(4); const id = randomString(4);
const tempPath = req.files.video.tempFilePath; const tempPath = req.files.video.tempFilePath;
const newPath = path.join(dirname(), "videos", `${id}.mp4`); const newPath = path.join(dirname(), "videos", `${id}.mp4`);
console.log(newPath); console.log(newPath);
const exitCode = await new Promise((resolve, _reject) => { const exitCode = await new Promise((resolve, _reject) => {
const process = childProcess.spawn("HandBrakeCLI", ["-i", tempPath, "-o", newPath, "-Z", "Social 25 MB 5 Minutes 360p60"]); const process = childProcess.spawn("ffmpeg", ["-i", tempPath, "-b:v", "1M", "-b:a", "192k", newPath]);
process.stderr.on("data", (data) => { process.stderr.on("data", (data) => {
console.error(data.toString()); console.error(data.toString());
}); });
process.on("close", (code) => { process.on("close", (code) => {
resolve(code); resolve(code);
}) })
}) });
if (exitCode !== 0) { if (exitCode !== 0) {
throw new Error("handbrake failed"); throw new Error("ffmpeg failed");
} }
const video = { await dbRun("INSERT INTO videos (id, user_id, title, path) VALUES (?, ?, ?, ?)", id, userId, title, newPath);
id,
userId, return res.status(200).json({ ok: true });
title,
path: newPath,
};
videos.push(video);
return res.status(200).json({ ok: true, video });
}) })
app.use((err, req, res, next) => { app.use((err, req, res, next) => {

14
migration.sql Normal file
View File

@ -0,0 +1,14 @@
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
username TEXT NOT NULL,
password TEXT NOT NULL
);
CREATE TABLE videos (
id TEXT PRIMARY KEY NOT NULL,
user_id INTEGER,
title TEXT NOT NULL,
path TEXT NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(id)
);

998
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -16,6 +16,7 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.18.2", "express": "^4.18.2",
"express-fileupload": "^1.4.3", "express-fileupload": "^1.4.3",
"js-levenshtein": "^1.1.6" "js-levenshtein": "^1.1.6",
"sqlite3": "^5.1.7"
} }
} }

View File

@ -9,7 +9,7 @@
<h1>MaoTube</h1> <h1>MaoTube</h1>
<div> <div>
<label for="username"><p>Username</p></label> <label for="username"><p>Username</p></label>
<input type="text" name="username" id="username"> <input type="text" name="username" id="username" autofocus>
<label for="password"><p>Password</p></label> <label for="password"><p>Password</p></label>
<input type="password" name="password" id="password"> <input type="password" name="password" id="password">
<br> <br>

View File

@ -9,7 +9,7 @@
<h1>MaoTube</h1> <h1>MaoTube</h1>
<div> <div>
<label for="username"><p>Username</p></label> <label for="username"><p>Username</p></label>
<input type="text" name="username" id="username"> <input type="text" name="username" id="username" autofocus>
<label for="password"><p>Password</p></label> <label for="password"><p>Password</p></label>
<input type="password" name="password" id="password"> <input type="password" name="password" id="password">
<br> <br>

View File

@ -17,11 +17,13 @@ function displayResponse(response) {
error("search returned no results"); error("search returned no results");
return; return;
} }
console.log(videos);
const resultElement = document.getElementById("result"); const resultElement = document.getElementById("result");
resultElement.innerHTML = `<p>Showing ${videos.length}/${total} results.</p>` resultElement.innerHTML = `<p>Showing ${videos.length}/${total} results.</p>`
+ "<ul id='video-list'>" + "<ul id='video-list'>"
+ videos + videos
.map(v => { .map(v => {
console.log(v);
return `<li><p><a href="/watch?id=${v.id}">${v.title}</a> - ${v.author}</p></li>` return `<li><p><a href="/watch?id=${v.id}">${v.title}</a> - ${v.author}</p></li>`
}) })
.join("") .join("")

View File

@ -83,7 +83,7 @@ header {
margin: 0; margin: 0;
} }
a { header a {
color: #fcf4c8; color: #fcf4c8;
text-decoration: none; text-decoration: none;
} }

View File

@ -9,7 +9,7 @@
<h1>MaoTube</h1> <h1>MaoTube</h1>
<form action="/api/upload_video" method="POST" enctype="multipart/form-data"> <form action="/api/upload_video" method="POST" enctype="multipart/form-data">
<label for="username"><p>Title</p></label> <label for="username"><p>Title</p></label>
<input type="text" name="title"> <input type="text" name="title" autofocus>
<label for="password"><p>Video</p></label> <label for="password"><p>Video</p></label>
<input type="file" name="video"> <input type="file" name="video">
<br> <br>