commit
0163d465c6
20 changed files with 1877 additions and 840 deletions
39
.eslintrc.js
Normal file
39
.eslintrc.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
module.exports = {
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"overrides": [
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
2
|
||||
],
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"double"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
]
|
||||
}
|
||||
};
|
|
@ -1,31 +0,0 @@
|
|||
{
|
||||
"env": {
|
||||
"node": true,
|
||||
"commonjs": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"overrides": [
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest"
|
||||
},
|
||||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
"tab"
|
||||
],
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"double"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
]
|
||||
}
|
||||
}
|
180
app/app.ts
180
app/app.ts
|
@ -1,6 +1,6 @@
|
|||
import type {MediaRow, UserRow} from './types';
|
||||
const version = 1.9;
|
||||
|
||||
require("dotenv").config();
|
||||
import "dotenv";
|
||||
|
||||
import express from "express";
|
||||
import passport from "passport";
|
||||
|
@ -15,28 +15,26 @@ import path from "path";
|
|||
|
||||
import authRouter from "./routes/auth";
|
||||
import indexRouter from "./routes/index";
|
||||
import adduserRouter from "./routes/adduser";
|
||||
|
||||
import {createUser} from "./db";
|
||||
import db from "./db"
|
||||
import {db, expire, createDatabase, updateDatabase, MediaRow} from "./types/db";
|
||||
|
||||
let app = express();
|
||||
let server = http.createServer(app);
|
||||
let port = normalizePort(process.env.EBPORT || "3000");
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
const port = normalizePort(process.env.EBPORT || "3000");
|
||||
|
||||
function normalizePort(val: string) {
|
||||
var port = parseInt(val, 10);
|
||||
const port = parseInt(val, 10);
|
||||
|
||||
if (isNaN(port)) {
|
||||
// named pipe
|
||||
return val;
|
||||
}
|
||||
if (isNaN(port)) {
|
||||
return val;
|
||||
}
|
||||
|
||||
if (port >= 0) {
|
||||
// port number
|
||||
return port;
|
||||
}
|
||||
if (port >= 0) {
|
||||
return port;
|
||||
}
|
||||
|
||||
return false;
|
||||
return false;
|
||||
}
|
||||
|
||||
app.set("port", port);
|
||||
|
@ -45,53 +43,57 @@ server.on("error", onError);
|
|||
server.on("listening", onListening);
|
||||
|
||||
function onError(error: any) {
|
||||
if (error.syscall !== "listen") {
|
||||
throw error;
|
||||
}
|
||||
if (error.syscall !== "listen") {
|
||||
throw error;
|
||||
}
|
||||
|
||||
var bind = typeof port === "string"
|
||||
? "Pipe " + port
|
||||
: "Port " + port;
|
||||
const bind = typeof port === "string"
|
||||
? "Pipe " + port
|
||||
: "Port " + port;
|
||||
|
||||
// handle specific listen errors with friendly messages
|
||||
switch (error.code) {
|
||||
case "EACCES":
|
||||
console.error(bind + " requires elevated privileges");
|
||||
process.exit(1);
|
||||
break;
|
||||
case "EADDRINUSE":
|
||||
console.error(bind + " is already in use");
|
||||
process.exit(1);
|
||||
break;
|
||||
default:
|
||||
throw error;
|
||||
}
|
||||
// handle specific listen errors with friendly messages
|
||||
switch (error.code) {
|
||||
case "EACCES":
|
||||
console.error(bind + " requires elevated privileges");
|
||||
process.exit(1);
|
||||
break;
|
||||
case "EADDRINUSE":
|
||||
console.error(bind + " is already in use");
|
||||
process.exit(1);
|
||||
break;
|
||||
default:
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
db.serialize(function() {
|
||||
// create the database schema for the embedders app
|
||||
db.run("CREATE TABLE IF NOT EXISTS users ( \
|
||||
id INTEGER PRIMARY KEY, \
|
||||
username TEXT UNIQUE, \
|
||||
hashed_password BLOB, \
|
||||
salt BLOB \
|
||||
)");
|
||||
|
||||
db.run("CREATE TABLE IF NOT EXISTS media ( \
|
||||
id INTEGER PRIMARY KEY, \
|
||||
path TEXT NOT NULL, \
|
||||
expire INTEGER \
|
||||
)");
|
||||
|
||||
createUser("admin", process.env.EBPASS || "changeme");
|
||||
// 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();
|
||||
});
|
||||
|
||||
function checkVersion () {
|
||||
db.get("PRAGMA user_version", (err: Error, row: any) => {
|
||||
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 {
|
||||
// Because ver 1 does not have user_version set, we can safely assume that it is ver 1
|
||||
updateDatabase(1, 2);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onListening() {
|
||||
var addr = server.address();
|
||||
var bind = typeof addr === "string"
|
||||
? "pipe " + addr
|
||||
: "port " + addr.port;
|
||||
console.log("Listening on " + bind);
|
||||
const addr = server.address();
|
||||
const bind = typeof addr === "string"
|
||||
? "pipe " + addr
|
||||
: "port " + addr.port;
|
||||
console.log("Embedder version: " + version);
|
||||
console.log("Listening on " + bind);
|
||||
}
|
||||
|
||||
app.enable("trust proxy");
|
||||
|
@ -102,62 +104,48 @@ app.set("view engine", "ejs");
|
|||
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({
|
||||
extended: false
|
||||
extended: false
|
||||
}));
|
||||
app.use(cookieParser());
|
||||
app.use(express.static(path.join(__dirname, "public")));
|
||||
|
||||
app.use(express.static(path.join(__dirname, "public")));
|
||||
app.use(session({
|
||||
secret: process.env.EBSECRET || "pleasechangeme",
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
// @ts-ignore
|
||||
store: new SQLiteStore({
|
||||
db: "sessions.db",
|
||||
dir: "./var/db"
|
||||
})
|
||||
secret: process.env.EBSECRET || "pleasechangeme",
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
store: new SQLiteStore({
|
||||
db: "sessions.db",
|
||||
dir: "./var/db"
|
||||
}) as session.Store
|
||||
}));
|
||||
app.use(passport.authenticate("session"));
|
||||
|
||||
app.use("/", indexRouter);
|
||||
app.use("/", authRouter);
|
||||
app.use("/", adduserRouter);
|
||||
|
||||
app.use("/uploads", express.static("uploads"));
|
||||
|
||||
function prune () {
|
||||
db.all("SELECT * FROM media", (err: Error, rows: []) => {
|
||||
console.log("Uploaded files: " + rows.length);
|
||||
console.log(rows);
|
||||
});
|
||||
async function prune () {
|
||||
db.all("SELECT * FROM media", (err: Error, rows: []) => {
|
||||
console.log("Uploaded files: " + rows.length);
|
||||
console.log(rows);
|
||||
});
|
||||
|
||||
console.log("Vacuuming database...");
|
||||
db.run("VACUUM");
|
||||
console.log("Vacuuming database...");
|
||||
db.run("VACUUM");
|
||||
|
||||
db.all("SELECT * FROM media WHERE expire < ?", [Date.now()], (err: Error, rows: []) => {
|
||||
console.log("Expired rows: " + rows);
|
||||
if (err) return console.error(err);
|
||||
rows.forEach((row: MediaRow) => {
|
||||
console.log(`Deleting ${row.path}`);
|
||||
fs.unlink(`uploads/${row.path}`, (err) => {
|
||||
if (err) {
|
||||
if(err.errno == -4058) {
|
||||
console.log("File already deleted");
|
||||
db.all("DELETE FROM media WHERE path = ?", [row.path], (err: Error) => {
|
||||
if (err) return console.error(err);
|
||||
});
|
||||
} else {
|
||||
console.error(err);
|
||||
}
|
||||
} else {
|
||||
db.all("DELETE FROM media WHERE path = ?", [row.path], (err: Error) => {
|
||||
if (err) return console.error(err);
|
||||
});
|
||||
}
|
||||
});
|
||||
console.log(`Deleted ${row.path}`);
|
||||
});
|
||||
});
|
||||
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");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await expire("media", "expire", Date.now());
|
||||
}
|
||||
|
||||
setInterval(prune, 1000 * 60); //prune every minute
|
21
app/db.ts
21
app/db.ts
|
@ -1,21 +0,0 @@
|
|||
import type {RequestHandler as Middleware} from 'express';
|
||||
|
||||
const sqlite3 = require("sqlite3");
|
||||
const mkdirp = require("mkdirp");
|
||||
const crypto = require("crypto");
|
||||
|
||||
mkdirp.sync("./uploads");
|
||||
mkdirp.sync("./var/db");
|
||||
|
||||
let db = new sqlite3.Database("./var/db/media.db");
|
||||
|
||||
export function createUser(username: string, password: string) {
|
||||
var 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
|
||||
]);
|
||||
}
|
||||
|
||||
export default db;
|
|
@ -39,17 +39,34 @@
|
|||
}
|
||||
|
||||
/* background image by Cole Bemis <https://feathericons.com> */
|
||||
.nav .user {
|
||||
.user {
|
||||
padding-left: 20px;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-user'%3E%3Cpath d='M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2'%3E%3C/path%3E%3Ccircle cx='12' cy='7' r='4'%3E%3C/circle%3E%3C/svg%3E");
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-user'%3E%3Cpath d='M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2'%3E%3C/path%3E%3Ccircle cx='12' cy='7' r='4'%3E%3C/circle%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: center left;
|
||||
}
|
||||
|
||||
/* background image by Cole Bemis <https://feathericons.com> */
|
||||
.nav .logout {
|
||||
.username {
|
||||
padding-left: 20px;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-log-out'%3E%3Cpath d='M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4'%3E%3C/path%3E%3Cpolyline points='16 17 21 12 16 7'%3E%3C/polyline%3E%3Cline x1='21' y1='12' x2='9' y2='12'%3E%3C/line%3E%3C/svg%3E%0A");
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-user'%3E%3Cpath d='M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2'%3E%3C/path%3E%3Ccircle cx='12' cy='7' r='4'%3E%3C/circle%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: center left;
|
||||
color: #73AD21;
|
||||
}
|
||||
|
||||
/* background image by Cole Bemis <https://feathericons.com> */
|
||||
.logout {
|
||||
padding-left: 20px;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-log-out'%3E%3Cpath d='M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4'%3E%3C/path%3E%3Cpolyline points='16 17 21 12 16 7'%3E%3C/polyline%3E%3Cline x1='21' y1='12' x2='9' y2='12'%3E%3C/line%3E%3C/svg%3E%0A");
|
||||
background-repeat: no-repeat;
|
||||
background-position: center left;
|
||||
}
|
||||
|
||||
/* background image by Cole Bemis <https://feathericons.com> */
|
||||
.adduser {
|
||||
padding-left: 20px;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-user-plus'%3E%3Cpath d='M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2'%3E%3C/path%3E%3Ccircle cx='8.5' cy='7' r='4'%3E%3C/circle%3E%3Cline x1='20' y1='8' x2='20' y2='14'%3E%3C/line%3E%3Cline x1='23' y1='11' x2='17' y2='11'%3E%3C/line%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: center left;
|
||||
}
|
||||
|
|
|
@ -131,8 +131,6 @@ function uploadFile(file) {
|
|||
});
|
||||
|
||||
if (file == null || file == undefined) {
|
||||
//file = reader.readAsDataURL(document.getElementById("fileupload").files[0]);
|
||||
//file = reader.readAsDataURL(document.querySelector("#fileupload").files[0]);
|
||||
file = document.querySelector("#fileupload").files[0];
|
||||
}
|
||||
|
||||
|
|
29
app/routes/adduser.ts
Normal file
29
app/routes/adduser.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import type {RequestHandler as Middleware, Router, Request, Response, NextFunction} from "express";
|
||||
import express from "express";
|
||||
|
||||
import {createUser} from "../types/db";
|
||||
|
||||
const router: Router = express.Router();
|
||||
/**Middleware to check if a user is actually signed in */
|
||||
const adminCheck: Middleware = (req: Request, res: Response, next: NextFunction) => {
|
||||
if (!req.user)
|
||||
return res.status(403).send("You are not authorized to perform this action");
|
||||
else {
|
||||
if (req.user.username != "admin")
|
||||
return res.status(403).send("You are not authorized to perform this action");
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
router.get("/adduser", adminCheck, (req: Request, res: Response) => {
|
||||
res.locals.filter = null;
|
||||
res.render("adduser", { user: req.user });
|
||||
});
|
||||
|
||||
router.post("/adduser", adminCheck, (req: Request, res: Response) => {
|
||||
createUser(req.body.username, req.body.password);
|
||||
res.redirect("/");
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -1,76 +1,69 @@
|
|||
import type {MediaRow, UserRow} from '../types';
|
||||
import type {RequestHandler as Middleware} from 'express';
|
||||
|
||||
import crypto from "crypto";
|
||||
import express from "express";
|
||||
import passport from "passport";
|
||||
import {Strategy as LocalStrategy} from "passport-local";
|
||||
|
||||
import { Strategy as LocalStrategy } from "passport-local";
|
||||
import {User} from "../types/lib";
|
||||
import {db, UserRow} from "../types/db";
|
||||
|
||||
import db from "../db";
|
||||
|
||||
let router = express.Router();
|
||||
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);
|
||||
}
|
||||
if (!row) {
|
||||
return cb(null, false, {
|
||||
message: "Incorrect username or password."
|
||||
});
|
||||
}
|
||||
db.get("SELECT * FROM users WHERE username = ?", [username], function(err: Error, row: UserRow) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
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);
|
||||
});
|
||||
});
|
||||
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);
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
passport.serializeUser(function(user, cb) {
|
||||
process.nextTick(function() {
|
||||
cb(null, {
|
||||
// @ts-ignore
|
||||
id: user.id,
|
||||
// @ts-ignore
|
||||
username: user.username
|
||||
});
|
||||
});
|
||||
passport.serializeUser(function(user:User, cb) {
|
||||
process.nextTick(function() {
|
||||
cb(null, {
|
||||
id: user.id,
|
||||
username: user.username
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
passport.deserializeUser(function(user, cb) {
|
||||
process.nextTick(function() {
|
||||
return cb(null, user);
|
||||
});
|
||||
passport.deserializeUser(function(user:User, cb) {
|
||||
process.nextTick(function() {
|
||||
return cb(null, user);
|
||||
});
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
router.get("/login", function(req, res) {
|
||||
res.render("login");
|
||||
res.render("login");
|
||||
});
|
||||
|
||||
router.post("/login/password", passport.authenticate("local", {
|
||||
successRedirect: "/",
|
||||
failureRedirect: "/login"
|
||||
successRedirect: "/",
|
||||
failureRedirect: "/login"
|
||||
}));
|
||||
|
||||
router.post("/logout", function(req, res, next) {
|
||||
// @ts-ignore, logout is already initalized in app.js
|
||||
req.logout(function(err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
res.redirect("/");
|
||||
});
|
||||
req.logout(function(err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
res.redirect("/");
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
import type {RequestHandler as Middleware, Router, Request, Response} from 'express';
|
||||
import types from 'multer';
|
||||
|
||||
import type {RequestHandler as Middleware, Request, Response, NextFunction} from "express";
|
||||
import multer from "multer";
|
||||
import express from "express";
|
||||
import ffmpeg from "fluent-ffmpeg";
|
||||
import imageProbe from "probe-image-size";
|
||||
import ffmpegpath from "@ffmpeg-installer/ffmpeg";
|
||||
// @ts-ignore
|
||||
import ffprobepath from "@ffprobe-installer/ffprobe";
|
||||
|
||||
ffmpeg.setFfmpegPath(ffmpegpath.path);
|
||||
|
@ -14,160 +11,87 @@ ffmpeg.setFfprobePath(ffprobepath.path);
|
|||
|
||||
import fs from "fs";
|
||||
|
||||
import db from "../db";
|
||||
import {checkAuth, convert, handleUpload} from "./middleware";
|
||||
import { MediaRow } from '../types';
|
||||
|
||||
function extension(str: String){
|
||||
let file = str.split("/").pop();
|
||||
return [file.substr(0,file.lastIndexOf(".")),file.substr(file.lastIndexOf("."),file.length).toLowerCase()];
|
||||
}
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: function (req, file, cb) {
|
||||
cb(null, "uploads/");
|
||||
},
|
||||
filename : function(req, file, cb) {
|
||||
let nameAndExtension = extension(file.originalname);
|
||||
db.all("SELECT * FROM media WHERE path = ?", [nameAndExtension[0] + nameAndExtension[1]], function (err: Error, exists: []) {
|
||||
if (exists.length != 0) {
|
||||
let suffix = new Date().getTime() / 1000;
|
||||
|
||||
if (req.body.title == "" || req.body.title == null || req.body.title == undefined)
|
||||
cb(null, nameAndExtension[0] + "-" + suffix + nameAndExtension[1]);
|
||||
else
|
||||
cb(null, req.body.title + "-" + suffix + nameAndExtension[1]);
|
||||
} else {
|
||||
if (req.body.title == "" || req.body.title == null || req.body.title == undefined)
|
||||
cb(null, nameAndExtension[0] + nameAndExtension[1]);
|
||||
else
|
||||
cb(null, req.body.title + nameAndExtension[1]);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**let allowedMimeTypes = [
|
||||
"image/png",
|
||||
"image/jpg",
|
||||
"image/jpeg",
|
||||
"image/gif",
|
||||
"image/webp",
|
||||
"video/mp4",
|
||||
"video/mov",
|
||||
"video/webm",
|
||||
"audio/mpeg",
|
||||
"audio/ogg"
|
||||
];
|
||||
|
||||
const fileFilter = function(req, file, cb) {
|
||||
if (allowedMimeTypes.includes(file.mimetype)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(null, false);
|
||||
}
|
||||
};**/
|
||||
|
||||
let upload = multer({ storage: storage /**, fileFilter: fileFilter**/ }); //maybe make this a env variable?
|
||||
import {extension} from "../types/lib";
|
||||
import {db, MediaRow, getPath, deleteId} from "../types/db";
|
||||
import {fileStorage} from "../types/multer";
|
||||
import {checkAuth, checkSharexAuth, createEmbedData, handleUpload} from "./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) => {
|
||||
db.all("SELECT * FROM media", (err: Error, rows: []) => {
|
||||
if (err) return next(err);
|
||||
let files = rows.map((row: MediaRow)=> {
|
||||
return {
|
||||
id: row.id,
|
||||
path: row.path,
|
||||
expire: row.expire,
|
||||
url: "/" + row.id
|
||||
};
|
||||
});
|
||||
res.locals.files = files.reverse(); //reverse so newest files appear first
|
||||
res.locals.Count = files.length;
|
||||
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}'`;
|
||||
|
||||
let router = express.Router();
|
||||
db.all(query, (err:Error, rows: []) => {
|
||||
if (err) return next(err);
|
||||
const files = rows.map((row: MediaRow)=> {
|
||||
return {
|
||||
id: row.id,
|
||||
path: row.path,
|
||||
expire: row.expire,
|
||||
username: row.username,
|
||||
url: "/" + row.id
|
||||
};
|
||||
});
|
||||
res.locals.files = files.reverse(); //reverse so newest files appear first
|
||||
res.locals.Count = files.length;
|
||||
next();
|
||||
});
|
||||
};
|
||||
|
||||
router.get("/", (req, res, next) => {
|
||||
// @ts-ignore, user is part of req header
|
||||
if (!req.user) { return res.render("home"); }
|
||||
next();
|
||||
}, fetchMedia, (req, res) => {
|
||||
res.locals.filter = null;
|
||||
// @ts-ignore, user is part of req header
|
||||
res.render("index", { user: req.user });
|
||||
const router = express.Router();
|
||||
|
||||
router.get("/", (req: Request, res: Response, next: NextFunction) => {
|
||||
if (!req.user)
|
||||
return res.render("home");
|
||||
next();
|
||||
}, fetchMedia, (req: Request, res: Response) => {
|
||||
res.locals.filter = null;
|
||||
res.render("index", { user: req.user });
|
||||
});
|
||||
|
||||
router.get("/gifv/:file", async function (req, res, next) {
|
||||
let url = `${req.protocol}://${req.get("host")}/uploads/${req.params.file}`;
|
||||
let width; let height;
|
||||
router.get("/gifv/:file", async (req: Request, res: Response, next: NextFunction) => {
|
||||
const url = `${req.protocol}://${req.get("host")}/uploads/${req.params.file}`;
|
||||
let width; let height;
|
||||
|
||||
let nameAndExtension = extension("uploads/" + req.params.file);
|
||||
if (nameAndExtension[1] == ".mp4") {
|
||||
ffmpeg()
|
||||
.input("uploads/" + req.params.file)
|
||||
.inputFormat("mp4")
|
||||
.ffprobe(function(err, data) {
|
||||
if (err) return next(err);
|
||||
width = data.streams[0].width;
|
||||
height = data.streams[0].height;
|
||||
return res.render("gifv", { url: url, host: `${req.protocol}://${req.get("host")}`, width: width, height: height });
|
||||
});
|
||||
} else if (nameAndExtension[1] == ".gif") {
|
||||
ffmpeg()
|
||||
.input("uploads/" + req.params.file)
|
||||
.inputFormat("gif")
|
||||
.ffprobe(function(err, data) {
|
||||
if (err) return next(err);
|
||||
width = data.streams[0].width;
|
||||
height = data.streams[0].height;
|
||||
return res.render("gifv", { url: url, host: `${req.protocol}://${req.get("host")}`, width: width, height: height });
|
||||
});
|
||||
} else {
|
||||
let imageData = await imageProbe(fs.createReadStream("uploads/" + req.params.file));
|
||||
return res.render("gifv", { url: url, host: `${req.protocol}://${req.get("host")}`, width: imageData.width, height: imageData.height });
|
||||
}
|
||||
const nameAndExtension = extension(`uploads/${req.params.file}`);
|
||||
if (nameAndExtension[1] == ".mp4" || nameAndExtension[1] == ".mov" || nameAndExtension[1] == ".webm" || nameAndExtension[1] == ".gif") {
|
||||
ffmpeg()
|
||||
.input(`uploads/${req.params.file}`)
|
||||
.inputFormat(nameAndExtension[1].substring(1))
|
||||
.ffprobe((err: Error, data: ffmpeg.FfprobeData) => {
|
||||
if (err) return next(err);
|
||||
width = data.streams[0].width;
|
||||
height = data.streams[0].height;
|
||||
return res.render("gifv", { url: url, host: `${req.protocol}://${req.get("host")}`, width: width, height: height });
|
||||
});
|
||||
} else {
|
||||
const imageData = await imageProbe(fs.createReadStream(`uploads/${req.params.file}`));
|
||||
return res.render("gifv", { url: url, host: `${req.protocol}://${req.get("host")}`, width: imageData.width, height: imageData.height });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/", [upload.array("fileupload"), convert, handleUpload], (req: Request, res: Response) => {
|
||||
return res.redirect("/");
|
||||
router.post("/", [checkAuth, upload.array("fileupload"), createEmbedData, handleUpload], (req: Request, res: Response) => {
|
||||
res.redirect("/");
|
||||
});
|
||||
|
||||
router.post("/sharex", [checkAuth, upload.array("fileupload"), convert, handleUpload], (req: Request, res: Response) => {
|
||||
// @ts-ignore
|
||||
return res.send(`${req.protocol}://${req.get("host")}/uploads/${req.files[0].filename}`);
|
||||
router.post("/sharex", [checkSharexAuth, upload.single("fileupload"), createEmbedData, handleUpload], (req: Request, res: Response) => {
|
||||
return res.send(`${req.protocol}://${req.get("host")}/uploads/${req.file.filename}`);
|
||||
});
|
||||
|
||||
router.post("/:id(\\d+)/delete", function(req, res, next) {
|
||||
db.all("SELECT path FROM media WHERE id = ?", [ req.params.id ], function(err: Error, path: Array<any>) {
|
||||
if (err) { return next(err); }
|
||||
fs.unlink(`uploads/${path[0].path}`, (err => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
if (err.errno == -4058) { //File just doesnt exist anymore
|
||||
db.run("DELETE FROM media WHERE id = ?", [
|
||||
req.params.id
|
||||
], (err: Error) => {
|
||||
if (err) { return next(err); }
|
||||
return res.redirect("/");
|
||||
});
|
||||
} else {
|
||||
console.log(err);
|
||||
return res.redirect("/");
|
||||
}
|
||||
} else {
|
||||
console.log(`Deleted ${path}`);
|
||||
//Callback Hell :D
|
||||
db.run("DELETE FROM media WHERE id = ?", [
|
||||
req.params.id
|
||||
], (err: Error) => {
|
||||
if (err) { return next(err); }
|
||||
return res.redirect("/");
|
||||
});
|
||||
}
|
||||
}));
|
||||
});
|
||||
router.post("/:id(\\d+)/delete", [checkAuth], async (req: Request, res: Response) => {
|
||||
const path: any = await getPath(req.params.id);
|
||||
fs.unlink(`uploads/${path.path}`, async (err) => {
|
||||
if (err && err.errno == -4058) {
|
||||
await deleteId("media", req.params.id).then(()=> {
|
||||
return res.redirect("/");
|
||||
});
|
||||
}
|
||||
await deleteId("media", req.params.id).then(()=> {
|
||||
return res.redirect("/");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -1,9 +1,7 @@
|
|||
import type {MediaRow, UserRow} from '../types';
|
||||
import type {RequestHandler as Middleware, Router, Request, Response} from 'express';
|
||||
import type {RequestHandler as Middleware, NextFunction} from "express";
|
||||
|
||||
import ffmpeg from "fluent-ffmpeg";
|
||||
import ffmpegpath from "@ffmpeg-installer/ffmpeg";
|
||||
// @ts-ignore
|
||||
import ffprobepath from "@ffprobe-installer/ffprobe";
|
||||
ffmpeg.setFfmpegPath(ffmpegpath.path);
|
||||
ffmpeg.setFfprobePath(ffprobepath.path);
|
||||
|
@ -11,121 +9,135 @@ ffmpeg.setFfprobePath(ffprobepath.path);
|
|||
import fs from "fs";
|
||||
import process from "process";
|
||||
|
||||
import db from "../db";
|
||||
import {extension} from "../types/lib";
|
||||
import {db, MediaParams} from "../types/db";
|
||||
|
||||
function extension(str: String){
|
||||
let file = str.split("/").pop();
|
||||
return [file.substr(0,file.lastIndexOf(".")),file.substr(file.lastIndexOf("."),file.length).toLowerCase()];
|
||||
}
|
||||
export const checkAuth: Middleware = (req, res, next) => {
|
||||
if (!req.user) {
|
||||
return res.status(401);
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
//Checks ShareX key
|
||||
export const checkAuth: Middleware = (req: Request, res: Response, next: Function) => {
|
||||
let auth = process.env.EBAPI_KEY || process.env.EBPASS || "pleaseSetAPI_KEY";
|
||||
let key = null;
|
||||
/**Checks shareX auth key */
|
||||
export const checkSharexAuth: Middleware = (req, res, next) => {
|
||||
const auth = process.env.EBAPI_KEY || process.env.EBPASS || "pleaseSetAPI_KEY";
|
||||
let key = null;
|
||||
|
||||
if (req.headers["key"]) {
|
||||
key = req.headers["key"];
|
||||
} else {
|
||||
return res.status(400).send("{success: false, message: \"No key provided\", fix: \"Provide a key\"}");
|
||||
}
|
||||
if (req.headers["key"]) {
|
||||
key = req.headers["key"];
|
||||
} else {
|
||||
return res.status(400).send("{success: false, message: \"No key provided\", fix: \"Provide a key\"}");
|
||||
}
|
||||
|
||||
if (auth != key) {
|
||||
return res.status(401).send("{success: false, message: '\"'Invalid key\", fix: \"Provide a valid key\"}");
|
||||
}
|
||||
if (auth != key) {
|
||||
return res.status(401).send("{success: false, message: '\"'Invalid key\", fix: \"Provide a valid key\"}");
|
||||
}
|
||||
|
||||
let shortKey = key.substr(0, 3) + "...";
|
||||
console.log("Authenicated user with key: " + shortKey);
|
||||
const shortKey = key.substr(0, 3) + "...";
|
||||
console.log(`Authenicated user with key: ${shortKey}`);
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
//Converts mp4 to gif and vice versa with ffmpeg
|
||||
export const convert: Middleware = (req: Request, res: Response, next: Function) => {
|
||||
for (let file in req.files) {
|
||||
// @ts-ignore
|
||||
let nameAndExtension = extension(req.files[file].originalname);
|
||||
let oembed = {
|
||||
type: "video",
|
||||
version: "1.0",
|
||||
provider_name: "embedder",
|
||||
provider_url: "https://github.com/WaveringAna/embedder",
|
||||
cache_age: 86400,
|
||||
html: `<iframe src='${req.protocol}://${req.get("host")}/gifv/${nameAndExtension[0]}${nameAndExtension[1]}'></iframe>`,
|
||||
width: 640,
|
||||
height: 360
|
||||
};
|
||||
|
||||
fs.writeFile(`uploads/oembed-${nameAndExtension[0]}${nameAndExtension[1]}.json`, JSON.stringify(oembed), function (err) {
|
||||
if (err) return next(err);
|
||||
console.log(`oembed file created ${nameAndExtension[0]}${nameAndExtension[1]}.json`);
|
||||
});
|
||||
|
||||
/**if (nameAndExtension[1] == ".mp4") {
|
||||
console.log("Converting " + nameAndExtension[0] + nameAndExtension[1] + " to gif");
|
||||
console.log(nameAndExtension[0] + nameAndExtension[1]);
|
||||
ffmpeg()
|
||||
.input(`uploads/${nameAndExtension[0]}${nameAndExtension[1]}`)
|
||||
.inputFormat("mp4")
|
||||
.outputFormat("gif")
|
||||
.output(`uploads/${nameAndExtension[0]}.gif`)
|
||||
.on("end", function() {
|
||||
console.log("Conversion complete");
|
||||
console.log(`Uploaded to uploads/${nameAndExtension[0]}.gif`);
|
||||
})
|
||||
.on("error", (e) => console.log(e))
|
||||
.run();
|
||||
} else if (nameAndExtension[1] == ".gif") {
|
||||
console.log(`Converting ${nameAndExtension[0]}${nameAndExtension[1]} to mp4`);
|
||||
ffmpeg(`uploads/${nameAndExtension[0]}${nameAndExtension[1]}`)
|
||||
.inputFormat("gif")
|
||||
.outputFormat("mp4")
|
||||
.outputOptions([
|
||||
"-pix_fmt yuv420p",
|
||||
"-c:v libx264",
|
||||
"-movflags +faststart"
|
||||
])
|
||||
.noAudio()
|
||||
.output(`uploads/${nameAndExtension[0]}.mp4`)
|
||||
.on("end", function() {
|
||||
console.log("Conversion complete");
|
||||
console.log(`Uploaded to uploads/${nameAndExtension[0]}.mp4`);
|
||||
})
|
||||
.run();
|
||||
}**/
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
export const handleUpload: Middleware = (req: Request, res: Response, next: Function) => {
|
||||
if (!req.files || Object.keys(req.files).length === 0) {
|
||||
console.log("No files were uploaded");
|
||||
return res.status(400).send("No files were uploaded.");
|
||||
}
|
||||
|
||||
for (let file in req.files) {
|
||||
let currentdate = Date.now();
|
||||
let expireDate: Date;
|
||||
if (req.body.expire) {
|
||||
expireDate = new Date(currentdate + (req.body.expire * 24 * 60 * 60 * 1000));
|
||||
console.log(req.body.expire);
|
||||
console.log(expireDate);
|
||||
} else
|
||||
expireDate = null;
|
||||
// @ts-ignore
|
||||
db.run("INSERT INTO media (path, expire) VALUES (?, ?)", [req.files[file].filename, expireDate], function (err) {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
return next(err);
|
||||
}
|
||||
// @ts-ignore
|
||||
console.log(`Uploaded ${req.files[file].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}`);
|
||||
});
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
next();
|
||||
/**Creates oembed json file for embed metadata */
|
||||
export const createEmbedData: Middleware = (req, res, next) => {
|
||||
const files = req.files as Express.Multer.File[];
|
||||
for (const file in files) {
|
||||
const nameAndExtension = extension(files[file].originalname);
|
||||
const oembed = {
|
||||
type: "video",
|
||||
version: "1.0",
|
||||
provider_name: "embedder",
|
||||
provider_url: "https://github.com/WaveringAna/embedder",
|
||||
cache_age: 86400,
|
||||
html: `<iframe src='${req.protocol}://${req.get("host")}/gifv/${nameAndExtension[0]}${nameAndExtension[1]}'></iframe>`,
|
||||
width: 640,
|
||||
height: 360
|
||||
};
|
||||
|
||||
fs.writeFile(`uploads/oembed-${nameAndExtension[0]}${nameAndExtension[1]}.json`, JSON.stringify(oembed), function (err) {
|
||||
if (err) return next(err);
|
||||
console.log(`oembed file created ${nameAndExtension[0]}${nameAndExtension[1]}.json`);
|
||||
});
|
||||
}
|
||||
next();
|
||||
};
|
||||
/** Converts video to gif and vice versa using ffmpeg */
|
||||
export const convert: Middleware = (req, res, next) => {
|
||||
const files = req.files as Express.Multer.File[];
|
||||
for (const file in files) {
|
||||
const nameAndExtension = extension(files[file].originalname);
|
||||
if (nameAndExtension[1] == ".mp4" || nameAndExtension[1] == ".webm" || nameAndExtension[1] == ".mkv" || nameAndExtension[1] == ".avi" || nameAndExtension[1] == ".mov") {
|
||||
console.log("Converting " + nameAndExtension[0] + nameAndExtension[1] + " to gif");
|
||||
ffmpeg()
|
||||
.input(`uploads/${nameAndExtension[0]}${nameAndExtension[1]}`)
|
||||
.inputFormat(nameAndExtension[1].substring(1))
|
||||
.outputFormat("gif")
|
||||
.output(`uploads/${nameAndExtension[0]}.gif`)
|
||||
.on("end", function() {
|
||||
console.log("Conversion complete");
|
||||
console.log(`Uploaded to uploads/${nameAndExtension[0]}.gif`);
|
||||
})
|
||||
.on("error", (e) => console.log(e))
|
||||
.run();
|
||||
} else if (nameAndExtension[1] == ".gif") {
|
||||
console.log(`Converting ${nameAndExtension[0]}${nameAndExtension[1]} to mp4`);
|
||||
ffmpeg(`uploads/${nameAndExtension[0]}${nameAndExtension[1]}`)
|
||||
.inputFormat("gif")
|
||||
.outputFormat("mp4")
|
||||
.outputOptions([
|
||||
"-pix_fmt yuv420p",
|
||||
"-c:v libx264",
|
||||
"-movflags +faststart"
|
||||
])
|
||||
.noAudio()
|
||||
.output(`uploads/${nameAndExtension[0]}.mp4`)
|
||||
.on("end", function() {
|
||||
console.log("Conversion complete");
|
||||
console.log(`Uploaded to uploads/${nameAndExtension[0]}.mp4`);
|
||||
})
|
||||
.run();
|
||||
}
|
||||
}
|
||||
};
|
||||
/**Middleware for handling uploaded files. Inserts it into the database */
|
||||
export const handleUpload: Middleware = (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 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, next);
|
||||
}
|
||||
} else
|
||||
insertToDB(files.filename, expireDate, username, next);
|
||||
|
||||
next();
|
||||
};
|
||||
/**Inserts into media database */
|
||||
function insertToDB (filename: string, expireDate: Date, username: string, next: NextFunction) {
|
||||
const params: MediaParams = [
|
||||
filename,
|
||||
expireDate,
|
||||
username
|
||||
];
|
||||
|
||||
db.run("INSERT INTO media (path, expire, username) VALUES (?, ?, ?)", params, function (err) {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
return next(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}`);
|
||||
});
|
||||
}
|
12
app/types.ts
12
app/types.ts
|
@ -1,12 +0,0 @@
|
|||
export interface MediaRow {
|
||||
id? : Number,
|
||||
path: String,
|
||||
expire: Number
|
||||
}
|
||||
|
||||
export interface UserRow {
|
||||
id? : Number,
|
||||
username: String,
|
||||
hashed_password: any,
|
||||
salt: any
|
||||
}
|
131
app/types/db.ts
Normal file
131
app/types/db.ts
Normal file
|
@ -0,0 +1,131 @@
|
|||
import sqlite3 from "sqlite3";
|
||||
import mkdirp from "mkdirp";
|
||||
import crypto from "crypto";
|
||||
|
||||
mkdirp.sync("./uploads");
|
||||
mkdirp.sync("./var/db");
|
||||
|
||||
export const db = new sqlite3.Database("./var/db/media.db");
|
||||
|
||||
export function createDatabase(version: number){
|
||||
// create the database schema for the embedders app
|
||||
console.log("Creating database");
|
||||
|
||||
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"));
|
||||
|
||||
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){
|
||||
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("ALTER TABLE users ADD COLUMN expire TEXT", (err) => {
|
||||
if(err) return;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**Inserts a new user to the database */
|
||||
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
|
||||
]);
|
||||
|
||||
resolve(null);
|
||||
});
|
||||
}
|
||||
|
||||
/**Selects a path for a file given ID */
|
||||
export function getPath(id: number | string) {
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**Expires a database row given a Date in unix time */
|
||||
export function expire(database: string, column: string, expiration:number) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const query = `SELECT * FROM ${database} WHERE ${column} < ?`;
|
||||
|
||||
db.each(query, [expiration], async (err: Error, row: GenericRow) => {
|
||||
if (err) reject(err);
|
||||
await deleteId(database, row.id);
|
||||
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**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,
|
||||
username: string,
|
||||
hashed_password: any,
|
||||
salt: any
|
||||
}
|
1
app/types/declarations/ffprobepath.d.ts
vendored
Normal file
1
app/types/declarations/ffprobepath.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
declare module "@ffprobe-installer/ffprobe";
|
19
app/types/lib.ts
Normal file
19
app/types/lib.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/* eslint-disable @typescript-eslint/no-namespace */
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface User {
|
||||
username: string;
|
||||
id?: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
/**Splits a file name into its name and then its extension */
|
||||
export function extension(str: string){
|
||||
const file = str.split("/").pop();
|
||||
return [file.substr(0,file.lastIndexOf(".")),file.substr(file.lastIndexOf("."),file.length).toLowerCase()];
|
||||
}
|
||||
/**Type for user data */
|
||||
export interface User {
|
||||
username: string;
|
||||
id?: string;
|
||||
}
|
72
app/types/multer.ts
Normal file
72
app/types/multer.ts
Normal file
|
@ -0,0 +1,72 @@
|
|||
import {Request} from "express";
|
||||
import multer, {FileFilterCallback} from "multer";
|
||||
|
||||
import {db, MediaRow} from "./db";
|
||||
import {extension} from "./lib";
|
||||
|
||||
export type DestinationCallback = (error: Error | null, destination: string) => void
|
||||
export type FileNameCallback = (error: Error | null, filename: string) => void
|
||||
|
||||
export const fileStorage = multer.diskStorage({
|
||||
destination: (
|
||||
request: Request,
|
||||
file: Express.Multer.File,
|
||||
callback: DestinationCallback
|
||||
): void => {
|
||||
callback(null, __dirname + "/../../uploads");
|
||||
},
|
||||
filename: (
|
||||
request: Request,
|
||||
file: Express.Multer.File,
|
||||
callback: FileNameCallback
|
||||
): 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;
|
||||
|
||||
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]);
|
||||
}
|
||||
} 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]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const allowedMimeTypes = [
|
||||
"image/png",
|
||||
"image/jpg",
|
||||
"image/jpeg",
|
||||
"image/gif",
|
||||
"image/webp",
|
||||
"video/mp4",
|
||||
"video/mov",
|
||||
"video/webm",
|
||||
"audio/mpeg",
|
||||
"audio/ogg"
|
||||
];
|
||||
|
||||
export const fileFilter = (
|
||||
request: Request,
|
||||
file: Express.Multer.File,
|
||||
callback: FileFilterCallback
|
||||
): void => {
|
||||
if (allowedMimeTypes.includes(file.mimetype)) {
|
||||
callback(null, true);
|
||||
} else {
|
||||
callback(null, false);
|
||||
}
|
||||
};
|
37
app/views/adduser.ejs
Normal file
37
app/views/adduser.ejs
Normal file
|
@ -0,0 +1,37 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Embedder</title>
|
||||
<link rel="stylesheet" href="/css/base.css">
|
||||
<link rel="stylesheet" href="/css/index.css">
|
||||
<link rel="stylesheet" href="/css/login.css">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
</head>
|
||||
<body>
|
||||
<section class="prompt">
|
||||
<h3>Embedder</h3>
|
||||
<h1>Add User</h1>
|
||||
<form action="/adduser" method="post">
|
||||
<section>
|
||||
<label for="username">Username</label>
|
||||
<input id="username" name="username" type="text" autocomplete="username" required autofocus>
|
||||
</section>
|
||||
<section>
|
||||
<label for="current-password">Password</label>
|
||||
<input id="current-password" name="password" type="password" autocomplete="current-password" required>
|
||||
</section>
|
||||
<button type="submit">Add User</button>
|
||||
</form>
|
||||
<hr>
|
||||
</section>
|
||||
<footer class="info">
|
||||
<p><a href="https://l.nekomimi.pet/project">Created by Wavering Ana</a></p>
|
||||
<p><a href="https://github.com/WaveringAna/Embedder">Github</a></p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
|
@ -1,105 +1,133 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Embedder</title>
|
||||
<link rel="stylesheet" href="/css/base.css">
|
||||
<link rel="stylesheet" href="/css/index.css">
|
||||
<link rel="stylesheet" href="/css/app.css">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<%
|
||||
function extension(string) {
|
||||
return string.slice((string.lastIndexOf(".") - 2 >>> 0) + 2);
|
||||
}
|
||||
%>
|
||||
</head>
|
||||
<body>
|
||||
<section class="embedderapp">
|
||||
<nav class="nav">
|
||||
<ul>
|
||||
<li class="user"><%= user.name || user.username %></li>
|
||||
<li>
|
||||
<form action="/logout" method="post">
|
||||
<button class="logout" type="submit">Sign out</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<header class="header">
|
||||
<h1>Embedder</h1>
|
||||
<form action="/" method="post" encType="multipart/form-data">
|
||||
<div id="dropArea">
|
||||
<p class="dragregion">Upload a file, copy paste, or drag n' drop into the dashed region</p>
|
||||
<div id="gallery"></div>
|
||||
<p class="dragregion"><input class="" type="file" id="fileupload" name="fileupload"><input type="button" value="Upload" id="submit" onclick="uploadFile()"></p>
|
||||
<br>
|
||||
<br>
|
||||
<p class="dragregion">Select file expiration date: <select name="expire" id="expire">
|
||||
<option value="0.00069">1 minute</option>
|
||||
<option value="0.00347">5 minutes</option>
|
||||
<option value="0.0417">1 hour</option>
|
||||
<option value="0.25">6 hours</option>
|
||||
<option value="1">1 day</option>
|
||||
<option value="7">7 days</option>
|
||||
<option value="14">14 days</option>
|
||||
<option value="30">30 days</option>
|
||||
<option selected value="">never</option>
|
||||
</select></p>
|
||||
<p class="dragregion">Click the file to copy the url</p>
|
||||
</div>
|
||||
</form>
|
||||
</header>
|
||||
<% if (Count > 0) { %>
|
||||
<section class="main">
|
||||
<ul class="embedder-list">
|
||||
<% files.forEach(function(file) { %>
|
||||
<li>
|
||||
<form action="<%= file.url %>" method="post">
|
||||
<div class="view">
|
||||
<% if (extension(file.path) == ".mp4" || extension(file.path) == ".mov" || extension(file.path) == "webp") { %>
|
||||
<div class="video">
|
||||
<video class="image" autoplay loop muted playsinline loading="lazy">
|
||||
<source src="/uploads/<%= file.path %>" loading="lazy">
|
||||
</video>
|
||||
<div class="overlay">
|
||||
<a href="/gifv/<%=file.path %>" onclick="copyA(event)">Copy as GIFv</a>
|
||||
</div>
|
||||
</div>
|
||||
<% } else if (extension(file.path) == ".gif") { %>
|
||||
<div class="video">
|
||||
<img class="image" src="/uploads/<%=file.path %>" width="100%" onclick="copyURI(event);" loading="lazy">
|
||||
<div class="overlay">
|
||||
<a href="/gifv/<%=file.path %>" onclick="copyA(event)">Copy as GIFv</a>
|
||||
</div>
|
||||
</div>
|
||||
<% } else if (extension(file.path) == ".jpg" || extension(file.path) == ".jpeg" || extension(file.path) == ".png" || extension(file.path) == ".gif" || extension(file.path) == ".webp" ) { %>
|
||||
<img class="image" src="/uploads/<%=file.path %>" width="100%" onclick="copyURI(event)" loading="lazy">
|
||||
<% } else {%> <!-- non-media file -->
|
||||
<div class="nonmedia" onclick="copyPath('/uploads/<%=file.path%>')">
|
||||
<p><%=extension(file.path)%> file</p>
|
||||
</div>
|
||||
<% } %>
|
||||
<label><%= file.path %></label>
|
||||
<button class="destroy" form="delete-<%= file.path %>"></button>
|
||||
<button type="button" class="fullsize" onclick="openFullSize('/uploads/<%=file.path%>')"></button>
|
||||
</div>
|
||||
</form>
|
||||
<form name="delete-<%= file.path %>" id="delete-<%= file.path %>" action="<%= file.url %>/delete" method="post">
|
||||
</form>
|
||||
</li>
|
||||
<% }); %>
|
||||
</ul>
|
||||
</section>
|
||||
<% } %>
|
||||
</section>
|
||||
<footer class="info">
|
||||
<p><a href="https://l.nekomimi.pet/project">Created by Wavering Ana</a></p>
|
||||
<p><a href="https://github.com/WaveringAna/Embedder">Github</a></p>
|
||||
</footer>
|
||||
<script src="/js/index.js"></script>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Embedder</title>
|
||||
<link rel="stylesheet" href="/css/base.css">
|
||||
<link rel="stylesheet" href="/css/index.css">
|
||||
<link rel="stylesheet" href="/css/app.css">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<%
|
||||
function extension(string) {
|
||||
return string.slice((string.lastIndexOf(".") - 2 >>> 0) + 2);
|
||||
}
|
||||
%>
|
||||
</head>
|
||||
<body>
|
||||
<section class="embedderapp">
|
||||
<nav class="nav">
|
||||
<ul>
|
||||
<li class="user"><%= user.name || user.username %></li>
|
||||
<li>
|
||||
<form action="/logout" method="post">
|
||||
<button class="logout" type="submit">Sign out</button>
|
||||
</form>
|
||||
</li>
|
||||
<% if (user.name == "admin" || user.username == "admin") { %>
|
||||
<li>
|
||||
<button class="adduser" onclick="location.href='/adduser';">Add user</a></button>
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</nav>
|
||||
<header class="header">
|
||||
<h1>Embedder</h1>
|
||||
<form action="/" method="post" encType="multipart/form-data">
|
||||
<div id="dropArea">
|
||||
<p class="dragregion">Upload a file, copy paste, or drag n' drop into the dashed region</p>
|
||||
<div id="gallery"></div>
|
||||
<p class="dragregion"><input class="" type="file" id="fileupload" name="fileupload"><input type="button" value="Upload" id="submit" onclick="uploadFile()"></p>
|
||||
<br>
|
||||
<br>
|
||||
<p class="dragregion">
|
||||
Select file expiration date:
|
||||
<select name="expire" id="expire">
|
||||
<option value="0.00069">1 minute</option>
|
||||
<option value="0.00347">5 minutes</option>
|
||||
<option value="0.0417">1 hour</option>
|
||||
<option value="0.25">6 hours</option>
|
||||
<option value="1">1 day</option>
|
||||
<option value="7">7 days</option>
|
||||
<option value="14">14 days</option>
|
||||
<option value="30">30 days</option>
|
||||
<option selected value="">never</option>
|
||||
</select>
|
||||
</p>
|
||||
<p class="dragregion">Click the file to copy the url</p>
|
||||
</div>
|
||||
</form>
|
||||
</header>
|
||||
<% if (Count > 0) { %>
|
||||
<section class="main">
|
||||
<ul class="embedder-list">
|
||||
<% files.forEach(function(file) { %>
|
||||
<li>
|
||||
<form action="<%= file.url %>" method="post">
|
||||
<div class="view">
|
||||
<% if (extension(file.path) == ".mp4" || extension(file.path) == ".mov" || extension(file.path) == "webp") { %>
|
||||
<div class="video">
|
||||
<video class="image" autoplay loop muted playsinline loading="lazy">
|
||||
<source src="/uploads/<%= file.path %>" loading="lazy">
|
||||
</video>
|
||||
<div class="overlay">
|
||||
<% if(user.username == "admin" && file.username != "admin") { %>
|
||||
<small class="username"><%= file.username %></small>
|
||||
<br>
|
||||
<% } %>
|
||||
<a href="/gifv/<%=file.path %>" onclick="copyA(event)">Copy as GIFv</a>
|
||||
</div>
|
||||
</div>
|
||||
<% } else if (extension(file.path) == ".gif") { %>
|
||||
<div class="video">
|
||||
<img class="image" src="/uploads/<%=file.path %>" width="100%" onclick="copyURI(event);" loading="lazy">
|
||||
<div class="overlay">
|
||||
<% if(user.username == "admin" && file.username != "admin") { %>
|
||||
<small class="username"><%= file.username %></small>
|
||||
<br>
|
||||
<% } %>
|
||||
<a href="/gifv/<%=file.path %>" onclick="copyA(event)">Copy as GIFv</a>
|
||||
</div>
|
||||
</div>
|
||||
<% } else if (extension(file.path) == ".jpg" || extension(file.path) == ".jpeg" || extension(file.path) == ".png" || extension(file.path) == ".gif" || extension(file.path) == ".webp" ) { %>
|
||||
<div class="video">
|
||||
<img class="image" src="/uploads/<%=file.path %>" width="100%" onclick="copyURI(event)" loading="lazy">
|
||||
<% if(user.username == "admin" && file.username != "admin") { %>
|
||||
<div class="overlay">
|
||||
<small class="username"><%= file.username %></small>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } else {%> <!-- non-media file -->
|
||||
<div class="nonmedia" onclick="copyPath('/uploads/<%=file.path%>')">
|
||||
<p><%=extension(file.path)%> file</p>
|
||||
<% if(user.username == "admin" && file.username != "admin") { %>
|
||||
<div class="overlay">
|
||||
<small class="username"><%= file.username %></small>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } %>
|
||||
<label><%= file.path %></label>
|
||||
<button class="destroy" form="delete-<%= file.path %>"></button>
|
||||
<button type="button" class="fullsize" onclick="openFullSize('/uploads/<%=file.path%>')"></button>
|
||||
</div>
|
||||
</form>
|
||||
<form name="delete-<%= file.path %>" id="delete-<%= file.path %>" action="<%= file.url %>/delete" method="post">
|
||||
</form>
|
||||
</li>
|
||||
<% }); %>
|
||||
</ul>
|
||||
</section>
|
||||
<% } %>
|
||||
</section>
|
||||
<footer class="info">
|
||||
<p><a href="https://l.nekomimi.pet/project">Created by Wavering Ana</a></p>
|
||||
<p><a href="https://github.com/WaveringAna/Embedder">Github</a></p>
|
||||
</footer>
|
||||
<script src="/js/index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -4,6 +4,5 @@ module.exports = defineConfig({
|
|||
e2e: {
|
||||
baseUrl: "http://localhost:3000",
|
||||
},
|
||||
chromeWebSecurity: false,
|
||||
"video": false
|
||||
chromeWebSecurity: false
|
||||
});
|
||||
|
|
1327
package-lock.json
generated
1327
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -26,7 +26,7 @@
|
|||
"copy-files": "copyfiles -a -u 1 app/public/* app/views/* app/public/**/* app/views/**/* dist/",
|
||||
"tsc": "tsc",
|
||||
"postinstall": "npm run tsc && npm run copy-files",
|
||||
"dev": "tsc-node-dev --respawn --pretty --transpile-only index.ts"
|
||||
"build": "npm run clean && npm run copy-files && npm run tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ffmpeg-installer/ffmpeg": "^1.1.0",
|
||||
|
@ -35,7 +35,7 @@
|
|||
"cookie-parser": "~1.4.4",
|
||||
"dotenv": "^8.6.0",
|
||||
"ejs": "^3.1.8",
|
||||
"express": "~4.16.1",
|
||||
"express": "^4.18.2",
|
||||
"express-session": "^1.17.3",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
"http-errors": "~1.6.3",
|
||||
|
@ -59,7 +59,8 @@
|
|||
"@types/passport": "^1.0.11",
|
||||
"@types/passport-local": "^1.0.34",
|
||||
"@types/probe-image-size": "^7.2.0",
|
||||
"@typescript-eslint/parser": "^5.45.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.46.1",
|
||||
"@typescript-eslint/parser": "^5.46.1",
|
||||
"copyfiles": "^2.4.1",
|
||||
"cypress": "^11.1.0",
|
||||
"cypress-file-upload": "^5.0.8",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue