import express from "express"; import cors from "cors"; import bcrypt from "bcrypt"; import fileUpload from "express-fileupload"; import path from "path"; import childProcess from "child_process"; import levenshtein from "js-levenshtein"; import cookieParser from "cookie-parser"; import { fileURLToPath } from "url"; import sqlite3 from "sqlite3"; let sessions = []; 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 videoPath(id) { return `videos/${id}.webm`; } function randomString(length) { const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789"; let result = ""; for (let i = 0; i < length; ++i) { result += chars[Math.floor(chars.length * Math.random())]; } return result; } function dirname() { return path.dirname(fileURLToPath(import.meta.url)); } const app = express(); app.use(cors()); app.use(express.json()); app.use(cookieParser()); app.use(express.urlencoded({ extended: true })); app.use("/", express.static("public/")); 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 = 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); 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 = 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.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 }); }); 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]; 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); const { total } = await dbGet("SELECT COUNT(*) AS total FROM videos"); return res.status(200).json({ ok: true, videos, total }); }); function authorized() { return async (req, res, next) => { const token = (() => { if (req.cookies && "token" in req.cookies) { return req.cookies["token"]; } else if ("token" in req.query) { return req.query["token"]; } else if (req.method === "post" && "token" in req.body) { return req.body["token"]; } else { return null; } })(); if (token === null) { return res.status(400).json({ ok: false, error: "unauthorized" }); } const session = sessions.find(session => session.token === token); if (session === undefined) { return res.status(400).json({ ok: false, error: "unauthorized" }); } const user = await dbGet("SELECT * FROM users WHERE id = ?", session.userId); if (user === undefined) { throw new Error("error: session with invalid userId"); } req.user = user; next(); } } app.get("/api/logout", authorized(), (req, res) => { sessions = sessions.filter(session => session.userId !== req.user.id); return res.status(200).json({ ok: true }); }); 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(), videoPath(id)); const thumbnailPath = path.join(dirname(), "videos", `${id}.png`); console.log(newPath); let exitCode = await new Promise(resolve => { 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", resolve); }); if (exitCode !== 0) { throw new Error("ffmpeg failed"); } exitCode = await new Promise(resolve => { const process = childProcess.spawn("ffmpeg", ["-i", tempPath, "-ss", "00:00:01.000", "-vframes", "1", thumbnailPath]); process.stderr.on("data", (data) => console.error(data.toString())); process.on("close", resolve); }); if (exitCode !== 0) { throw new Error("thumbnail generation failed"); } await dbRun("INSERT INTO videos (id, user_id, title) VALUES (?, ?, ?)", id, userId, title); return res.status(200).json({ ok: true }); }); app.get("/api/video-info", async (req, res) => { const id = req.query["id"]; const video = await dbGet(` SELECT videos.id, videos.title, users.username AS author FROM videos JOIN users ON users.id = videos.user_id WHERE videos.id = ? LIMIT 1 `, id); if (!video) { return res.status(404).json({ ok: false, error: "video not found" }); } video.path = path.join("/", videoPath(id)); return res.status(200).json({ ok: true, video }); }); app.use((err, req, res, next) => { console.error(err); res.status(500).json({ ok: false, error: "server error" }) }); app.listen(8000, () => { console.log("app at http://localhost:8000/"); })