htmx is so cool
This commit is contained in:
parent
e04ef78a42
commit
3ed7b0b5c7
11 changed files with 557 additions and 400 deletions
120
app/lib/db.ts
120
app/lib/db.ts
|
@ -8,62 +8,74 @@ mkdirp.sync("./var/db");
|
|||
export const db = new sqlite3.Database("./var/db/media.db");
|
||||
|
||||
/**Create the database schema for the embedders app*/
|
||||
export function createDatabase(version: number){
|
||||
export function createDatabase(version: number) {
|
||||
console.log("Creating database");
|
||||
|
||||
db.run("CREATE TABLE IF NOT EXISTS users ( \
|
||||
db.run(
|
||||
"CREATE TABLE IF NOT EXISTS users ( \
|
||||
id INTEGER PRIMARY KEY, \
|
||||
username TEXT UNIQUE, \
|
||||
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 ( \
|
||||
db.run(
|
||||
"CREATE TABLE IF NOT EXISTS media ( \
|
||||
id INTEGER PRIMARY KEY, \
|
||||
path TEXT NOT NULL, \
|
||||
expire INTEGER, \
|
||||
username TEXT \
|
||||
)");
|
||||
)"
|
||||
);
|
||||
|
||||
db.run(`PRAGMA user_version = ${version}`);
|
||||
}
|
||||
|
||||
/**Updates old Database schema to new */
|
||||
export function updateDatabase(oldVersion: number, newVersion: number){
|
||||
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;
|
||||
if (err) return;
|
||||
});
|
||||
db.run("ALTER TABLE media ADD COLUMN username TEXT", (err) => {
|
||||
if(err) return;
|
||||
if (err) return;
|
||||
});
|
||||
|
||||
|
||||
db.run("ALTER TABLE users ADD COLUMN expire TEXT", (err) => {
|
||||
if(err) return;
|
||||
if (err) return;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**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`);
|
||||
if (expireDate == null)
|
||||
console.log("It will not expire");
|
||||
else if (expireDate != null || expireDate != undefined)
|
||||
console.log(`It will expire on ${expireDate}`);
|
||||
export function insertToDB(
|
||||
filename: string,
|
||||
expireDate: Date,
|
||||
username: string
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const params: MediaParams = [filename, expireDate, username];
|
||||
|
||||
db.run(
|
||||
"INSERT INTO media (path, expire, username) VALUES (?, ?, ?)",
|
||||
params,
|
||||
function (err) {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
reject(err);
|
||||
} else {
|
||||
console.log(`Uploaded ${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}`);
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -74,7 +86,7 @@ export function searchImages(imagename: string, partial: boolean) {
|
|||
});
|
||||
}
|
||||
|
||||
export function updateImageName(oldimagename: string, newname:string) {
|
||||
export function updateImageName(oldimagename: string, newname: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log(`updating ${oldimagename} to ${newname}`);
|
||||
});
|
||||
|
@ -85,12 +97,11 @@ export function createUser(username: string, password: string) {
|
|||
return new Promise((resolve, reject) => {
|
||||
console.log(`Creating user ${username}`);
|
||||
const salt = crypto.randomBytes(16);
|
||||
|
||||
db.run("INSERT OR IGNORE INTO users (username, hashed_password, salt) VALUES (?, ?, ?)", [
|
||||
username,
|
||||
crypto.pbkdf2Sync(password, salt, 310000, 32, "sha256"),
|
||||
salt
|
||||
]);
|
||||
|
||||
db.run(
|
||||
"INSERT OR IGNORE INTO users (username, hashed_password, salt) VALUES (?, ?, ?)",
|
||||
[username, crypto.pbkdf2Sync(password, salt, 310000, 32, "sha256"), salt]
|
||||
);
|
||||
|
||||
resolve(null);
|
||||
});
|
||||
|
@ -102,7 +113,9 @@ export function getPath(id: number | string) {
|
|||
const query = "SELECT path FROM media WHERE id = ?";
|
||||
|
||||
db.get(query, [id], (err: Error, path: object) => {
|
||||
if (err) {reject(err);}
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
resolve(path);
|
||||
});
|
||||
});
|
||||
|
@ -114,14 +127,17 @@ export function deleteId(database: string, id: number | string) {
|
|||
const query = `DELETE FROM ${database} WHERE id = ?`;
|
||||
|
||||
db.run(query, [id], (err: Error) => {
|
||||
if (err) {reject(err); return;}
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**Expires a database row given a Date in unix time */
|
||||
export function expire(database: string, column: string, expiration:number) {
|
||||
export function expire(database: string, column: string, expiration: number) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const query = `SELECT * FROM ${database} WHERE ${column} < ?`;
|
||||
|
||||
|
@ -136,30 +152,26 @@ export function expire(database: string, column: string, expiration:number) {
|
|||
|
||||
/**A generic database row */
|
||||
export interface GenericRow {
|
||||
id? : number | string,
|
||||
username?: string
|
||||
expire? :Date
|
||||
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
|
||||
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
|
||||
]
|
||||
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
|
||||
}
|
||||
id?: number | string;
|
||||
username: string;
|
||||
hashed_password: any;
|
||||
salt: any;
|
||||
}
|
||||
|
|
|
@ -54,8 +54,8 @@ export const setEncodingType = (type: EncodingType) => {
|
|||
const getExecutablePath = (
|
||||
envVar: string,
|
||||
executable: string,
|
||||
installer: { path: string }
|
||||
) => {
|
||||
installer: { path: string },
|
||||
): string => {
|
||||
if (process.env[envVar]) {
|
||||
return process.env[envVar];
|
||||
}
|
||||
|
@ -70,12 +70,13 @@ const getExecutablePath = (
|
|||
const ffmpegPath = getExecutablePath(
|
||||
"EB_FFMPEG_PATH",
|
||||
"ffmpeg",
|
||||
ffmpegInstaller
|
||||
ffmpegInstaller,
|
||||
);
|
||||
|
||||
const ffprobePath = getExecutablePath(
|
||||
"EB_FFPROBE_PATH",
|
||||
"ffprobe",
|
||||
ffprobeInstaller
|
||||
ffprobeInstaller,
|
||||
);
|
||||
|
||||
console.log(`Using ffmpeg from path: ${ffmpegPath}`);
|
||||
|
@ -89,14 +90,14 @@ const checkEnvForEncoder = () => {
|
|||
|
||||
if (envEncoder && Object.keys(EncodingType).includes(envEncoder)) {
|
||||
setEncodingType(
|
||||
EncodingType[envEncoder as keyof typeof EncodingType] as EncodingType
|
||||
EncodingType[envEncoder as keyof typeof EncodingType] as EncodingType,
|
||||
);
|
||||
console.log(
|
||||
`Setting encoding type to ${envEncoder} based on environment variable.`
|
||||
`Setting encoding type to ${envEncoder} based on environment variable.`,
|
||||
);
|
||||
} else if (envEncoder) {
|
||||
console.warn(
|
||||
`Invalid encoder value "${envEncoder}" in environment variable, defaulting to CPU.`
|
||||
`Invalid encoder value "${envEncoder}" in environment variable, defaulting to CPU.`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -121,7 +122,7 @@ checkEnvForEncoder();
|
|||
export const ffmpegDownscale = (
|
||||
path: string,
|
||||
filename: string,
|
||||
extension: string
|
||||
extension: string,
|
||||
) => {
|
||||
const startTime = Date.now();
|
||||
const outputOptions = [
|
||||
|
@ -136,32 +137,31 @@ export const ffmpegDownscale = (
|
|||
];
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const progressFile = `uploads/${filename}${extension}-progress.json`;
|
||||
|
||||
ffmpeg()
|
||||
.input(path)
|
||||
.outputOptions(outputOptions)
|
||||
.output(`uploads/720p-${filename}${extension}`)
|
||||
.on("start", () => {
|
||||
// Create the .processing file
|
||||
fs.closeSync(
|
||||
fs.openSync(`uploads/720p-${filename}${extension}.processing`, "w")
|
||||
);
|
||||
.on("progress", function(progress) {
|
||||
fs.writeFileSync(progressFile, JSON.stringify({ progress: progress.percent / 100 }));
|
||||
})
|
||||
.on("end", () => {
|
||||
console.log(
|
||||
`720p copy complete using ${currentEncoding}, took ${
|
||||
Date.now() - startTime
|
||||
}ms to complete`
|
||||
}ms to complete`,
|
||||
);
|
||||
|
||||
// Delete the .processing file
|
||||
fs.unlinkSync(`uploads/720p-${filename}${extension}.processing`);
|
||||
fs.unlinkSync(progressFile);
|
||||
|
||||
resolve();
|
||||
})
|
||||
.on("error", (e) => {
|
||||
// Ensure to delete the .processing file even on error
|
||||
if (fs.existsSync(`uploads/720p-${filename}${extension}.processing`)) {
|
||||
fs.unlinkSync(`uploads/720p-${filename}${extension}.processing`);
|
||||
if (fs.existsSync(progressFile)) {
|
||||
fs.unlinkSync(progressFile);
|
||||
}
|
||||
|
||||
reject(new Error(e));
|
||||
|
@ -188,7 +188,7 @@ export const ffmpegDownscale = (
|
|||
export const ffmpegConvert = (
|
||||
path: string,
|
||||
filename: string,
|
||||
extension: string
|
||||
extension: string,
|
||||
) => {
|
||||
const startTime = Date.now();
|
||||
const outputOptions = [
|
||||
|
@ -217,15 +217,20 @@ export const ffmpegConvert = (
|
|||
}
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const progressFile = `uploads/${filename}${extension}-progress.json`;
|
||||
|
||||
ffmpeg()
|
||||
.input(path)
|
||||
.outputOptions(outputOptions)
|
||||
.output("uploads/")
|
||||
.outputFormat(outputFormat)
|
||||
.output(`uploads/${filename}${outputFormat}`)
|
||||
.on("progress", function(progress) {
|
||||
fs.writeFileSync(progressFile, JSON.stringify({ progress: progress.percent / 100 }));
|
||||
})
|
||||
.on("end", function () {
|
||||
console.log(
|
||||
`Conversion complete, took ${Date.now() - startTime} to complete`
|
||||
`Conversion complete, took ${Date.now() - startTime} to complete`,
|
||||
);
|
||||
console.log(`uploads/${filename}${outputFormat}`);
|
||||
resolve();
|
||||
|
@ -238,9 +243,14 @@ export const ffmpegConvert = (
|
|||
export const ffProbe = async (
|
||||
path: string,
|
||||
filename: string,
|
||||
extension: string
|
||||
extension: string,
|
||||
) => {
|
||||
return new Promise<FfprobeData>((resolve, reject) => {
|
||||
if (!videoExtensions.includes(extension) && !imageExtensions.includes(extension)) {
|
||||
console.log(`Extension is ${extension}`);
|
||||
reject(`Submitted file is neither a video nor an image: ${path}`);
|
||||
}
|
||||
|
||||
ffprobe(path, (err, data) => {
|
||||
if (err) reject(err);
|
||||
resolve(data);
|
||||
|
|
|
@ -25,6 +25,17 @@ export interface User {
|
|||
salt?: any;
|
||||
}
|
||||
|
||||
export interface oembedObj {
|
||||
type: string;
|
||||
version: string;
|
||||
provider_name: string;
|
||||
provider_url: string;
|
||||
cache_age: number;
|
||||
html: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export const videoExtensions = [
|
||||
".mp4",
|
||||
".mov",
|
||||
|
|
|
@ -3,7 +3,7 @@ import type { RequestHandler as Middleware, NextFunction } from "express";
|
|||
import fs from "fs";
|
||||
import process from "process";
|
||||
|
||||
import { extension, videoExtensions, imageExtensions } from "./lib";
|
||||
import { extension, videoExtensions, imageExtensions, oembedObj } from "./lib";
|
||||
import { insertToDB } from "./db";
|
||||
import { ffmpegDownscale, ffProbe } from "./ffmpeg";
|
||||
import { ffprobe } from "fluent-ffmpeg";
|
||||
|
@ -50,15 +50,9 @@ export const createEmbedData: Middleware = async (req, res, next) => {
|
|||
const files = req.files as Express.Multer.File[];
|
||||
for (const file in files) {
|
||||
const nameAndExtension = extension(files[file].originalname);
|
||||
const ffProbeData = await ffProbe(
|
||||
`uploads/${files[file].originalname}`,
|
||||
nameAndExtension[0],
|
||||
nameAndExtension[1],
|
||||
);
|
||||
const width = ffProbeData.streams[0].width;
|
||||
const height = ffProbeData.streams[0].height;
|
||||
const isMedia = videoExtensions.includes(nameAndExtension[1]) || imageExtensions.includes(nameAndExtension[1]);
|
||||
|
||||
const oembed = {
|
||||
const oembed: oembedObj = {
|
||||
type: "video",
|
||||
version: "1.0",
|
||||
provider_name: "embedder",
|
||||
|
@ -66,11 +60,24 @@ export const createEmbedData: Middleware = async (req, res, next) => {
|
|||
cache_age: 86400,
|
||||
html: `<iframe src='${req.protocol}://${req.get("host")}/gifv/${
|
||||
nameAndExtension[0]
|
||||
}${nameAndExtension[1]}'></iframe>`,
|
||||
width: width,
|
||||
height: height,
|
||||
}${nameAndExtension[1]}'></iframe>`
|
||||
};
|
||||
|
||||
if (isMedia) {
|
||||
let ffProbeData;
|
||||
try { ffProbeData = await ffProbe(
|
||||
`uploads/${files[file].originalname}`,
|
||||
nameAndExtension[0],
|
||||
nameAndExtension[1],
|
||||
); } catch (error) {
|
||||
console.log(`Error: ${error}`);
|
||||
console.log(nameAndExtension[1]);
|
||||
}
|
||||
|
||||
oembed.width = ffProbeData.streams[0].width;
|
||||
oembed.height = ffProbeData.streams[0].height;
|
||||
}
|
||||
|
||||
fs.writeFile(
|
||||
`uploads/oembed-${nameAndExtension[0]}${nameAndExtension[1]}.json`,
|
||||
JSON.stringify(oembed),
|
||||
|
@ -121,23 +128,27 @@ export const convertTo720p: Middleware = (req, res, next) => {
|
|||
};
|
||||
|
||||
/**Middleware for handling uploaded files. Inserts it into the database */
|
||||
export const handleUpload: Middleware = (req, res, next) => {
|
||||
export const handleUpload: Middleware = async (req, res, next) => {
|
||||
if (!req.file && !req.files) {
|
||||
console.log("No files were uploaded");
|
||||
return res.status(400).send("No files were uploaded.");
|
||||
}
|
||||
|
||||
const files = req.files ? (req.files as Express.Multer.File[]) : req.file; //Check if a single file was uploaded or multiple
|
||||
const username = req.user ? req.user.username : "sharex"; //if no username was provided, we can presume that it is sharex
|
||||
const files = req.files ? (req.files as Express.Multer.File[]) : req.file;
|
||||
const username = req.user ? req.user.username : "sharex";
|
||||
const expireDate: Date = req.body.expire
|
||||
? new Date(Date.now() + req.body.expire * 24 * 60 * 60 * 1000)
|
||||
: null;
|
||||
|
||||
if (files instanceof Array) {
|
||||
for (const file in files) {
|
||||
insertToDB(files[file].filename, expireDate, username);
|
||||
try {
|
||||
if (files instanceof Array) {
|
||||
await Promise.all(files.map(file => insertToDB(file.filename, expireDate, username)));
|
||||
} else {
|
||||
await insertToDB(files.filename, expireDate, username);
|
||||
}
|
||||
} else insertToDB(files.filename, expireDate, username);
|
||||
|
||||
next();
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error("Error in handleUpload:", error);
|
||||
res.status(500).send("Error processing files.");
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue