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 levenshtein from "js-levenshtein";
import cookieParser from "cookie-parser";
import { fileURLToPath } from 'url';
import { fileURLToPath } from "url";
import sqlite3 from "sqlite3";
const users = [];
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) {
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789";
@ -36,38 +49,51 @@ app.use("/videos", express.static("videos/"));
app.post("/api/register", async (req, res) => {
const { username, password } = req.body;
if (typeof username !== "string" || typeof password !== "string") {
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) {
return res.status(400).json({ ok: false, error: "username taken" });
}
const passwordHash = await bcrypt.hash(password, 10);
const id = users.length;
const user = { id, username, passwordHash };
users.push(user);
return res.status(200).json({ ok: true, user });
await dbGet("INSERT INTO users (username, password) VALUES (?, ?)", username, passwordHash);
return res.status(200).json({ ok: true });
});
app.post("/api/login", async (req, res) => {
const { username, password } = req.body;
if (typeof username !== "string" || typeof password !== "string") {
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) {
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" });
}
sessions = sessions.filter(session => session.userId !== user.id);
const token = randomString(64);
const session = { userId: user.id, token };
sessions.push(session);
res.clearCookie("token");
res.cookie("token", token);
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) => {
const page = +req.query.page || 0;
const search = req.query.query;
if (!search) {
return res.status(400).json({ ok: false, error: "bad request" });
}
const [start, end] = [20 * page, 20 * (page + 1)];
const withDistance = videos
.map(video => ({ dist: levenshtein(search, video.title), ...video }));
withDistance.sort((a, b) => a.dist - b.dist);
const returnedVideos = withDistance
.slice(start, end)
.map(video => {
const user = users.find(user => user.id === video.userId);
if (!user) {
return { ...video, author: "[Liberal]" };
}
return { ...video, author: user.username };
});
return res.status(200).json({ ok: true, videos: returnedVideos, total: videos.length });
const [start, end] = [20 * page, 20];
const videos = await dbAll(`
SELECT videos.*, users.username AS author
FROM videos
JOIN users ON users.id = videos.user_id
WHERE title LIKE CONCAT('%', ?, '%')
LIMIT ?
OFFSET ?
`, search, end, start);
console.log(videos);
const { total } = await dbGet("SELECT COUNT(*) AS total FROM videos");
return res.status(200).json({ ok: true, videos, total });
});
function authorized() {
return (req, res, next) => {
return async (req, res, next) => {
const token = (() => {
if (req.cookies && "token" in req.cookies) {
return req.cookies["token"];
@ -114,7 +143,7 @@ function authorized() {
if (session === undefined) {
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) {
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) => {
const { title } = req.body;
if (!req.files || !req.files.video) {
return res.status(400).json({ ok: false, error: "bad request" });
}
if (req.files.video.mimetype !== "video/mp4") {
return res.status(400).json({ ok: false, error: "bad mimetype" });
}
const userId = req.user.id;
const id = randomString(4);
const tempPath = req.files.video.tempFilePath;
const newPath = path.join(dirname(), "videos", `${id}.mp4`);
console.log(newPath);
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) => {
console.error(data.toString());
});
process.on("close", (code) => {
resolve(code);
})
})
});
if (exitCode !== 0) {
throw new Error("handbrake failed");
throw new Error("ffmpeg failed");
}
const video = {
id,
userId,
title,
path: newPath,
};
videos.push(video);
return res.status(200).json({ ok: true, video });
await dbRun("INSERT INTO videos (id, user_id, title, path) VALUES (?, ?, ?, ?)", id, userId, title, newPath);
return res.status(200).json({ ok: true });
})
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",
"express": "^4.18.2",
"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>
<div>
<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>
<input type="password" name="password" id="password">
<br>

View File

@ -9,7 +9,7 @@
<h1>MaoTube</h1>
<div>
<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>
<input type="password" name="password" id="password">
<br>

View File

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

View File

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

View File

@ -9,7 +9,7 @@
<h1>MaoTube</h1>
<form action="/api/upload_video" method="POST" enctype="multipart/form-data">
<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>
<input type="file" name="video">
<br>