diff --git a/app/app.ts b/app/app.ts index 4672266..0649b7a 100644 --- a/app/app.ts +++ b/app/app.ts @@ -12,12 +12,13 @@ const SQLiteStore = sqlite3(session); import fs from "fs"; import http from "http"; import path from "path"; +import { unlink } from 'fs/promises'; import authRouter from "./routes/auth"; import indexRouter from "./routes/index"; import adduserRouter from "./routes/adduser"; -import {db, expire, createDatabase, updateDatabase, MediaRow} from "./types/db"; +import {db, expire, createDatabase, updateDatabase, MediaRow, UserRow} from "./types/db"; const app = express(); const server = http.createServer(app); @@ -68,25 +69,31 @@ function onError(error: any) { // Check if there is an existing DB or not, then check if it needs to be updated to new schema -db.get("SELECT * FROM sqlite_master WHERE name ='users' and type='table'", async (err, row) => { - if (!row) createDatabase(2); - else checkVersion(); -}); +const row = db.prepare("SELECT * FROM sqlite_master WHERE name ='users' and type='table'").get(); -function checkVersion () { - db.get("PRAGMA user_version", (err: Error, row: any) => { - if (row && row.user_version) { +if (!row) { + createDatabase(2); +} else { + checkVersion(); +} + + +function checkVersion() { + // Using the synchronous API of better-sqlite3 + const row = db.prepare("PRAGMA user_version").get() as UserRow; + + if (row && row.user_version) { const version = row.user_version; if (version != 2) console.log("DATABASE IS OUTDATED"); //no future releases yet, and else statement handles version 1 //updateDatabase(version, 2); - } else { + } else { // Because ver 1 does not have user_version set, we can safely assume that it is ver 1 updateDatabase(1, 2); - } - }); + } } + function onListening() { const addr = server.address(); const bind = typeof addr === "string" @@ -127,25 +134,38 @@ app.use("/", adduserRouter); app.use("/uploads", express.static("uploads")); -async function prune () { - db.all("SELECT * FROM media", (err: Error, rows: []) => { - console.log("Uploaded files: " + rows.length); - console.log(rows); - }); +async function prune() { + try { + // Fetching all media entries + const rows = db.prepare("SELECT * FROM media").all() as MediaRow[]; + console.log("Uploaded files: " + rows.length); + console.log(rows); - console.log("Vacuuming database..."); - db.run("VACUUM"); + // Vacuuming the database + console.log("Vacuuming database..."); + db.prepare("VACUUM").run(); - db.each("SELECT path FROM media WHERE expire < ?", [Date.now()], (err: Error, row: MediaRow) => { - console.log(`Expired row: ${row}`); - fs.unlink(`uploads/${row.path}`, (err) => { - if (err && err.errno == -4058) { - console.log("File already deleted"); - } - }); - }); + // Deleting expired media files + const expiredRows= db.prepare("SELECT path FROM media WHERE expire < ?").all(Date.now()) as MediaRow[]; - await expire("media", "expire", Date.now()); + for (const row of expiredRows) { + console.log(`Expired row: ${row}`); + try { + await unlink(`uploads/${row.path}`); + } catch (err: any) { + if (err && err.code === 'ENOENT') { + console.log("File already deleted"); + } else { + console.error(`Failed to delete file: ${row.path}`, err); + } + } + } + + await expire("media", "expire", Date.now()); + + } catch (err) { + console.error("Error in prune function:", err); + } } -setInterval(prune, 1000 * 60); //prune every minute \ No newline at end of file +setInterval(prune, 1000 * 60); //prune every minute diff --git a/app/routes/auth.ts b/app/routes/auth.ts index 9bee3cf..b8c18e9 100644 --- a/app/routes/auth.ts +++ b/app/routes/auth.ts @@ -9,30 +9,33 @@ import {db, UserRow} from "../types/db"; const router = express.Router(); passport.use(new LocalStrategy(function verify(username, password, cb) { - db.get("SELECT * FROM users WHERE username = ?", [username], function(err: Error, row: UserRow) { - if (err) { - return cb(err); - } + try { + // Fetch user from database using better-sqlite3's synchronous API + const row = db.prepare("SELECT * FROM users WHERE username = ?").get(username) as UserRow; + if (!row) { return cb(null, false, { message: "Incorrect username or password." }); } - crypto.pbkdf2(password, row.salt, 310000, 32, "sha256", function(err, hashedPassword) { - if (err) { - return cb(err); - } - if (!crypto.timingSafeEqual(row.hashed_password, hashedPassword)) { - return cb(null, false, { - message: "Incorrect username or password." - }); - } - return cb(null, row); - }); - }); + // Synchronously hash the provided password with the stored salt + const hashedPassword = crypto.pbkdf2Sync(password, row.salt, 310000, 32, "sha256"); + + if (!crypto.timingSafeEqual(row.hashed_password, hashedPassword)) { + return cb(null, false, { + message: "Incorrect username or password." + }); + } + + return cb(null, row); + + } catch (err) { + return cb(err); + } })); + passport.serializeUser(function(user:User, cb) { process.nextTick(function() { cb(null, { diff --git a/app/routes/index.ts b/app/routes/index.ts index f42eac7..664fc8f 100644 --- a/app/routes/index.ts +++ b/app/routes/index.ts @@ -17,13 +17,32 @@ import {fileStorage} from "../types/multer"; import {checkAuth, checkSharexAuth, convertTo720p, createEmbedData, handleUpload} from "../types/middleware"; const upload = multer({ storage: fileStorage /**, fileFilter: fileFilter**/ }); //maybe make this a env variable? + /**Middleware to grab media from media database */ const fetchMedia: Middleware = (req, res, next) => { - const admin: boolean = req.user.username == "admin" ? true : false; - /**Check if the user is an admin, if so, show all posts from all users */ - const query: string = admin == true ? "SELECT * FROM media" : `SELECT * FROM media WHERE username = '${req.user.username}'`; + try { + const admin: boolean = req.user.username == "admin" ? true : false; + /**Check if the user is an admin, if so, show all posts from all users */ + const query: string = admin ? "SELECT * FROM media" : `SELECT * FROM media WHERE username = ?`; + const rows = (admin ? db.prepare(query).all() : db.prepare(query).all(req.user.username)) as MediaRow[]; + const files = rows.map((row: MediaRow) => { + return { + id: row.id, + path: row.path, + expire: row.expire, + sername: row.username, + url: "/" + row.id + }; + }); - db.all(query, (err:Error, rows: []) => { + res.locals.files = files.reverse(); //reverse so newest files appear first + res.locals.Count = files.length; + next(); + } catch (err) { + next(err); + } + + /**db.all(query, (err:Error, rows: []) => { if (err) return next(err); const files = rows.map((row: MediaRow)=> { return { @@ -37,7 +56,7 @@ const fetchMedia: Middleware = (req, res, next) => { res.locals.files = files.reverse(); //reverse so newest files appear first res.locals.Count = files.length; next(); - }); + });**/ }; const router = express.Router(); diff --git a/app/types/db.ts b/app/types/db.ts index adc4ff9..37eac7e 100644 --- a/app/types/db.ts +++ b/app/types/db.ts @@ -1,13 +1,57 @@ -import sqlite3 from "sqlite3"; +//import sqlite3 from "sqlite3"; import mkdirp from "mkdirp"; import crypto from "crypto"; +import { Database } from "bun:sqlite"; + mkdirp.sync("./uploads"); mkdirp.sync("./var/db"); -export const db = new sqlite3.Database("./var/db/media.db"); +export const db = new Database("./var/db/media.db"); -/**Create the database schema for the embedders app*/ +/** + * Represents a generic row structure in a database table. + */ +export interface GenericRow { + id? : number | string, + username?: string + expire? :Date +} + +/** + * Represents a row structure in the media table. + */ +export interface MediaRow { + id? : number | string, + path: string, + expire: Date, + username?: string +} + +/** + * A tuple representing parameters for media-related operations. + */ +export type MediaParams = [ + path: string, + expire: string | number | null, + username?: string +] + +/** + * Represents a row structure in the user table. + */ +export interface UserRow { + id? : number | string, + username: string, + hashed_password: any, + salt: any, + user_version? : number +} + +/** + * Initializes the database tables and sets the user version pragma. + * @param {number} version - The desired user version for the database. + */ export function createDatabase(version: number){ console.log("Creating database"); @@ -17,7 +61,9 @@ export function createDatabase(version: number){ hashed_password BLOB, \ expire INTEGER, \ salt BLOB \ - )", () => createUser("admin", process.env.EBPASS || "changeme")); + )") + + createUser("admin", process.env.EBPASS || "changeme"); db.run("CREATE TABLE IF NOT EXISTS media ( \ id INTEGER PRIMARY KEY, \ @@ -29,58 +75,75 @@ export function createDatabase(version: number){ db.run(`PRAGMA user_version = ${version}`); } -/**Updates old Database schema to new */ +/** + * Updates the database schema from an old version to a new one. + * @param {number} oldVersion - The current version of the database. + * @param {number} newVersion - The desired version to upgrade to. + */ export function updateDatabase(oldVersion: number, newVersion: number){ if (oldVersion == 1) { console.log(`Updating database from ${oldVersion} to ${newVersion}`); - db.run("PRAGMA user_version = 2", (err) => { - if(err) return; - }); - db.run("ALTER TABLE media ADD COLUMN username TEXT", (err) => { - if(err) return; - }); + db.run("PRAGMA user_version = 2"); + db.run("ALTER TABLE media ADD COLUMN username TEXT"); - db.run("ALTER TABLE users ADD COLUMN expire TEXT", (err) => { - if(err) return; - }); + db.run("ALTER TABLE users ADD COLUMN expire TEXT"); } } -/**Inserts into the media table */ -export function insertToDB (filename: string, expireDate: Date, username: string) { - const params: MediaParams = [ - filename, - expireDate, - username - ]; - - db.run("INSERT INTO media (path, expire, username) VALUES (?, ?, ?)", params, function (err) { - if (err) { - console.log(err); - return err; - } - console.log(`Uploaded ${filename} to database`); +/** + * Inserts a new media record into the media table. + * @param {string} filename - The name of the file. + * @param {string | number | null} expireDate - The expiration date for the media. + * @param {string} username - The name of the user. + */ +export function insertToDB (filename: string, expireDate: string | number | null, username: string) { + try { + const params: MediaParams = [ + filename, + expireDate, + username + ]; + + const query = db.prepare("INSERT INTO media (path, expire, username) VALUES (?, ?, ?)"); + query.run(...params); + + console.log(`Inserted ${filename} to database`) if (expireDate == null) console.log("It will not expire"); else if (expireDate != null || expireDate != undefined) console.log(`It will expire on ${expireDate}`); - }); + } catch (err) { + console.log(err); + throw err; + } } -/**Searches the database and returns images with partial or exact keysearches */ +/** + * Searches for images in the database based on a name or keyword. + * @param {string} imagename - The name or keyword to search for. + * @param {boolean} partial - Whether to perform a partial search. + */ export function searchImages(imagename: string, partial: boolean) { return new Promise((resolve, reject) => { console.log(`searching for ${imagename}`); }); } +/** + * Updates the name of an image in the database. + * @param {string} oldimagename - The current name of the image. + * @param {string} newname - The new name for the image. + */ export function updateImageName(oldimagename: string, newname:string) { return new Promise((resolve, reject) => { console.log(`updating ${oldimagename} to ${newname}`); }); } - -/**Inserts a new user to the database */ +/** + * Adds a new user to the users table in the database. + * @param {string} username - The name of the user. + * @param {string} password - The user's password. + */ export function createUser(username: string, password: string) { return new Promise((resolve, reject) => { console.log(`Creating user ${username}`); @@ -96,70 +159,50 @@ export function createUser(username: string, password: string) { }); } -/**Selects the path for a file given ID */ -export function getPath(id: number | string) { +/** + * Retrieves the path for a file from the media table based on the given ID. + * + * @param {number|string} id - The ID of the record whose path needs to be fetched. + * @returns {Promise} A promise that resolves with the selected paths. + * The resolved value is an array of records (usually containing a single item). + * @throws Will reject the promise with an error if the query fails. + */ +export function getPath(id: number | string): Promise { return new Promise((resolve, reject) => { - const query = "SELECT path FROM media WHERE id = ?"; - - db.get(query, [id], (err: Error, path: object) => { - if (err) {reject(err);} - resolve(path); - }); + try { + const query = db.prepare("SELECT path FROM media WHERE id = ?"); + resolve(query.all(id)); + } catch (err) { + reject(err); + } }); } -/**Deletes from database given an ID */ -export function deleteId(database: string, id: number | string) { - return new Promise((resolve, reject) => { - const query = `DELETE FROM ${database} WHERE id = ?`; - - db.run(query, [id], (err: Error) => { - if (err) {reject(err); return;} - resolve(null); - }); - }); +/** + * Deletes a row from a specified table based on its ID. + * + * @param {string} database - The name of the database table. + * @param {number | string} id - The ID of the row to delete. + */ +export function deleteId(database: string, id: number | string): void { + const deleteQuery = `DELETE FROM ${database} WHERE id = ?`; + db.prepare(deleteQuery).run(id); } -/**Expires a database row given a Date in unix time */ +/** + * Removes rows from a table that are older than a given expiration date. + * + * @param {string} database - The name of the database table. + * @param {string} column - The column name used for date comparison. + * @param {number} expiration - The expiration date in UNIX time. + * @returns {Promise} + */ export function expire(database: string, column: string, expiration:number) { return new Promise((resolve, reject) => { - const query = `SELECT * FROM ${database} WHERE ${column} < ?`; + const rows: GenericRow[] = db.query(`SELECT * FROM ${database} WHERE ${column} < ?`).all(expiration); - db.each(query, [expiration], async (err: Error, row: GenericRow) => { - if (err) reject(err); - await deleteId(database, row.id); - - resolve(null); - }); + for (const row of rows) { + deleteId(database, row.id); + } }); -} - -/**A generic database row */ -export interface GenericRow { - id? : number | string, - username?: string - expire? :Date -} - -/**A row for the media database */ -export interface MediaRow { - id? : number | string, - path: string, - expire: Date, - username?: string -} - -/**Params type for doing work with media database */ -export type MediaParams = [ - path: string, - expire: Date, - username?: string -] - -/**A row for the user database */ -export interface UserRow { - id? : number | string, - username: string, - hashed_password: any, - salt: any } \ No newline at end of file diff --git a/app/types/middleware.ts b/app/types/middleware.ts index 7c596e1..00fa278 100644 --- a/app/types/middleware.ts +++ b/app/types/middleware.ts @@ -151,10 +151,13 @@ export const handleUpload: Middleware = (req, res, next) => { console.log("No files were uploaded"); return res.status(400).send("No files were uploaded."); } + + console.log("running handleUpload") const files = (req.files) ? req.files as Express.Multer.File[] : req.file; //Check if a single file was uploaded or multiple + console.log(files); const username = (req.user) ? req.user.username : "sharex"; //if no username was provided, we can presume that it is sharex - const expireDate: Date = (req.body.expire) ? new Date(Date.now() + (req.body.expire * 24 * 60 * 60 * 1000)) : null; + const expireDate: number | null = (req.body.expire) ? Date.now() + (req.body.expire * 24 * 60 * 60 * 1000) : null; if (files instanceof Array) { for (const file in files) { diff --git a/app/types/multer.ts b/app/types/multer.ts index 64f4568..8debe45 100644 --- a/app/types/multer.ts +++ b/app/types/multer.ts @@ -1,5 +1,5 @@ import {Request} from "express"; -import multer, {FileFilterCallback} from "multer"; +import multer, {FileFilterCallback, MulterError} from "multer"; import {db, MediaRow} from "./db"; import {extension} from "./lib"; @@ -22,27 +22,26 @@ export const fileStorage = multer.diskStorage({ ): void => { const nameAndExtension = extension(file.originalname); console.log(`Uploading ${file}`); - db.all("SELECT * FROM media WHERE path = ?", [nameAndExtension[0] + nameAndExtension[1]], (err: Error, exists: []) => { - if (err) { - console.log(err); - callback(err, null); - } - if (exists.length != 0) { - const suffix = new Date().getTime() / 1000; + try { + console.log("querying") + const query = db.query(`SELECT * FROM media WHERE path = ?`); + const exists = query.all(nameAndExtension[0] + nameAndExtension[1]); + console.log(exists) - if (request.body.title == "" || request.body.title == null || request.body.title == undefined) { - callback(null, nameAndExtension[0] + "-" + suffix + nameAndExtension[1]); - } else { - callback(null, request.body.title + "-" + suffix + nameAndExtension[1]); - } + if (exists.length !== 0) { + const suffix = (new Date().getTime() / 1000).toString(); + const newName = (request.body.title || nameAndExtension[0]) + "-" + suffix + nameAndExtension[1]; + callback(null, newName); + console.log("ran callback with suffix") } else { - if (request.body.title == "" || request.body.title == null || request.body.title == undefined) { - callback(null, nameAndExtension[0] + nameAndExtension[1]); - } else { - callback(null, request.body.title + nameAndExtension[1]); - } + const newName = (request.body.title || nameAndExtension[0]) + nameAndExtension[1]; + callback(null, newName); + console.log("ran callback") } - }); + } catch (err: any) { + console.log(err); + callback(err, null); + } } }); diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..d6a9248 Binary files /dev/null and b/bun.lockb differ diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..f67b2c6 --- /dev/null +++ b/index.ts @@ -0,0 +1 @@ +console.log("Hello via Bun!"); \ No newline at end of file diff --git a/package.json b/package.json index 66e6211..093909a 100644 --- a/package.json +++ b/package.json @@ -61,11 +61,14 @@ "@types/probe-image-size": "^7.2.0", "@typescript-eslint/eslint-plugin": "^5.46.1", "@typescript-eslint/parser": "^5.46.1", + "bun-types": "^1.0.2", "copyfiles": "^2.4.1", "eslint": "^8.28.0", "rimraf": "^3.0.2", "ts-node": "^10.9.1", "ts-node-dev": "^2.0.0", "typescript": "^4.9.3" - } -} + }, + "module": "index.ts", + "type": "module" +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 572267d..ea9f3ca 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "types": ["bun-types"], /* Visit https://aka.ms/tsconfig to read more about this file */ /* Projects */