wip
This commit is contained in:
parent
af0e46e552
commit
b924fbd314
|
@ -61,15 +61,20 @@
|
|||
"@babel/core": "^7.12.10",
|
||||
"@babel/preset-env": "^7.12.11",
|
||||
"@nuxt/types": "2.15.7",
|
||||
"@types/adm-zip": "^0.4.34",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/cors": "^2.8.10",
|
||||
"@types/cron": "^1.7.2",
|
||||
"@types/express": "^4.17.11",
|
||||
"@types/express-rate-limit": "^5.1.2",
|
||||
"@types/fluent-ffmpeg": "^2.1.17",
|
||||
"@types/jsonwebtoken": "^8.5.1",
|
||||
"@types/morgan": "^1.9.2",
|
||||
"@types/node": "^14.14.41",
|
||||
"@types/randomstring": "^1.1.6",
|
||||
"@types/sharp": "^0.27.1",
|
||||
"@types/systeminformation": "^3.54.1",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.22.0",
|
||||
"@typescript-eslint/parser": "^4.22.0",
|
||||
"@vue/test-utils": "^1.2.1",
|
||||
|
@ -3797,6 +3802,15 @@
|
|||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/adm-zip": {
|
||||
"version": "0.4.34",
|
||||
"resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.4.34.tgz",
|
||||
"integrity": "sha512-8ToYLLAYhkRfcmmljrKi22gT2pqu7hGMDtORP1emwIEGmgUTZOsaDjzWFzW5N2frcFRz/50CWt4zA1CxJ73pmQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/anymatch": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-3.0.0.tgz",
|
||||
|
@ -3997,6 +4011,15 @@
|
|||
"@types/webpack": "^4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/fluent-ffmpeg": {
|
||||
"version": "2.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.17.tgz",
|
||||
"integrity": "sha512-/bdvjKw/mtBHlJ2370d04nt4CsWqU5MrwS/NtO96V01jxitJ4+iq8OFNcqc5CegeV3TQOK3uueK02kvRK+zjUg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/glob": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz",
|
||||
|
@ -4163,6 +4186,12 @@
|
|||
"integrity": "sha512-0/HnwIfW4ki2D8L8c9GVcG5I72s9jP5GSLVF0VIXDW00kmIpA6O33G7a8n59Tmh7Nz0WUC3rSb7PTY/sdW2JzA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/randomstring": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/randomstring/-/randomstring-1.1.6.tgz",
|
||||
"integrity": "sha512-XRIZIMTxjcUukqQcYBdpFWGbcRDyNBXrvTEtTYgFMIbBNUVt+9mCKsU+jUUDLeFO/RXopUgR5OLiBqbY18vSHQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/range-parser": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz",
|
||||
|
@ -4246,6 +4275,16 @@
|
|||
"integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/systeminformation": {
|
||||
"version": "3.54.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/systeminformation/-/systeminformation-3.54.1.tgz",
|
||||
"integrity": "sha512-vvisj2mdWygyc0jk/5XtSVq9gtxCmF3nrGwv8wVway8pwNRhtPji/MU9dc1L0F6rl0F/NFIHa4ScRU7wmNaHmg==",
|
||||
"deprecated": "This is a stub types definition. systeminformation provides its own type definitions, so you do not need this installed.",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"systeminformation": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/tapable": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.7.tgz",
|
||||
|
@ -4277,6 +4316,12 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/uuid": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.0.tgz",
|
||||
"integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/webpack": {
|
||||
"version": "4.41.29",
|
||||
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.29.tgz",
|
||||
|
@ -28025,6 +28070,15 @@
|
|||
"integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/adm-zip": {
|
||||
"version": "0.4.34",
|
||||
"resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.4.34.tgz",
|
||||
"integrity": "sha512-8ToYLLAYhkRfcmmljrKi22gT2pqu7hGMDtORP1emwIEGmgUTZOsaDjzWFzW5N2frcFRz/50CWt4zA1CxJ73pmQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/anymatch": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-3.0.0.tgz",
|
||||
|
@ -28222,6 +28276,15 @@
|
|||
"@types/webpack": "^4"
|
||||
}
|
||||
},
|
||||
"@types/fluent-ffmpeg": {
|
||||
"version": "2.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.17.tgz",
|
||||
"integrity": "sha512-/bdvjKw/mtBHlJ2370d04nt4CsWqU5MrwS/NtO96V01jxitJ4+iq8OFNcqc5CegeV3TQOK3uueK02kvRK+zjUg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/glob": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz",
|
||||
|
@ -28388,6 +28451,12 @@
|
|||
"integrity": "sha512-0/HnwIfW4ki2D8L8c9GVcG5I72s9jP5GSLVF0VIXDW00kmIpA6O33G7a8n59Tmh7Nz0WUC3rSb7PTY/sdW2JzA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/randomstring": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/randomstring/-/randomstring-1.1.6.tgz",
|
||||
"integrity": "sha512-XRIZIMTxjcUukqQcYBdpFWGbcRDyNBXrvTEtTYgFMIbBNUVt+9mCKsU+jUUDLeFO/RXopUgR5OLiBqbY18vSHQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/range-parser": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz",
|
||||
|
@ -28471,6 +28540,15 @@
|
|||
"integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/systeminformation": {
|
||||
"version": "3.54.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/systeminformation/-/systeminformation-3.54.1.tgz",
|
||||
"integrity": "sha512-vvisj2mdWygyc0jk/5XtSVq9gtxCmF3nrGwv8wVway8pwNRhtPji/MU9dc1L0F6rl0F/NFIHa4ScRU7wmNaHmg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"systeminformation": "*"
|
||||
}
|
||||
},
|
||||
"@types/tapable": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.7.tgz",
|
||||
|
@ -28501,6 +28579,12 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"@types/uuid": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.0.tgz",
|
||||
"integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/webpack": {
|
||||
"version": "4.41.29",
|
||||
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.29.tgz",
|
||||
|
|
|
@ -86,15 +86,20 @@
|
|||
"@babel/core": "^7.12.10",
|
||||
"@babel/preset-env": "^7.12.11",
|
||||
"@nuxt/types": "2.15.7",
|
||||
"@types/adm-zip": "^0.4.34",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/cors": "^2.8.10",
|
||||
"@types/cron": "^1.7.2",
|
||||
"@types/express": "^4.17.11",
|
||||
"@types/express-rate-limit": "^5.1.2",
|
||||
"@types/fluent-ffmpeg": "^2.1.17",
|
||||
"@types/jsonwebtoken": "^8.5.1",
|
||||
"@types/morgan": "^1.9.2",
|
||||
"@types/node": "^14.14.41",
|
||||
"@types/randomstring": "^1.1.6",
|
||||
"@types/sharp": "^0.27.1",
|
||||
"@types/systeminformation": "^3.54.1",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.22.0",
|
||||
"@typescript-eslint/parser": "^4.22.0",
|
||||
"@vue/test-utils": "^1.2.1",
|
||||
|
|
|
@ -7,9 +7,9 @@ import fstatic from 'fastify-static';
|
|||
import path from 'path';
|
||||
import rateLimit from 'fastify-rate-limit';
|
||||
import jetpack from 'fs-jetpack';
|
||||
import cron from 'cron';
|
||||
// import cron from 'cron';
|
||||
// @ts-ignore - nuxt types can't be found - https://github.com/nuxt/nuxt.js/issues/7651
|
||||
import { loadNuxt, build } from 'nuxt';
|
||||
// import { loadNuxt, build } from 'nuxt';
|
||||
|
||||
import Routes from './structures/routes';
|
||||
|
||||
|
@ -103,4 +103,5 @@ const start = async () => {
|
|||
// new cron.CronJob('0 0 * * * *', Util.saveStatsToDb, null, true);
|
||||
};
|
||||
|
||||
export const log = server.log;
|
||||
void start();
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
import type { FastifyRequest, FastifyReply, HookHandlerDoneFunction } from 'fastify';
|
||||
import prisma from '../structures/database';
|
||||
|
||||
export interface RequestWithUser extends FastifyRequest {
|
||||
user: {
|
||||
id: number;
|
||||
username: string | null;
|
||||
isAdmin: boolean | null;
|
||||
};
|
||||
}
|
||||
|
||||
export default async (req: RequestWithUser, res: FastifyReply, next: HookHandlerDoneFunction) => {
|
||||
// TODO: Search for canApiKey in the codebase and add this file as middleware on those, before auth
|
||||
const token = req.headers.token as string;
|
||||
if (!token) return next();
|
||||
const user = await prisma.users.findFirst({
|
||||
where: {
|
||||
apiKey: token
|
||||
}
|
||||
});
|
||||
|
||||
if (!user) return res.status(401).send({ message: 'Invalid authorization' });
|
||||
if (!user.enabled) return res.status(401).send({ message: 'This account has been disabled' });
|
||||
|
||||
req.user = user;
|
||||
next();
|
||||
};
|
|
@ -1,5 +1,6 @@
|
|||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
previewFeatures = ["selectRelationCount"]
|
||||
}
|
||||
|
||||
datasource db {
|
||||
|
@ -13,7 +14,7 @@ model albums {
|
|||
userId Int?
|
||||
name String?
|
||||
zippedAt DateTime?
|
||||
createdAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
editedAt DateTime?
|
||||
nsfw Boolean? @default(false)
|
||||
|
||||
|
@ -22,28 +23,28 @@ model albums {
|
|||
|
||||
model albumsFiles {
|
||||
id Int @id @default(autoincrement())
|
||||
albumId Int?
|
||||
fileId Int?
|
||||
albumId Int
|
||||
fileId Int
|
||||
|
||||
@@unique([albumId, fileId], name: "albumsfiles_albumid_fileid_unique")
|
||||
}
|
||||
|
||||
model albumsLinks {
|
||||
id Int @id @default(autoincrement())
|
||||
albumId Int?
|
||||
linkId Int? @unique
|
||||
albumId Int
|
||||
linkId Int @unique
|
||||
}
|
||||
|
||||
model bans {
|
||||
id Int @id @default(autoincrement())
|
||||
ip String?
|
||||
createdAt DateTime?
|
||||
ip String
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model fileTags {
|
||||
id Int @id @default(autoincrement())
|
||||
fileId Int?
|
||||
tagId Int?
|
||||
fileId Int
|
||||
tagId Int
|
||||
|
||||
@@unique([fileId, tagId], name: "filetags_fileid_tagid_unique")
|
||||
}
|
||||
|
@ -51,26 +52,26 @@ model fileTags {
|
|||
model files {
|
||||
id Int @id @default(autoincrement())
|
||||
userId Int?
|
||||
name String?
|
||||
original String?
|
||||
type String?
|
||||
size Int?
|
||||
hash String?
|
||||
ip String?
|
||||
createdAt DateTime?
|
||||
name String
|
||||
original String
|
||||
type String
|
||||
size Int
|
||||
hash String
|
||||
ip String
|
||||
createdAt DateTime @default(now())
|
||||
editedAt DateTime?
|
||||
}
|
||||
|
||||
model links {
|
||||
id Int @id @default(autoincrement())
|
||||
userId Int?
|
||||
albumId Int?
|
||||
identifier String?
|
||||
views Int?
|
||||
enabled Boolean?
|
||||
enableDownload Boolean?
|
||||
userId Int
|
||||
albumId Int
|
||||
identifier String
|
||||
views Int @default(0)
|
||||
enabled Boolean @default(true)
|
||||
enableDownload Boolean @default(false)
|
||||
expiresAt DateTime?
|
||||
createdAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
editedAt DateTime?
|
||||
|
||||
@@unique([userId, albumId, identifier], name: "links_userid_albumid_identifier_unique")
|
||||
|
@ -78,8 +79,8 @@ model links {
|
|||
|
||||
model settings {
|
||||
id Int @id @default(autoincrement())
|
||||
key String?
|
||||
value String?
|
||||
key String
|
||||
value String
|
||||
}
|
||||
|
||||
model statistics {
|
||||
|
@ -88,17 +89,17 @@ model statistics {
|
|||
type String?
|
||||
// TODO: This was JSON before so make sure to stringify and parse when saving stats
|
||||
data String?
|
||||
createdAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@unique([batchId, type], name: "statistics_batchid_type_unique")
|
||||
}
|
||||
|
||||
model tags {
|
||||
id Int @id @default(autoincrement())
|
||||
uuid String?
|
||||
userId Int?
|
||||
name String?
|
||||
createdAt DateTime?
|
||||
uuid String
|
||||
userId Int
|
||||
name String
|
||||
createdAt DateTime @default(now())
|
||||
editedAt DateTime?
|
||||
|
||||
@@unique([userId, name], name: "tags_userid_name_unique")
|
||||
|
@ -106,14 +107,14 @@ model tags {
|
|||
|
||||
model users {
|
||||
id Int @id @default(autoincrement())
|
||||
username String?
|
||||
password String?
|
||||
enabled Boolean?
|
||||
isAdmin Boolean?
|
||||
username String
|
||||
password String
|
||||
enabled Boolean @default(true)
|
||||
isAdmin Boolean @default(false)
|
||||
apiKey String?
|
||||
passwordEditedAt DateTime?
|
||||
apiKeyEditedAt DateTime?
|
||||
createdAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
editedAt DateTime?
|
||||
|
||||
@@unique([username, apiKey], name: "users_username_apikey_unique")
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
const bcrypt = require('bcrypt');
|
||||
const moment = require('moment');
|
||||
const JWT = require('jsonwebtoken');
|
||||
const Route = require('../../structures/Route');
|
||||
const Util = require('../../utils/Util');
|
||||
|
||||
class loginPOST extends Route {
|
||||
constructor() {
|
||||
super('/auth/login', 'post', { bypassAuth: true });
|
||||
}
|
||||
|
||||
async run(req, res, db) {
|
||||
if (!req.body) return res.status(400).json({ message: 'No body provided' });
|
||||
const { username, password } = req.body;
|
||||
if (!username || !password) return res.status(401).json({ message: 'Invalid body provided' });
|
||||
|
||||
/*
|
||||
Checks if the user exists
|
||||
*/
|
||||
const user = await db.table('users').where('username', username).first();
|
||||
if (!user) return res.status(401).json({ message: 'Invalid authorization' });
|
||||
|
||||
/*
|
||||
Checks if the user is disabled
|
||||
*/
|
||||
if (!user.enabled) return res.status(401).json({ message: 'This account has been disabled' });
|
||||
|
||||
/*
|
||||
Checks if the password is right
|
||||
*/
|
||||
const comparePassword = await bcrypt.compare(password, user.password);
|
||||
if (!comparePassword) return res.status(401).json({ message: 'Invalid authorization.' });
|
||||
|
||||
/*
|
||||
Create the jwt with some data
|
||||
*/
|
||||
const jwt = JWT.sign({
|
||||
iss: 'chibisafe',
|
||||
sub: user.id,
|
||||
iat: moment.utc().valueOf()
|
||||
}, Util.config.secret, { expiresIn: '30d' });
|
||||
|
||||
return res.json({
|
||||
message: 'Successfully logged in.',
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
apiKey: user.apiKey,
|
||||
isAdmin: user.isAdmin
|
||||
},
|
||||
token: jwt,
|
||||
apiKey: user.apiKey
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = loginPOST;
|
|
@ -1,63 +0,0 @@
|
|||
const bcrypt = require('bcrypt');
|
||||
const moment = require('moment');
|
||||
const Route = require('../../structures/Route');
|
||||
const log = require('../../utils/Log');
|
||||
const Util = require('../../utils/Util');
|
||||
|
||||
class registerPOST extends Route {
|
||||
constructor() {
|
||||
super('/auth/register', 'post', { bypassAuth: true });
|
||||
}
|
||||
|
||||
async run(req, res, db) {
|
||||
// Only allow admins to create new accounts if the sign up is deactivated
|
||||
const user = await Util.isAuthorized(req);
|
||||
if ((!user || !user.isAdmin) && !Util.config.userAccounts) return res.status(401).json({ message: 'Creation of new accounts is currently disabled' });
|
||||
|
||||
if (!req.body) return res.status(400).json({ message: 'No body provided' });
|
||||
const { username, password } = req.body;
|
||||
if (!username || !password) return res.status(401).json({ message: 'Invalid body provided' });
|
||||
|
||||
if (username.length < 4 || username.length > 32) {
|
||||
return res.status(400).json({ message: 'Username must have 4-32 characters' });
|
||||
}
|
||||
if (password.length < 6 || password.length > 64) {
|
||||
return res.status(400).json({ message: 'Password must have 6-64 characters' });
|
||||
}
|
||||
|
||||
/*
|
||||
Make sure the username doesn't exist yet
|
||||
*/
|
||||
const exists = await db.table('users').where('username', username).first();
|
||||
if (exists) return res.status(401).json({ message: 'Username already exists' });
|
||||
|
||||
/*
|
||||
Hash the supplied password
|
||||
*/
|
||||
let hash;
|
||||
try {
|
||||
hash = await bcrypt.hash(password, 10);
|
||||
} catch (error) {
|
||||
log.error('Error generating password hash');
|
||||
log.error(error);
|
||||
return res.status(401).json({ message: 'There was a problem processing your account' });
|
||||
}
|
||||
|
||||
/*
|
||||
Create the user
|
||||
*/
|
||||
const now = moment.utc().toDate();
|
||||
await db.table('users').insert({
|
||||
username,
|
||||
password: hash,
|
||||
passwordEditedAt: now,
|
||||
createdAt: now,
|
||||
editedAt: now,
|
||||
enabled: true,
|
||||
isAdmin: false
|
||||
}).wasMutated();
|
||||
return res.json({ message: 'The account was created successfully' });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = registerPOST;
|
|
@ -1,7 +1,52 @@
|
|||
export interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
username: string;
|
||||
password: string;
|
||||
stravaLink: string;
|
||||
enabled: boolean;
|
||||
isAdmin: boolean;
|
||||
apiKey: string;
|
||||
passwordEditedAt: string;
|
||||
apiKeyEditedAt: string;
|
||||
createdAt: string;
|
||||
editedAt: string;
|
||||
}
|
||||
|
||||
export interface File {
|
||||
id: number;
|
||||
userId?: number;
|
||||
name: string;
|
||||
original: string;
|
||||
type: string;
|
||||
size: number;
|
||||
hash: string;
|
||||
ip: string;
|
||||
createdAt: string;
|
||||
editedAt: string;
|
||||
}
|
||||
|
||||
export interface ExtendedFile extends File {
|
||||
url?: string;
|
||||
thumb?: string;
|
||||
thumbSquare?: string;
|
||||
preview?: string;
|
||||
}
|
||||
|
||||
export interface ExtendedFileWithData extends ExtendedFile {
|
||||
data: {
|
||||
hash: string;
|
||||
size: number;
|
||||
filename: string;
|
||||
originalName: string;
|
||||
mimeType: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Album {
|
||||
id: number;
|
||||
userId: number;
|
||||
name: string;
|
||||
zippedAt: string;
|
||||
createdAt: string;
|
||||
editedAt: string;
|
||||
nsfw: boolean;
|
||||
}
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
const chalk = require('chalk');
|
||||
const { dump } = require('dumper.js');
|
||||
|
||||
class Log {
|
||||
static info(args) {
|
||||
if (Log.checkIfArrayOrObject(args)) dump(args);
|
||||
else console.log(args); // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
static success(args) {
|
||||
if (Log.checkIfArrayOrObject(args)) dump(args);
|
||||
else console.log(chalk.green(args)); // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
static warn(args) {
|
||||
if (Log.checkIfArrayOrObject(args)) dump(args);
|
||||
else console.log(chalk.yellow(args)); // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
static error(args) {
|
||||
if (Log.checkIfArrayOrObject(args)) dump(args);
|
||||
else console.log(chalk.red(args)); // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
static debug(args) {
|
||||
if (Log.checkIfArrayOrObject(args)) dump(args);
|
||||
else console.log(chalk.gray(args)); // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
static checkIfArrayOrObject(thing) {
|
||||
if (typeof thing === typeof [] || typeof thing === typeof {}) return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Log;
|
|
@ -1,225 +0,0 @@
|
|||
const si = require('systeminformation');
|
||||
|
||||
class StatsGenerator {
|
||||
// symbols would be better because they're unique, but harder to serialize them
|
||||
static Type = Object.freeze({
|
||||
// should contain key value: number
|
||||
TIME: 'time',
|
||||
// should contain key value: number
|
||||
BYTE: 'byte',
|
||||
// should contain key value: { used: number, total: number }
|
||||
BYTE_USAGE: 'byteUsage',
|
||||
// should contain key data: Array<{ key: string, value: number | string }>
|
||||
// and optionally a count/total
|
||||
DETAILED: 'detailed',
|
||||
// hidden type should be skipped during iteration, can contain anything
|
||||
// these should be treated on a case by case basis on the frontend
|
||||
HIDDEN: 'hidden'
|
||||
});
|
||||
|
||||
static statGenerators = {
|
||||
system: StatsGenerator.getSystemInfo,
|
||||
fileSystems: StatsGenerator.getFileSystemsInfo,
|
||||
uploads: StatsGenerator.getUploadsInfo,
|
||||
users: StatsGenerator.getUsersInfo,
|
||||
albums: StatsGenerator.getAlbumStats
|
||||
};
|
||||
|
||||
static keyOrder = Object.keys(StatsGenerator.statGenerators);
|
||||
|
||||
static async getSystemInfo() {
|
||||
const os = await si.osInfo();
|
||||
|
||||
const currentLoad = await si.currentLoad();
|
||||
const mem = await si.mem();
|
||||
const time = si.time();
|
||||
const nodeUptime = process.uptime();
|
||||
|
||||
return {
|
||||
'Platform': `${os.platform} ${os.arch}`,
|
||||
'Distro': `${os.distro} ${os.release}`,
|
||||
'Kernel': os.kernel,
|
||||
'CPU Load': `${currentLoad.currentload.toFixed(1)}%`,
|
||||
'CPUs Load': currentLoad.cpus.map(cpu => `${cpu.load.toFixed(1)}%`).join(', '),
|
||||
'System Memory': {
|
||||
value: {
|
||||
used: mem.active,
|
||||
total: mem.total
|
||||
},
|
||||
type: StatsGenerator.Type.BYTE_USAGE
|
||||
},
|
||||
'Memory Usage': {
|
||||
value: process.memoryUsage().rss,
|
||||
type: StatsGenerator.Type.BYTE
|
||||
},
|
||||
'System Uptime': {
|
||||
value: time.uptime,
|
||||
type: StatsGenerator.Type.TIME
|
||||
},
|
||||
'Node.js': `${process.versions.node}`,
|
||||
'Service Uptime': {
|
||||
value: Math.floor(nodeUptime),
|
||||
type: StatsGenerator.Type.TIME
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static async getFileSystemsInfo() {
|
||||
const stats = {};
|
||||
|
||||
const fsSize = await si.fsSize();
|
||||
for (const fs of fsSize) {
|
||||
stats[`${fs.fs} (${fs.type}) on ${fs.mount}`] = {
|
||||
value: {
|
||||
total: fs.size,
|
||||
used: fs.used
|
||||
},
|
||||
type: StatsGenerator.Type.BYTE_USAGE
|
||||
};
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
static async getUploadsInfo(db) {
|
||||
const stats = {
|
||||
'Total': 0,
|
||||
'Images': 0,
|
||||
'Videos': 0,
|
||||
'Others': {
|
||||
data: {},
|
||||
count: 0,
|
||||
type: StatsGenerator.Type.DETAILED
|
||||
},
|
||||
'Size in DB': {
|
||||
value: 0,
|
||||
type: StatsGenerator.Type.BYTE
|
||||
}
|
||||
};
|
||||
|
||||
const getFilesCountAndSize = async () => {
|
||||
const uploads = await db.table('files').select('size');
|
||||
|
||||
return {
|
||||
'Total': uploads.length,
|
||||
'Size in DB': {
|
||||
value: uploads.reduce((acc, upload) => acc + parseInt(upload.size, 10), 0),
|
||||
type: StatsGenerator.Type.BYTE
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const getImagesCount = async () => {
|
||||
const Images = await db.table('files')
|
||||
.where('type', 'like', `image/%`)
|
||||
.count('id as count')
|
||||
.then(rows => rows[0].count);
|
||||
|
||||
return { Images };
|
||||
};
|
||||
|
||||
const getVideosCount = async () => {
|
||||
const Videos = await db.table('files')
|
||||
.where('type', 'like', `video/%`)
|
||||
.count('id as count')
|
||||
.then(rows => rows[0].count);
|
||||
|
||||
return { Videos };
|
||||
};
|
||||
|
||||
const getOthersCount = async () => {
|
||||
// rename to key, value from type, count
|
||||
const data = await db.table('files')
|
||||
.select('type as key')
|
||||
.count('id as value')
|
||||
.whereNot('type', 'like', `image/%`)
|
||||
.whereNot('type', 'like', `video/%`)
|
||||
.groupBy('key')
|
||||
.orderBy('value', 'desc');
|
||||
|
||||
const count = data.reduce((acc, val) => acc + val.value, 0);
|
||||
|
||||
return {
|
||||
Others: {
|
||||
data,
|
||||
count,
|
||||
type: StatsGenerator.Type.DETAILED
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const result = await Promise.all([getFilesCountAndSize(), getImagesCount(), getVideosCount(), getOthersCount()]);
|
||||
|
||||
return { ...stats, ...Object.assign({}, ...result) };
|
||||
}
|
||||
|
||||
static async getUsersInfo(db) {
|
||||
const stats = {
|
||||
Total: 0,
|
||||
Admins: 0,
|
||||
Disabled: 0
|
||||
};
|
||||
|
||||
const users = await db.table('users');
|
||||
stats.Total = users.length;
|
||||
|
||||
for (const user of users) {
|
||||
if (!user.enabled) {
|
||||
stats.Disabled++;
|
||||
}
|
||||
|
||||
if (user.isAdmin) {
|
||||
stats.Admins++;
|
||||
}
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
static async getAlbumStats(db) {
|
||||
const stats = {
|
||||
'Total': 0,
|
||||
'NSFW': 0,
|
||||
'Generated archives': 0,
|
||||
'Generated identifiers': 0,
|
||||
'Files in albums': 0
|
||||
};
|
||||
|
||||
const albums = await db.table('albums');
|
||||
stats.Total = albums.length;
|
||||
for (const album of albums) {
|
||||
if (album.nsfw) stats.NSFW++;
|
||||
if (album.zipGeneratedAt) stats['Generated archives']++; // XXX: Bobby checks each after if a zip really exists on the disk. Is it really needed?
|
||||
}
|
||||
|
||||
stats['Generated identifiers'] = await db.table('albumsLinks').count('id as count').then(rows => rows[0].count);
|
||||
stats['Files in albums'] = await db.table('albumsFiles')
|
||||
.whereNotNull('albumId')
|
||||
.count('id as count')
|
||||
.then(rows => rows[0].count);
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
static async getStats(db) {
|
||||
const res = {};
|
||||
|
||||
for (const [name, funct] of Object.entries(StatsGenerator.statGenerators)) {
|
||||
res[name] = await funct(db);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
static async getMissingStats(db, existingStats) {
|
||||
const res = {};
|
||||
|
||||
for (const [name, funct] of Object.entries(StatsGenerator.statGenerators)) {
|
||||
if (existingStats.indexOf(name) === -1) res[name] = await funct(db);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = StatsGenerator;
|
|
@ -0,0 +1,276 @@
|
|||
import si from 'systeminformation';
|
||||
import prisma from '../structures/database';
|
||||
|
||||
export const Type = Object.freeze({
|
||||
// should contain key value: number
|
||||
TIME: 'time',
|
||||
// should contain key value: number
|
||||
BYTE: 'byte',
|
||||
// should contain key value: { used: number, total: number }
|
||||
BYTE_USAGE: 'byteUsage',
|
||||
// should contain key data: Array<{ key: string, value: number | string }>
|
||||
// and optionally a count/total
|
||||
DETAILED: 'detailed',
|
||||
// hidden type should be skipped during iteration, can contain anything
|
||||
// these should be treated on a case by case basis on the frontend
|
||||
HIDDEN: 'hidden'
|
||||
});
|
||||
|
||||
export const getSystemInfo = async () => {
|
||||
const os = await si.osInfo();
|
||||
|
||||
const currentLoad = await si.currentLoad();
|
||||
const mem = await si.mem();
|
||||
const time = si.time();
|
||||
const nodeUptime = process.uptime();
|
||||
|
||||
return {
|
||||
'Platform': `${os.platform} ${os.arch}`,
|
||||
'Distro': `${os.distro} ${os.release}`,
|
||||
'Kernel': os.kernel,
|
||||
'CPU Load': `${currentLoad.currentLoad.toFixed(1)}%`,
|
||||
'CPUs Load': currentLoad.cpus.map(cpu => `${cpu.load.toFixed(1)}%`).join(', '),
|
||||
'System Memory': {
|
||||
value: {
|
||||
used: mem.active,
|
||||
total: mem.total
|
||||
},
|
||||
type: Type.BYTE_USAGE
|
||||
},
|
||||
'Memory Usage': {
|
||||
value: process.memoryUsage().rss,
|
||||
type: Type.BYTE
|
||||
},
|
||||
'System Uptime': {
|
||||
value: time.uptime,
|
||||
type: Type.TIME
|
||||
},
|
||||
'Node.js': `${process.versions.node}`,
|
||||
'Service Uptime': {
|
||||
value: Math.floor(nodeUptime),
|
||||
type: Type.TIME
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const getFileSystemsInfo = async () => {
|
||||
const stats = {};
|
||||
|
||||
const fsSize = await si.fsSize();
|
||||
for (const fs of fsSize) {
|
||||
stats[`${fs.fs} (${fs.type}) on ${fs.mount}`] = {
|
||||
value: {
|
||||
total: fs.size,
|
||||
used: fs.used
|
||||
},
|
||||
type: Type.BYTE_USAGE
|
||||
};
|
||||
}
|
||||
|
||||
return stats;
|
||||
};
|
||||
|
||||
export const getUploadsInfo = async () => {
|
||||
const stats = {
|
||||
'Total': 0,
|
||||
'Images': 0,
|
||||
'Videos': 0,
|
||||
'Others': {
|
||||
data: {},
|
||||
count: 0,
|
||||
type: Type.DETAILED
|
||||
},
|
||||
'Size in DB': {
|
||||
value: 0,
|
||||
type: Type.BYTE
|
||||
}
|
||||
};
|
||||
|
||||
const getFilesCountAndSize = async () => {
|
||||
const uploads = await prisma.files.findMany({
|
||||
select: {
|
||||
size: true
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
'Total': uploads.length,
|
||||
'Size in DB': {
|
||||
value: uploads.reduce((acc, upload) => acc + upload.size, 0),
|
||||
type: Type.BYTE
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const getImagesCount = async () => {
|
||||
const Images = await prisma.files.count({
|
||||
select: {
|
||||
id: true
|
||||
},
|
||||
where: {
|
||||
type: {
|
||||
contains: 'image/'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { Images: Images.id };
|
||||
};
|
||||
|
||||
const getVideosCount = async () => {
|
||||
const Videos = await prisma.files.count({
|
||||
select: {
|
||||
id: true
|
||||
},
|
||||
where: {
|
||||
type: {
|
||||
contains: 'video/'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { Videos: Videos.id };
|
||||
};
|
||||
|
||||
const getOthersCount = async () => {
|
||||
// TODO: This needs additional testing as the returned object is no the same one as
|
||||
// before the typescript rewrite due to Prisma constraints.
|
||||
// https://www.prisma.io/docs/concepts/components/prisma-client/aggregation-grouping-summarizing
|
||||
const data = await prisma.files.findMany({
|
||||
select: {
|
||||
type: true
|
||||
},
|
||||
where: {
|
||||
NOT: [
|
||||
{
|
||||
type: {
|
||||
contains: 'image/'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: {
|
||||
contains: 'video/'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
orderBy: {
|
||||
type: 'desc'
|
||||
}
|
||||
});
|
||||
/*
|
||||
// rename to key, value from type, count
|
||||
const data = await db.table('files')
|
||||
.select('type as key')
|
||||
.count('id as value')
|
||||
.whereNot('type', 'like', `image/%`)
|
||||
.whereNot('type', 'like', `video/%`)
|
||||
.groupBy('key')
|
||||
.orderBy('value', 'desc');
|
||||
|
||||
const count = data.reduce((acc, val) => acc + val.value, 0);
|
||||
|
||||
return {
|
||||
Others: {
|
||||
data,
|
||||
count,
|
||||
type: Type.DETAILED
|
||||
}
|
||||
};
|
||||
*/
|
||||
};
|
||||
|
||||
const result = await Promise.all([getFilesCountAndSize(), getImagesCount(), getVideosCount(), getOthersCount()]);
|
||||
|
||||
return { ...stats, ...Object.assign({}, ...result) };
|
||||
};
|
||||
|
||||
export const getUsersInfo = async () => {
|
||||
const stats = {
|
||||
Total: 0,
|
||||
Admins: 0,
|
||||
Disabled: 0
|
||||
};
|
||||
|
||||
const users = await prisma.users.findMany({
|
||||
select: {
|
||||
enabled: true,
|
||||
isAdmin: true
|
||||
}
|
||||
});
|
||||
|
||||
stats.Total = users.length;
|
||||
|
||||
for (const user of users) {
|
||||
if (!user.enabled) {
|
||||
stats.Disabled++;
|
||||
}
|
||||
|
||||
if (user.isAdmin) {
|
||||
stats.Admins++;
|
||||
}
|
||||
}
|
||||
|
||||
return stats;
|
||||
};
|
||||
|
||||
export const getAlbumStats = async () => {
|
||||
const stats = {
|
||||
'Total': 0,
|
||||
'NSFW': 0,
|
||||
'Generated archives': 0,
|
||||
'Generated identifiers': 0,
|
||||
'Files in albums': 0
|
||||
};
|
||||
|
||||
const albums = await prisma.albums.findMany({
|
||||
select: {
|
||||
nsfw: true,
|
||||
zippedAt: true
|
||||
}
|
||||
});
|
||||
stats.Total = albums.length;
|
||||
for (const album of albums) {
|
||||
if (album.nsfw) stats.NSFW++;
|
||||
if (album.zippedAt) stats['Generated archives']++; // XXX: Bobby checks each after if a zip really exists on the disk. Is it really needed?
|
||||
}
|
||||
|
||||
stats['Generated identifiers'] = await prisma.albumsLinks.count();
|
||||
stats['Files in albums'] = await prisma.albumsFiles.count();
|
||||
|
||||
return stats;
|
||||
};
|
||||
|
||||
// TODO: Finish this as it does nothing rn
|
||||
export const getStats = () => ({});
|
||||
/*
|
||||
export const getStats = async () => {
|
||||
const res = {};
|
||||
|
||||
for (const [name, funct] of Object.entries(statGenerators)) {
|
||||
res[name] = await funct(db);
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getMissingStats = async (db, existingStats) => {
|
||||
const res = {};
|
||||
|
||||
for (const [name, funct] of Object.entries(statGenerators)) {
|
||||
if (existingStats.indexOf(name) === -1) res[name] = await funct(db);
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
*/
|
||||
|
||||
export const statGenerators = {
|
||||
system: getSystemInfo,
|
||||
fileSystems: getFileSystemsInfo,
|
||||
uploads: getUploadsInfo,
|
||||
users: getUsersInfo,
|
||||
albums: getAlbumStats
|
||||
};
|
||||
|
||||
export const keyOrder = Object.keys(statGenerators);
|
|
@ -1,105 +0,0 @@
|
|||
const jetpack = require('fs-jetpack');
|
||||
const path = require('path');
|
||||
const sharp = require('sharp');
|
||||
const ffmpeg = require('fluent-ffmpeg');
|
||||
const previewUtil = require('./videoPreview/FragmentPreview');
|
||||
|
||||
const log = require('./Log');
|
||||
|
||||
class ThumbUtil {
|
||||
static imageExtensions = ['.jpg', '.jpeg', '.gif', '.png', '.webp'];
|
||||
static videoExtensions = ['.webm', '.mp4', '.wmv', '.avi', '.mov'];
|
||||
|
||||
static thumbPath = path.join(__dirname, '../../../', 'uploads', 'thumbs');
|
||||
static squareThumbPath = path.join(__dirname, '../../../', 'uploads', 'thumbs', 'square');
|
||||
static videoPreviewPath = path.join(__dirname, '../../../', 'uploads', 'thumbs', 'preview');
|
||||
|
||||
static generateThumbnails(filename) {
|
||||
if (!filename) return;
|
||||
const ext = path.extname(filename).toLowerCase();
|
||||
const output = `${filename.slice(0, -ext.length)}.webp`;
|
||||
const previewOutput = `${filename.slice(0, -ext.length)}.webm`;
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
if (ThumbUtil.imageExtensions.includes(ext)) return ThumbUtil.generateThumbnailForImage(filename, output);
|
||||
// eslint-disable-next-line max-len
|
||||
if (ThumbUtil.videoExtensions.includes(ext)) return ThumbUtil.generateThumbnailForVideo(filename, previewOutput);
|
||||
return null;
|
||||
}
|
||||
|
||||
static async generateThumbnailForImage(filename, output) {
|
||||
const filePath = path.join(__dirname, '../../../', 'uploads', filename);
|
||||
|
||||
const file = await jetpack.readAsync(filePath, 'buffer');
|
||||
await sharp(file)
|
||||
.resize(64, 64)
|
||||
.toFormat('webp')
|
||||
.toFile(path.join(ThumbUtil.squareThumbPath, output));
|
||||
await sharp(file)
|
||||
.resize(225, null)
|
||||
.toFormat('webp')
|
||||
.toFile(path.join(ThumbUtil.thumbPath, output));
|
||||
}
|
||||
|
||||
static async generateThumbnailForVideo(filename, output) {
|
||||
const filePath = path.join(__dirname, '../../../', 'uploads', filename);
|
||||
|
||||
ffmpeg(filePath)
|
||||
.thumbnail({
|
||||
timestamps: [0],
|
||||
filename: '%b.webp',
|
||||
folder: ThumbUtil.squareThumbPath,
|
||||
size: '64x64'
|
||||
})
|
||||
.on('error', error => log.error(error.message));
|
||||
|
||||
ffmpeg(filePath)
|
||||
.thumbnail({
|
||||
timestamps: [0],
|
||||
filename: '%b.webp',
|
||||
folder: ThumbUtil.thumbPath,
|
||||
size: '150x?'
|
||||
})
|
||||
.on('error', error => log.error(error.message));
|
||||
|
||||
try {
|
||||
await previewUtil({
|
||||
input: filePath,
|
||||
width: 150,
|
||||
output: path.join(ThumbUtil.videoPreviewPath, output)
|
||||
});
|
||||
} catch (e) {
|
||||
log.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
static getFileThumbnail(filename) {
|
||||
if (!filename) return null;
|
||||
const ext = path.extname(filename).toLowerCase();
|
||||
|
||||
const isImage = ThumbUtil.imageExtensions.includes(ext);
|
||||
const isVideo = ThumbUtil.videoExtensions.includes(ext);
|
||||
|
||||
if (isImage) return { thumb: `${filename.slice(0, -ext.length)}.webp` };
|
||||
if (isVideo) {
|
||||
return {
|
||||
thumb: `${filename.slice(0, -ext.length)}.webp`,
|
||||
preview: `${filename.slice(0, -ext.length)}.webm`
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static async removeThumbs({ thumb, preview }) {
|
||||
if (thumb) {
|
||||
await jetpack.removeAsync(path.join(ThumbUtil.thumbPath, thumb));
|
||||
await jetpack.removeAsync(path.join(ThumbUtil.squareThumbPath, thumb));
|
||||
}
|
||||
if (preview) {
|
||||
await jetpack.removeAsync(path.join(ThumbUtil.videoPreviewPath, preview));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ThumbUtil;
|
|
@ -0,0 +1,102 @@
|
|||
import jetpack from 'fs-jetpack';
|
||||
import path from 'path';
|
||||
import sharp from 'sharp';
|
||||
import ffmpeg from 'fluent-ffmpeg';
|
||||
import previewUtil from './videoPreview/FragmentPreview';
|
||||
|
||||
// TODO: Check that importing the log function works for routes and CLI (generateThumbs.ts)
|
||||
import { log } from '../main';
|
||||
|
||||
const imageExtensions = ['.jpg', '.jpeg', '.gif', '.png', '.webp'];
|
||||
const videoExtensions = ['.webm', '.mp4', '.wmv', '.avi', '.mov'];
|
||||
|
||||
const thumbPath = path.join(__dirname, '../../../', 'uploads', 'thumbs');
|
||||
const squareThumbPath = path.join(__dirname, '../../../', 'uploads', 'thumbs', 'square');
|
||||
const videoPreviewPath = path.join(__dirname, '../../../', 'uploads', 'thumbs', 'preview');
|
||||
|
||||
export const generateThumbnails = (filename: string) => {
|
||||
if (!filename) return;
|
||||
const ext = path.extname(filename).toLowerCase();
|
||||
const output = `${filename.slice(0, -ext.length)}.webp`;
|
||||
const previewOutput = `${filename.slice(0, -ext.length)}.webm`;
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
if (imageExtensions.includes(ext)) return generateThumbnailForImage(filename, output);
|
||||
// eslint-disable-next-line max-len
|
||||
if (videoExtensions.includes(ext)) return generateThumbnailForVideo(filename, previewOutput);
|
||||
return null;
|
||||
};
|
||||
|
||||
const generateThumbnailForImage = async (filename: string, output: string) => {
|
||||
const filePath = path.join(__dirname, '../../../', 'uploads', filename);
|
||||
|
||||
const file = await jetpack.readAsync(filePath, 'buffer');
|
||||
await sharp(file)
|
||||
.resize(64, 64)
|
||||
.toFormat('webp')
|
||||
.toFile(path.join(squareThumbPath, output));
|
||||
await sharp(file)
|
||||
.resize(225, null)
|
||||
.toFormat('webp')
|
||||
.toFile(path.join(thumbPath, output));
|
||||
};
|
||||
|
||||
const generateThumbnailForVideo = async (filename: string, output: string) => {
|
||||
const filePath = path.join(__dirname, '../../../', 'uploads', filename);
|
||||
|
||||
ffmpeg(filePath)
|
||||
.thumbnail({
|
||||
timestamps: [0],
|
||||
filename: '%b.webp',
|
||||
folder: squareThumbPath,
|
||||
size: '64x64'
|
||||
})
|
||||
.on('error', error => log.error(error.message));
|
||||
|
||||
ffmpeg(filePath)
|
||||
.thumbnail({
|
||||
timestamps: [0],
|
||||
filename: '%b.webp',
|
||||
folder: thumbPath,
|
||||
size: '150x?'
|
||||
})
|
||||
.on('error', error => log.error(error.message));
|
||||
|
||||
try {
|
||||
await previewUtil({
|
||||
input: filePath,
|
||||
width: 150,
|
||||
output: path.join(videoPreviewPath, output)
|
||||
});
|
||||
} catch (e) {
|
||||
log.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
export const getFileThumbnail = (filename: string) => {
|
||||
if (!filename) return null;
|
||||
const ext = path.extname(filename).toLowerCase();
|
||||
|
||||
const isImage = imageExtensions.includes(ext);
|
||||
const isVideo = videoExtensions.includes(ext);
|
||||
|
||||
if (isImage) return { thumb: `${filename.slice(0, -ext.length)}.webp` };
|
||||
if (isVideo) {
|
||||
return {
|
||||
thumb: `${filename.slice(0, -ext.length)}.webp`,
|
||||
preview: `${filename.slice(0, -ext.length)}.webm`
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const removeThumbs = async ({ thumb, preview }: { thumb?: string; preview?: string }) => {
|
||||
if (thumb) {
|
||||
await jetpack.removeAsync(path.join(thumbPath, thumb));
|
||||
await jetpack.removeAsync(path.join(squareThumbPath, thumb));
|
||||
}
|
||||
if (preview) {
|
||||
await jetpack.removeAsync(path.join(videoPreviewPath, preview));
|
||||
}
|
||||
};
|
|
@ -1,419 +0,0 @@
|
|||
/* eslint-disable no-await-in-loop */
|
||||
const jetpack = require('fs-jetpack');
|
||||
const randomstring = require('randomstring');
|
||||
const path = require('path');
|
||||
const JWT = require('jsonwebtoken');
|
||||
const db = require('../structures/Database');
|
||||
const moment = require('moment');
|
||||
const Zip = require('adm-zip');
|
||||
const uuidv4 = require('uuid/v4');
|
||||
|
||||
const log = require('./Log');
|
||||
const ThumbUtil = require('./ThumbUtil');
|
||||
const StatsGenerator = require('./StatsGenerator');
|
||||
|
||||
const preserveExtensions = ['.tar.gz', '.tar.z', '.tar.bz2', '.tar.lzma', '.tar.lzo', '.tar.xz'];
|
||||
|
||||
class Util {
|
||||
static uploadPath = path.join(__dirname, '../../../', 'uploads');
|
||||
static statsLastSavedTime = null;
|
||||
static _config = null;
|
||||
|
||||
static get config() {
|
||||
if (this._config) return this._config;
|
||||
return (async () => {
|
||||
if (this._config === null) {
|
||||
const conf = await db('settings').select('key', 'value');
|
||||
this._config = conf.reduce((obj, item) => (
|
||||
// eslint-disable-next-line no-sequences
|
||||
obj[item.key] = typeof item.value === 'string' || item.value instanceof String ? JSON.parse(item.value) : item.value, obj
|
||||
), {});
|
||||
}
|
||||
return this._config;
|
||||
})();
|
||||
}
|
||||
|
||||
static invalidateConfigCache() {
|
||||
this._config = null;
|
||||
}
|
||||
|
||||
static getEnvironmentDefaults() {
|
||||
return {
|
||||
domain: process.env.DOMAIN,
|
||||
routePrefix: '/api',
|
||||
rateLimitWindow: process.env.RATE_LIMIT_WINDOW || 2,
|
||||
rateLimitMax: process.env.RATE_LIMIT_MAX || 5,
|
||||
secret: process.env.SECRET || randomstring.generate(64),
|
||||
serviceName: process.env.SERVICE_NAME || 'change-me',
|
||||
chunkSize: process.env.CHUNK_SIZE || 90,
|
||||
maxSize: process.env.MAX_SIZE || 5000,
|
||||
// eslint-disable-next-line eqeqeq
|
||||
generateZips: process.env.GENERATE_ZIPS == undefined ? true : false,
|
||||
generatedFilenameLength: process.env.GENERATED_FILENAME_LENGTH || 12,
|
||||
generatedAlbumLength: process.env.GENERATED_ALBUM_LENGTH || 6,
|
||||
blockedExtensions: process.env.BLOCKED_EXTENSIONS ? process.env.BLOCKED_EXTENSIONS.split(',') : ['.jar', '.exe', '.msi', '.com', '.bat', '.cmd', '.scr', '.ps1', '.sh'],
|
||||
// eslint-disable-next-line eqeqeq
|
||||
publicMode: process.env.PUBLIC_MODE == undefined ? true : false,
|
||||
// eslint-disable-next-line eqeqeq
|
||||
userAccounts: process.env.USER_ACCOUNTS == undefined ? true : false,
|
||||
metaThemeColor: process.env.META_THEME_COLOR || '#20222b',
|
||||
metaDescription: process.env.META_DESCRIPTION || 'Blazing fast file uploader and bunker written in node! 🚀',
|
||||
metaKeywords: process.env.META_KEYWORDS || 'chibisafe,lolisafe,upload,uploader,file,vue,images,ssr,file uploader,free',
|
||||
metaTwitterHandle: process.env.META_TWITTER_HANDLE || '@your-handle',
|
||||
backgroundImageURL: process.env.BACKGROUND_IMAGE_URL || '',
|
||||
logoURL: process.env.LOGO_URL || '',
|
||||
statisticsCron: process.env.STATISTICS_CRON || '0 0 * * * *',
|
||||
enabledStatistics: process.env.ENABLED_STATISTICS ? process.env.ENABLED_STATISTICS.split(',') : ['system', 'fileSystems', 'uploads', 'users', 'albums'],
|
||||
savedStatistics: process.env.SAVED_STATISTICS ? process.env.SAVED_STATISTICS.split(',') : ['system', 'fileSystems', 'uploads', 'users', 'albums']
|
||||
};
|
||||
}
|
||||
|
||||
static async wipeConfigDb() {
|
||||
try {
|
||||
await db.table('settings').del();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
static async writeConfigToDb(config, wipe = false) {
|
||||
// TODO: Check that the config passes the joi schema validation
|
||||
if (!config || !config.key) return;
|
||||
try {
|
||||
config.value = JSON.stringify(config.value);
|
||||
await db.table('settings').insert(config);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
this.invalidateConfigCache();
|
||||
}
|
||||
}
|
||||
|
||||
static uuid() {
|
||||
return uuidv4();
|
||||
}
|
||||
|
||||
static isExtensionBlocked(extension) {
|
||||
return this.config.blockedExtensions.includes(extension);
|
||||
}
|
||||
|
||||
static getMimeFromType(fileTypeMimeObj) {
|
||||
return fileTypeMimeObj ? fileTypeMimeObj.mime : undefined;
|
||||
}
|
||||
|
||||
static constructFilePublicLink(req, file) {
|
||||
/*
|
||||
TODO: This wont work without a reverse proxy serving both
|
||||
the site and the API under the same domain. Pls fix.
|
||||
*/
|
||||
const host = this.getHost(req);
|
||||
file.url = `${host}/${file.name}`;
|
||||
const { thumb, preview } = ThumbUtil.getFileThumbnail(file.name) || {};
|
||||
if (thumb) {
|
||||
file.thumb = `${host}/thumbs/${thumb}`;
|
||||
file.thumbSquare = `${host}/thumbs/square/${thumb}`;
|
||||
file.preview = preview && `${host}/thumbs/preview/${preview}`;
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
static getUniqueFilename(extension) {
|
||||
const retry = (i = 0) => {
|
||||
const filename = randomstring.generate({
|
||||
length: this.config.generatedFilenameLength,
|
||||
capitalization: 'lowercase'
|
||||
}) + extension;
|
||||
|
||||
// TODO: Change this to look for the file in the db instead of in the filesystem
|
||||
const exists = jetpack.exists(path.join(Util.uploadPath, filename));
|
||||
if (!exists) return filename;
|
||||
if (i < 5) return retry(i + 1);
|
||||
log.error('Couldnt allocate identifier for file');
|
||||
return null;
|
||||
};
|
||||
return retry();
|
||||
}
|
||||
|
||||
static getUniqueAlbumIdentifier() {
|
||||
const retry = async (i = 0) => {
|
||||
const identifier = randomstring.generate({
|
||||
length: this.config.generatedAlbumLength,
|
||||
capitalization: 'lowercase'
|
||||
});
|
||||
const exists = await db
|
||||
.table('links')
|
||||
.where({ identifier })
|
||||
.first();
|
||||
if (!exists) return identifier;
|
||||
/*
|
||||
It's funny but if you do i++ the asignment never gets done resulting in an infinite loop
|
||||
*/
|
||||
if (i < 5) return retry(i + 1);
|
||||
log.error('Couldnt allocate identifier for album');
|
||||
return null;
|
||||
};
|
||||
return retry();
|
||||
}
|
||||
|
||||
static getFilenameFromPath(fullPath) {
|
||||
return fullPath.replace(/^.*[\\\/]/, ''); // eslint-disable-line no-useless-escape
|
||||
}
|
||||
|
||||
static async deleteFile(filename, deleteFromDB = false) {
|
||||
const thumbName = ThumbUtil.getFileThumbnail(filename);
|
||||
try {
|
||||
await jetpack.removeAsync(path.join(Util.uploadPath, filename));
|
||||
if (thumbName) await ThumbUtil.removeThumbs(thumbName);
|
||||
|
||||
if (deleteFromDB) {
|
||||
await db
|
||||
.table('files')
|
||||
.where('name', filename)
|
||||
.delete()
|
||||
.wasMutated();
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`There was an error removing the file < ${filename} >`);
|
||||
log.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
static async deleteAllFilesFromAlbum(id) {
|
||||
try {
|
||||
const fileAlbums = await db.table('albumsFiles').where({ albumId: id });
|
||||
for (const fileAlbum of fileAlbums) {
|
||||
const file = await db
|
||||
.table('files')
|
||||
.where({ id: fileAlbum.fileId })
|
||||
.first();
|
||||
|
||||
if (!file) continue;
|
||||
|
||||
await this.deleteFile(file.name, true);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
static async deleteAllFilesFromUser(id) {
|
||||
try {
|
||||
const files = await db.table('files').where({ userId: id });
|
||||
for (const file of files) {
|
||||
await this.deleteFile(file.name, true);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
static async deleteAllFilesFromTag(id) {
|
||||
try {
|
||||
const fileTags = await db.table('fileTags').where({ tagId: id });
|
||||
for (const fileTag of fileTags) {
|
||||
const file = await db
|
||||
.table('files')
|
||||
.where({ id: fileTag.fileId })
|
||||
.first();
|
||||
if (!file) continue;
|
||||
await this.deleteFile(file.name, true);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
static async isAuthorized(req) {
|
||||
if (req.headers.token) {
|
||||
const user = await db.table('users').where({ apiKey: req.headers.token }).first();
|
||||
if (!user || !user.enabled) return false;
|
||||
return user;
|
||||
}
|
||||
|
||||
if (!req.headers.authorization) return false;
|
||||
const token = req.headers.authorization.split(' ')[1];
|
||||
if (!token) return false;
|
||||
|
||||
return JWT.verify(token, this.config.secret, async (error, decoded) => {
|
||||
if (error) {
|
||||
log.error(error);
|
||||
return false;
|
||||
}
|
||||
const id = decoded ? decoded.sub : '';
|
||||
const iat = decoded ? decoded.iat : '';
|
||||
|
||||
const user = await db
|
||||
.table('users')
|
||||
.where({ id })
|
||||
.first();
|
||||
if (!user || !user.enabled) return false;
|
||||
if (iat && iat < moment(user.passwordEditedAt).format('x')) return false;
|
||||
|
||||
return user;
|
||||
});
|
||||
}
|
||||
|
||||
static createZip(files, album) {
|
||||
try {
|
||||
const zip = new Zip();
|
||||
for (const file of files) {
|
||||
zip.addLocalFile(path.join(Util.uploadPath, file));
|
||||
}
|
||||
zip.writeZip(
|
||||
path.join(__dirname, '../../../', 'uploads', 'zips', `${album.userId}-${album.id}.zip`)
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
static generateThumbnails = ThumbUtil.generateThumbnails;
|
||||
|
||||
static async fileExists(req, res, exists, filename) {
|
||||
exists = Util.constructFilePublicLink(req, exists);
|
||||
res.json({
|
||||
message: 'Successfully uploaded the file.',
|
||||
name: exists.name,
|
||||
hash: exists.hash,
|
||||
size: exists.size,
|
||||
url: exists.url,
|
||||
thumb: exists.thumb,
|
||||
deleteUrl: `${this.getHost(req)}/api/file/${exists.id}`,
|
||||
repeated: true
|
||||
});
|
||||
|
||||
return this.deleteFile(filename);
|
||||
}
|
||||
|
||||
static async storeFileToDb(req, res, user, file, db) {
|
||||
const dbFile = await db.table('files')
|
||||
// eslint-disable-next-line func-names
|
||||
.where(function() {
|
||||
if (user === undefined) {
|
||||
this.whereNull('userId');
|
||||
} else {
|
||||
this.where('userId', user.id);
|
||||
}
|
||||
})
|
||||
.where({
|
||||
hash: file.data.hash,
|
||||
size: file.data.size
|
||||
})
|
||||
.first();
|
||||
|
||||
if (dbFile) {
|
||||
await this.fileExists(req, res, dbFile, file.data.filename);
|
||||
return;
|
||||
}
|
||||
|
||||
const now = moment.utc().toDate();
|
||||
const data = {
|
||||
userId: user ? user.id : null,
|
||||
name: file.data.filename,
|
||||
original: file.data.originalname,
|
||||
type: file.data.mimetype,
|
||||
size: file.data.size,
|
||||
hash: file.data.hash,
|
||||
ip: req.ip,
|
||||
createdAt: now,
|
||||
editedAt: now
|
||||
};
|
||||
Util.generateThumbnails(file.data.filename);
|
||||
|
||||
let fileId;
|
||||
if (process.env.DB_CLIENT === 'sqlite3') {
|
||||
fileId = await db.table('files').insert(data).wasMutated();
|
||||
} else {
|
||||
fileId = await db.table('files').insert(data, 'id').wasMutated();
|
||||
}
|
||||
|
||||
return {
|
||||
file: data,
|
||||
id: fileId
|
||||
};
|
||||
}
|
||||
|
||||
static async saveFileToAlbum(db, albumId, insertedId) {
|
||||
if (!albumId) return;
|
||||
|
||||
const now = moment.utc().toDate();
|
||||
try {
|
||||
await db.table('albumsFiles').insert({ albumId, fileId: insertedId[0] }).wasMutated();
|
||||
await db.table('albums').where('id', albumId).update('editedAt', now);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
static getExtension(filename) {
|
||||
// Always return blank string if the filename does not seem to have a valid extension
|
||||
// Files such as .DS_Store (anything that starts with a dot, without any extension after) will still be accepted
|
||||
if (!/\../.test(filename)) return '';
|
||||
|
||||
let lower = filename.toLowerCase(); // due to this, the returned extname will always be lower case
|
||||
let multi = '';
|
||||
let extname = '';
|
||||
|
||||
// check for multi-archive extensions (.001, .002, and so on)
|
||||
if (/\.\d{3}$/.test(lower)) {
|
||||
multi = lower.slice(lower.lastIndexOf('.') - lower.length);
|
||||
lower = lower.slice(0, lower.lastIndexOf('.'));
|
||||
}
|
||||
|
||||
// check against extensions that must be preserved
|
||||
for (const extPreserve of preserveExtensions) {
|
||||
if (lower.endsWith(extPreserve)) {
|
||||
extname = extPreserve;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!extname) {
|
||||
extname = lower.slice(lower.lastIndexOf('.') - lower.length); // path.extname(lower)
|
||||
}
|
||||
|
||||
return extname + multi;
|
||||
}
|
||||
|
||||
// TODO: Allow choosing what to save to db and what stats we care about in general
|
||||
// TODO: if a stat is not saved to db but selected to be shows on the dashboard, it will be generated during the request
|
||||
static async saveStatsToDb(force) {
|
||||
// If there were no changes since the instance started, don't generate new stats
|
||||
// OR
|
||||
// if we alredy saved a stats to the db, and there were no new changes to the db since then
|
||||
// skip generating and saving new stats.
|
||||
if (!force &&
|
||||
(!db.userParams.lastMutationTime ||
|
||||
(Util.statsLastSavedTime && Util.statsLastSavedTime > db.userParams.lastMutationTime)
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = moment.utc().toDate();
|
||||
const stats = await StatsGenerator.getStats(db);
|
||||
|
||||
let batchId = 1;
|
||||
|
||||
const res = (await db('statistics').max({ lastBatch: 'batchId' }))[0];
|
||||
if (res && res.lastBatch) {
|
||||
batchId = res.lastBatch + 1;
|
||||
}
|
||||
|
||||
try {
|
||||
for (const [type, data] of Object.entries(stats)) {
|
||||
await db.table('statistics').insert({ type, data: JSON.stringify(data), createdAt: now, batchId });
|
||||
}
|
||||
|
||||
Util.statsLastSavedTime = now.getTime();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
static getHost(req) {
|
||||
return `${req.protocol}://${req.headers.host}`;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Util;
|
|
@ -0,0 +1,392 @@
|
|||
import jetpack from 'fs-jetpack';
|
||||
import randomstring from 'randomstring';
|
||||
import path from 'path';
|
||||
import prisma from '../structures/database';
|
||||
import moment from 'moment';
|
||||
import Zip from 'adm-zip';
|
||||
import { generateThumbnails, getFileThumbnail, removeThumbs } from './ThumbUtil';
|
||||
import { getStats } from './StatsGenerator';
|
||||
import type { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import type { File, ExtendedFile, ExtendedFileWithData, Album, User } from '../structures/interfaces';
|
||||
|
||||
// TODO: Check that importing the log function works for routes and CLI (generateThumbs.ts)
|
||||
import { log } from '../main';
|
||||
|
||||
export { generateThumbnails } from './ThumbUtil';
|
||||
export { v4 as uuid } from 'uuid';
|
||||
|
||||
const preserveExtensions = ['.tar.gz', '.tar.z', '.tar.bz2', '.tar.lzma', '.tar.lzo', '.tar.xz'];
|
||||
|
||||
export const uploadPath = path.join(__dirname, '../../../', 'uploads');
|
||||
export const statsLastSavedTime = null;
|
||||
export const _config = null;
|
||||
|
||||
export const getConfig = async () => {
|
||||
const config = await prisma.settings.findMany();
|
||||
return config.reduce((conf, item) => {
|
||||
if (typeof item.value === 'string') {
|
||||
conf[item.key] = JSON.parse(item.value);
|
||||
} else {
|
||||
conf[item.key] = item.value;
|
||||
}
|
||||
return config;
|
||||
}, {} as Record<string, any>);
|
||||
};
|
||||
|
||||
export const getEnvironmentDefaults = () => ({
|
||||
domain: process.env.DOMAIN,
|
||||
routePrefix: '/api',
|
||||
rateLimitWindow: process.env.RATE_LIMIT_WINDOW ?? 2,
|
||||
rateLimitMax: process.env.RATE_LIMIT_MAX ?? 5,
|
||||
secret: process.env.SECRET ?? randomstring.generate(64),
|
||||
serviceName: process.env.SERVICE_NAME ?? 'change-me',
|
||||
chunkSize: process.env.CHUNK_SIZE ?? 90,
|
||||
maxSize: process.env.MAX_SIZE ?? 5000,
|
||||
// eslint-disable-next-line eqeqeq
|
||||
generateZips: process.env.GENERATE_ZIPS == undefined ? true : false,
|
||||
generatedFilenameLength: process.env.GENERATED_FILENAME_LENGTH ?? 12,
|
||||
generatedAlbumLength: process.env.GENERATED_ALBUM_LENGTH ?? 6,
|
||||
blockedExtensions: process.env.BLOCKED_EXTENSIONS ? process.env.BLOCKED_EXTENSIONS.split(',') : ['.jar', '.exe', '.msi', '.com', '.bat', '.cmd', '.scr', '.ps1', '.sh'],
|
||||
// eslint-disable-next-line eqeqeq
|
||||
publicMode: process.env.PUBLIC_MODE == undefined ? true : false,
|
||||
// eslint-disable-next-line eqeqeq
|
||||
userAccounts: process.env.USER_ACCOUNTS == undefined ? true : false,
|
||||
metaThemeColor: process.env.META_THEME_COLOR ?? '#20222b',
|
||||
metaDescription: process.env.META_DESCRIPTION ?? 'Blazing fast file uploader and bunker written in node! 🚀',
|
||||
metaKeywords: process.env.META_KEYWORDS ?? 'chibisafe,lolisafe,upload,uploader,file,vue,images,ssr,file uploader,free',
|
||||
metaTwitterHandle: process.env.META_TWITTER_HANDLE ?? '@your-handle',
|
||||
backgroundImageURL: process.env.BACKGROUND_IMAGE_URL ?? '',
|
||||
logoURL: process.env.LOGO_URL ?? '',
|
||||
statisticsCron: process.env.STATISTICS_CRON ?? '0 0 * * * *',
|
||||
enabledStatistics: process.env.ENABLED_STATISTICS ? process.env.ENABLED_STATISTICS.split(',') : ['system', 'fileSystems', 'uploads', 'users', 'albums'],
|
||||
savedStatistics: process.env.SAVED_STATISTICS ? process.env.SAVED_STATISTICS.split(',') : ['system', 'fileSystems', 'uploads', 'users', 'albums']
|
||||
});
|
||||
|
||||
export const wipeConfigDb = async () => {
|
||||
try {
|
||||
await prisma.settings.deleteMany();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const writeConfigToDb = async (config: { key: string; value: string }) => {
|
||||
if (!config.key) return;
|
||||
try {
|
||||
config.value = JSON.stringify(config.value);
|
||||
await prisma.settings.create({
|
||||
data: config
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const isExtensionBlocked = async (extension: string) => (await getConfig()).blockedExtensions.includes(extension);
|
||||
export const getMimeFromType = (fileTypeMimeObj: Record<string, null>) => fileTypeMimeObj.mime;
|
||||
|
||||
export const constructFilePublicLink = (req: FastifyRequest, file: File) => {
|
||||
/*
|
||||
TODO: This wont work without a reverse proxy serving both
|
||||
the site and the API under the same domain. Pls fix.
|
||||
*/
|
||||
const extended = file as ExtendedFile;
|
||||
const host = getHost(req);
|
||||
extended.url = `${host}/${extended.name}`;
|
||||
const { thumb, preview } = getFileThumbnail(extended.name) ?? {};
|
||||
if (thumb) {
|
||||
extended.thumb = `${host}/thumbs/${thumb}`;
|
||||
extended.thumbSquare = `${host}/thumbs/square/${thumb}`;
|
||||
extended.preview = preview && `${host}/thumbs/preview/${preview}`;
|
||||
}
|
||||
return extended;
|
||||
};
|
||||
|
||||
export const getUniqueFilename = (extension: string) => {
|
||||
const retry: any = async (i = 0) => {
|
||||
const filename = randomstring.generate({
|
||||
length: (await getConfig()).generatedFilenameLength,
|
||||
capitalization: 'lowercase'
|
||||
}) + extension;
|
||||
|
||||
// TODO: Change this to look for the file in the db instead of in the filesystem
|
||||
const exists = jetpack.exists(path.join(uploadPath, filename));
|
||||
if (!exists) return filename;
|
||||
if (i < 5) return retry(i + 1);
|
||||
log.error('Couldnt allocate identifier for file');
|
||||
return null;
|
||||
};
|
||||
return retry();
|
||||
};
|
||||
|
||||
export const getUniqueAlbumIdentifier = () => {
|
||||
const retry: any = async (i = 0) => {
|
||||
const identifier = randomstring.generate({
|
||||
length: (await getConfig()).generatedAlbumLength,
|
||||
capitalization: 'lowercase'
|
||||
});
|
||||
const exists = await prisma.links.findFirst({
|
||||
where: {
|
||||
identifier
|
||||
}
|
||||
});
|
||||
if (!exists) return identifier;
|
||||
/*
|
||||
It's funny but if you do i++ the asignment never gets done resulting in an infinite loop
|
||||
*/
|
||||
if (i < 5) return retry(i + 1);
|
||||
log.error('Couldnt allocate identifier for album');
|
||||
return null;
|
||||
};
|
||||
return retry();
|
||||
};
|
||||
|
||||
export const getFilenameFromPath = (fullPath: string) => fullPath.replace(/^.*[\\\/]/, ''); // eslint-disable-line no-useless-escape
|
||||
|
||||
export const deleteFile = async (filename: string, deleteFromDB = false) => {
|
||||
const thumbName = getFileThumbnail(filename);
|
||||
try {
|
||||
await jetpack.removeAsync(path.join(uploadPath, filename));
|
||||
if (thumbName) await removeThumbs(thumbName);
|
||||
|
||||
if (deleteFromDB) {
|
||||
await prisma.files.deleteMany({
|
||||
where: {
|
||||
name: filename
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`There was an error removing the file < ${filename} >`);
|
||||
log.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteAllFilesFromAlbum = async (id: number) => {
|
||||
try {
|
||||
const fileAlbums = await prisma.albumsFiles.findMany({
|
||||
where: {
|
||||
albumId: id
|
||||
},
|
||||
select: {
|
||||
fileId: true
|
||||
}
|
||||
});
|
||||
|
||||
for (const fileAlbum of fileAlbums) {
|
||||
const file = await prisma.files.findUnique({
|
||||
where: {
|
||||
id: fileAlbum.fileId
|
||||
}
|
||||
});
|
||||
|
||||
if (!file?.name) continue;
|
||||
|
||||
await deleteFile(file.name, true);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteAllFilesFromUser = async (id: number) => {
|
||||
try {
|
||||
const files = await prisma.files.findMany({
|
||||
where: {
|
||||
userId: id
|
||||
}
|
||||
});
|
||||
|
||||
for (const file of files) {
|
||||
await deleteFile(file.name, true);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteAllFilesFromTag = async (id: number) => {
|
||||
try {
|
||||
const fileTags = await prisma.fileTags.findMany({
|
||||
where: {
|
||||
tagId: id
|
||||
}
|
||||
});
|
||||
for (const fileTag of fileTags) {
|
||||
const file = await prisma.files.findFirst({
|
||||
where: {
|
||||
id: fileTag.fileId
|
||||
},
|
||||
select: {
|
||||
name: true
|
||||
}
|
||||
});
|
||||
if (!file) continue;
|
||||
await deleteFile(file.name, true);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const createZip = (files: string[], album: Album) => {
|
||||
try {
|
||||
const zip = new Zip();
|
||||
for (const file of files) {
|
||||
zip.addLocalFile(path.join(uploadPath, file));
|
||||
}
|
||||
zip.writeZip(
|
||||
path.join(__dirname, '../../../', 'uploads', 'zips', `${album.userId}-${album.id}.zip`)
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const fileExists = (req: FastifyRequest, res: FastifyReply, exists: File, filename: string) => {
|
||||
const file = constructFilePublicLink(req, exists);
|
||||
void res.send({
|
||||
message: 'Successfully uploaded the file.',
|
||||
name: file.name,
|
||||
hash: file.hash,
|
||||
size: file.size,
|
||||
url: file.url,
|
||||
thumb: file.thumb,
|
||||
deleteUrl: `${getHost(req)}/api/file/${file.id}`,
|
||||
repeated: true
|
||||
});
|
||||
|
||||
return deleteFile(filename);
|
||||
};
|
||||
|
||||
export const storeFileToDb = async (req: FastifyRequest, res: FastifyReply, user: User, file: ExtendedFileWithData) => {
|
||||
const dbFile = await prisma.files.findFirst({
|
||||
where: {
|
||||
hash: file.data.hash,
|
||||
size: file.data.size,
|
||||
userId: user.id ? user.id : undefined
|
||||
}
|
||||
});
|
||||
|
||||
if (dbFile) {
|
||||
await fileExists(req, res, dbFile, file.data.filename);
|
||||
return;
|
||||
}
|
||||
|
||||
const now = moment.utc().toDate();
|
||||
const data = {
|
||||
userId: user.id ? user.id : undefined,
|
||||
name: file.data.filename,
|
||||
original: file.data.originalName,
|
||||
type: file.data.mimeType,
|
||||
size: file.data.size,
|
||||
hash: file.data.hash,
|
||||
ip: req.ip,
|
||||
createdAt: now,
|
||||
editedAt: now
|
||||
};
|
||||
void generateThumbnails(file.data.filename);
|
||||
|
||||
const fileId = await prisma.files.create({
|
||||
data
|
||||
});
|
||||
|
||||
return {
|
||||
file: data,
|
||||
id: fileId.id
|
||||
};
|
||||
};
|
||||
|
||||
export const saveFileToAlbum = async (albumId: number, insertedId: number) => {
|
||||
if (!albumId) return;
|
||||
|
||||
const now = moment.utc().toDate();
|
||||
try {
|
||||
await prisma.albumsFiles.create({
|
||||
data: {
|
||||
albumId,
|
||||
fileId: insertedId
|
||||
}
|
||||
});
|
||||
await prisma.albums.update({
|
||||
where: {
|
||||
id: albumId
|
||||
},
|
||||
data: {
|
||||
editedAt: now
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const getExtension = (filename: string) => {
|
||||
// Always return blank string if the filename does not seem to have a valid extension
|
||||
// Files such as .DS_Store (anything that starts with a dot, without any extension after) will still be accepted
|
||||
if (!/\../.test(filename)) return '';
|
||||
|
||||
let lower = filename.toLowerCase(); // due to this, the returned extname will always be lower case
|
||||
let multi = '';
|
||||
let extname = '';
|
||||
|
||||
// check for multi-archive extensions (.001, .002, and so on)
|
||||
if (/\.\d{3}$/.test(lower)) {
|
||||
multi = lower.slice(lower.lastIndexOf('.') - lower.length);
|
||||
lower = lower.slice(0, lower.lastIndexOf('.'));
|
||||
}
|
||||
|
||||
// check against extensions that must be preserved
|
||||
for (const extPreserve of preserveExtensions) {
|
||||
if (lower.endsWith(extPreserve)) {
|
||||
extname = extPreserve;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!extname) {
|
||||
extname = lower.slice(lower.lastIndexOf('.') - lower.length); // path.extname(lower)
|
||||
}
|
||||
|
||||
return extname + multi;
|
||||
};
|
||||
|
||||
/*
|
||||
// TODO: Allow choosing what to save to db and what stats we care about in general
|
||||
// TODO: if a stat is not saved to db but selected to be shows on the dashboard, it will be generated during the request
|
||||
export const saveStatsToDb = async (force: boolean) => {
|
||||
// If there were no changes since the instance started, don't generate new stats
|
||||
// OR
|
||||
// if we alredy saved a stats to the db, and there were no new changes to the db since then
|
||||
// skip generating and saving new stats.
|
||||
if (!force &&
|
||||
(!db.userParams.lastMutationTime ||
|
||||
(Util.statsLastSavedTime && Util.statsLastSavedTime > db.userParams.lastMutationTime)
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = moment.utc().toDate();
|
||||
const stats = await getStats(db);
|
||||
|
||||
let batchId = 1;
|
||||
|
||||
const res = (await db('statistics').max({ lastBatch: 'batchId' }))[0];
|
||||
if (res && res.lastBatch) {
|
||||
batchId = res.lastBatch + 1;
|
||||
}
|
||||
|
||||
try {
|
||||
for (const [type, data] of Object.entries(stats)) {
|
||||
await db.table('statistics').insert({ type, data: JSON.stringify(data), createdAt: now, batchId });
|
||||
}
|
||||
|
||||
Util.statsLastSavedTime = now.getTime();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
*/
|
||||
|
||||
export const getHost = (req: FastifyRequest) => `${req.protocol}://${req.headers.host as string}`;
|
|
@ -1,17 +1,17 @@
|
|||
require('dotenv').config();
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ThumbUtil = require('./ThumbUtil');
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { generateThumbnails } from './ThumbUtil';
|
||||
|
||||
const start = async () => {
|
||||
const files = fs.readdirSync(path.join(__dirname, '../../../uploads'));
|
||||
for (const fileName of files) {
|
||||
console.log(`Generating thumb for '${fileName}`);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await ThumbUtil.generateThumbnails(fileName);
|
||||
await generateThumbnails(fileName);
|
||||
}
|
||||
};
|
||||
|
||||
start();
|
||||
void start();
|
|
@ -1,18 +1,20 @@
|
|||
require('dotenv').config();
|
||||
const blake3 = require('blake3');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const db = require('knex')({
|
||||
client: 'sqlite3',
|
||||
connection: {
|
||||
filename: path.join(__dirname, '../../../database/', 'database.sqlite')
|
||||
}
|
||||
});
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
|
||||
import blake3 from 'blake3';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import prisma from '../structures/database';
|
||||
|
||||
const start = async () => {
|
||||
const hrstart = process.hrtime();
|
||||
const uploads = await db.table('files')
|
||||
.select('id', 'name', 'hash');
|
||||
const uploads = await prisma.files.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
hash: true
|
||||
}
|
||||
});
|
||||
console.log(`Uploads : ${uploads.length}`);
|
||||
|
||||
|
||||
|
@ -26,21 +28,30 @@ const start = async () => {
|
|||
|
||||
for (const upload of uploads) {
|
||||
await new Promise((resolve, reject) => {
|
||||
// TODO: Check that this return works as a continue and doesnt break the loop
|
||||
if (!upload.name) return;
|
||||
fs.createReadStream(path.join(__dirname, '../../../uploads', upload.name))
|
||||
.on('error', reject)
|
||||
.pipe(blake3.createHash())
|
||||
.on('error', reject)
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
.on('data', async source => {
|
||||
const hash = source.toString('hex');
|
||||
console.log(`${upload.name}: ${hash}`);
|
||||
await db.table('files')
|
||||
.update('hash', hash)
|
||||
.where('id', upload.id);
|
||||
const hash = source.toStrin('hex');
|
||||
console.log(`${upload.name as string}: ${hash as string}`);
|
||||
await prisma.files.update({
|
||||
where: {
|
||||
id: upload.id
|
||||
},
|
||||
data: {
|
||||
hash
|
||||
}
|
||||
});
|
||||
done++;
|
||||
resolve();
|
||||
// TODO: Removed parenthesis here, check if it still works as expected
|
||||
resolve;
|
||||
});
|
||||
}).catch(error => {
|
||||
console.log(`${upload.name}: ${error.toString()}`);
|
||||
console.log(`${upload.name as string}: ${error.toString() as string}`);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -52,4 +63,4 @@ const start = async () => {
|
|||
process.exit(0);
|
||||
};
|
||||
|
||||
start();
|
||||
void start();
|
|
@ -1,10 +1,11 @@
|
|||
/* eslint-disable no-bitwise */
|
||||
const ffmpeg = require('fluent-ffmpeg');
|
||||
const probe = require('ffmpeg-probe');
|
||||
import ffmpeg from 'fluent-ffmpeg';
|
||||
// @ts-ignore - no typings for this package
|
||||
import probe from 'ffmpeg-probe';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
const noop = () => {};
|
||||
|
||||
const getRandomInt = (min, max) => {
|
||||
const getRandomInt = (min: number, max: number) => {
|
||||
const minInt = Math.ceil(min);
|
||||
const maxInt = Math.floor(max);
|
||||
|
||||
|
@ -12,7 +13,7 @@ const getRandomInt = (min, max) => {
|
|||
return Math.floor(Math.random() * (maxInt - minInt + 1) + minInt);
|
||||
};
|
||||
|
||||
const getStartTime = (vDuration, fDuration, ignoreBeforePercent, ignoreAfterPercent) => {
|
||||
const getStartTime = (vDuration: number, fDuration: number, ignoreBeforePercent: number, ignoreAfterPercent: number) => {
|
||||
// by subtracting the fragment duration we can be sure that the resulting
|
||||
// start time + fragment duration will be less than the video duration
|
||||
const safeVDuration = vDuration - fDuration;
|
||||
|
@ -25,7 +26,7 @@ const getStartTime = (vDuration, fDuration, ignoreBeforePercent, ignoreAfterPerc
|
|||
return getRandomInt(ignoreBeforePercent * safeVDuration, ignoreAfterPercent * safeVDuration);
|
||||
};
|
||||
|
||||
module.exports = async opts => {
|
||||
export default async (opts: any) => {
|
||||
const {
|
||||
log = noop,
|
||||
|
||||
|
@ -48,7 +49,7 @@ module.exports = async opts => {
|
|||
|
||||
const startTime = getStartTime(duration, fragmentDurationSecond, ignoreBeforePercent, ignoreAfterPercent);
|
||||
|
||||
const result = { startTime, duration };
|
||||
const result: any = { startTime, duration };
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
let scale = null;
|
||||
|
@ -56,15 +57,15 @@ module.exports = async opts => {
|
|||
if (width && height) {
|
||||
result.width = width | 0;
|
||||
result.height = height | 0;
|
||||
scale = `scale=${width}:${height}`;
|
||||
scale = `scale=${width as number}:${height as number}`;
|
||||
} else if (width) {
|
||||
result.width = width | 0;
|
||||
result.height = ((info.height * width) / info.width) | 0;
|
||||
scale = `scale=${width}:-1`;
|
||||
scale = `scale=${width as number}:-1`;
|
||||
} else if (height) {
|
||||
result.height = height | 0;
|
||||
result.width = ((info.width * height) / info.height) | 0;
|
||||
scale = `scale=-1:${height}`;
|
||||
scale = `scale=-1:${height as number}`;
|
||||
} else {
|
||||
result.width = info.width;
|
||||
result.height = info.height;
|
||||
|
@ -75,10 +76,10 @@ module.exports = async opts => {
|
|||
.inputOptions([`-ss ${startTime}`])
|
||||
.outputOptions(['-vsync', 'vfr'])
|
||||
.outputOptions(['-q:v', quality, '-vf', scale])
|
||||
.outputOptions([`-t ${fragmentDurationSecond}`])
|
||||
.outputOptions([`-t ${fragmentDurationSecond as number}`])
|
||||
.noAudio()
|
||||
.output(output)
|
||||
.on('start', cmd => log && log({ cmd }))
|
||||
.on('start', cmd => log?.({ cmd }))
|
||||
.on('end', resolve)
|
||||
.on('error', reject)
|
||||
.run();
|
|
@ -1,10 +1,12 @@
|
|||
/* eslint-disable no-bitwise */
|
||||
const ffmpeg = require('fluent-ffmpeg');
|
||||
const probe = require('ffmpeg-probe');
|
||||
import ffmpeg from 'fluent-ffmpeg';
|
||||
// @ts-ignore - no typings for this package
|
||||
import probe from 'ffmpeg-probe';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
const noop = () => {};
|
||||
|
||||
module.exports = async opts => {
|
||||
module.exports = async (opts: any) => {
|
||||
const {
|
||||
log = noop,
|
||||
|
||||
|
@ -22,7 +24,7 @@ module.exports = async opts => {
|
|||
const info = await probe(input);
|
||||
// const numFramesTotal = parseInt(info.streams[0].nb_frames, 10);
|
||||
const { avg_frame_rate: avgFrameRate, duration } = info.streams[0];
|
||||
const [frames, time] = avgFrameRate.split('/').map(e => parseInt(e, 10));
|
||||
const [frames, time] = avgFrameRate.split('/').map((e: string) => parseInt(e, 10));
|
||||
|
||||
const numFramesTotal = (frames / time) * duration;
|
||||
|
||||
|
@ -30,26 +32,26 @@ module.exports = async opts => {
|
|||
numFramesToCapture = Math.max(1, Math.min(numFramesTotal, numFramesToCapture)) | 0;
|
||||
const nthFrame = (numFramesTotal / numFramesToCapture) | 0;
|
||||
|
||||
const result = {
|
||||
const result: any = {
|
||||
output,
|
||||
numFrames: numFramesToCapture
|
||||
};
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
let scale = null;
|
||||
|
||||
if (width && height) {
|
||||
result.width = width | 0;
|
||||
result.height = height | 0;
|
||||
scale = `scale=${width}:${height}`;
|
||||
scale = `scale=${width as number}:${height as number}`;
|
||||
} else if (width) {
|
||||
result.width = width | 0;
|
||||
result.height = ((info.height * width) / info.width) | 0;
|
||||
scale = `scale=${width}:-1`;
|
||||
scale = `scale=${width as number}:-1`;
|
||||
} else if (height) {
|
||||
result.height = height | 0;
|
||||
result.width = ((info.width * height) / info.height) | 0;
|
||||
scale = `scale=-1:${height}`;
|
||||
scale = `scale=-1:${height as number}`;
|
||||
} else {
|
||||
result.width = info.width;
|
||||
result.height = info.height;
|
||||
|
@ -63,7 +65,7 @@ module.exports = async opts => {
|
|||
.noAudio()
|
||||
.outputFormat('webm')
|
||||
.output(output)
|
||||
.on('start', cmd => log && log({ cmd }))
|
||||
.on('start', cmd => log?.({ cmd }))
|
||||
.on('end', () => resolve())
|
||||
.on('error', err => reject(err))
|
||||
.run();
|
Loading…
Reference in New Issue