This commit is contained in:
Pitu 2018-09-16 00:39:58 -03:00
parent 51e4d6182a
commit 868f4a64ec
47 changed files with 0 additions and 5954 deletions

14
.gitignore vendored
View File

@ -1,14 +0,0 @@
.DS_Store
!.gitkeep
node_modules/
uploads/
logs/
database/db
config.js
start.json
npm-debug.log
pages/custom/**
migrate.js
yarn.lock
package-lock.json
.vscode/

View File

@ -1,19 +0,0 @@
FROM node:9
LABEL name "lolisafe"
LABEL version "3.0.0"
LABEL maintainer "iCrawl <icrawltogo@gmail.com>"
WORKDIR /usr/src/lolisafe
COPY package.json yarn.lock ./
RUN sh -c 'echo "deb http://www.deb-multimedia.org jessie main" >> /etc/apt/sources.list' \
&& apt-key adv --keyserver keyring.debian.org --recv-keys 5C808C2B65558117 \
&& apt-get update \
&& apt-get install -y ffmpeg graphicsmagick \
&& yarn install
COPY . .
CMD ["node", "lolisafe.js"]

21
LICENSE
View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2018 Pitu
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,80 +0,0 @@
![lolisafe](public/images/fb_share.png)
[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](https://raw.githubusercontent.com/kanadeko/Kuro/master/LICENSE)
[![Chat / Support](https://img.shields.io/badge/Chat%20%2F%20Support-discord-7289DA.svg?style=flat-square)](https://discord.gg/5g6vgwn)
# lolisafe, a small safe worth protecting.
## What's new in v3.0.0
- Backend rewrite to make it faster, better and easier to extend
- Album downloads (Thanks to [PascalTemel](https://github.com/PascalTemel))
- See releases for changelog
If you're upgrading from a version prior to v3.0.0 make sure to run **ONCE** `node database/migration.js` to create the missing columns on the database.
## Running
1. Ensure you have at least version 7.6.0 of node installed
2. Clone the repo
3. Rename `config.sample.js` to `config.js`
4. Modify port, domain and privacy options if desired
5. run `npm install` to install all dependencies
6. run `pm2 start lolisafe.js` or `node lolisafe.js` to start the service
## Getting started
This service supports running both as public and private. The only difference is that one needs a token to upload and the other one doesn't. If you want it to be public so anyone can upload files either from the website or API, just set the option `private: false` in the `config.js` file. In case you want to run it privately, you should set `private: true`.
Upon running the service for the first time, it's gonna create a user account with the username `root` and password `root`. This is your admin account and you should change the password immediately. This account will let you manage all uploaded files and remove any if necessary.
The option `serveFilesWithNode` in the `config.js` dictates if you want lolisafe to serve the files or nginx/apache once they are uploaded. The main difference between the two is the ease of use and the chance of analytics in the future.
If you set it to `true`, the uploaded files will be located after the host like:
https://lolisafe.moe/yourFile.jpg
If you set it to `false`, you need to set nginx to directly serve whatever folder it is you are serving your
downloads in. This also gives you the ability to serve them, for example, like this:
https://files.lolisafe.moe/yourFile.jpg
Both cases require you to type the domain where the files will be served on the `domain` key below.
Which one you use is ultimately up to you. Either way, I've provided a sample config files for nginx that you can use to set it up quickly and painlessly!
- [Normal Version](https://github.com/WeebDev/lolisafe/blob/master/nginx.sample.conf)
- [SSL Version](https://github.com/WeebDev/lolisafe/blob/master/nginx-ssl.sample.conf)
If you set `enableUserAccounts: true`, people will be able to create accounts on the service to keep track of their uploaded files and create albums to upload stuff to, pretty much like imgur does, but only through the API. Every user account has a token that the user can use to upload stuff through the API. You can find this token on the section called `Change your token` on the administration dashboard, and if it gets leaked or compromised you can renew it by clicking the button titled `Request new token`.
## Cloudflare Support
If you are running lolisafe behind Cloudflare there is support to make the NGINX logs have the users IP instead of Cloudflares IP. You will need to compile NGINX from source with `--with-http_realip_module` as well as uncomment the following line in the NGINX config: `include /path/to/lolisafe/real-ip-from-cf;`
## Using lolisafe
Once the service starts you can start hitting the upload endpoint at `/api/upload` with any file. If you're using the frontend to do so then you are pretty much set, but if using the API to upload make sure the form name is set to `files[]` and the form type to `multipart/form-data`. If the service is running in private mode, dont forget to send a header of type `token: YOUR-CLIENT-TOKEN` to validate the request.
A sample of the returning json from the endpoint can be seen below:
```json
{
"name": "EW7C.png",
"size": "71400",
"url": "https://i.kanacchi.moe/EW7C.png"
}
```
To make it easier and better than any other service, you can download [our Chrome extension](https://chrome.google.com/webstore/detail/lolisafe-uploader/enkkmplljfjppcdaancckgilmgoiofnj) that will let you configure your hostname and tokens, so that you can simply `right click` -> `loli-safe` -> `send to safe` to any image/audio/video file on the web.
Because of how nodejs apps work, if you want it attached to a domain name you will need to make a reverse proxy for it. Here is a tutorial [on how to do this with nginx](https://www.digitalocean.com/community/tutorials/how-to-set-up-a-node-js-application-for-production-on-ubuntu-16-04). Keep in mind that this is only a requirement if you want to access your lolisafe service by using a domain name, otherwise you can use the service just fine by accessing it from your server's IP.
## Sites using lolisafe
- [lolisafe.moe](https://lolisafe.moe): A small safe worth protecting.
- [safe.fiery.me](https://safe.fiery.me): Just another clone.
- [kayo.moe](https://kayo.moe): File hosting for all~
- [dmca.gripe](https://dmca.gripe): a dmca-resistant, permanent file hosting service.
- [succmy.wang](https://succmy.wang): A private clone with a funny name
- [namir.in](https://namir.in): A private clone dedicated to best girl.
- [safe.waliant.pw](https://safe.waliant.pw): A generic private clone for personal use.
- [a.hyper.lol](https://a.hyper.lol): My personal clone with some ~~terrible~~ great changes.
- [discordjs.moe](https://discordjs.moe): A まじ卍 as fuck copy of lolisafe.moe
- [i.liich.me](https://i.liich.me): Another private clone with a different look.
- [discordimages.com](https://discordimages.com): A file hosting service that is available for public use.
- Feel free to add yours here.
## Author
**lolisafe** © [Pitu](https://github.com/Pitu), Released under the [MIT](https://github.com/WeebDev/lolisafe/blob/master/LICENSE) License.<br>
Authored and maintained by Pitu.
> [lolisafe.moe](https://lolisafe.moe) · GitHub [@Pitu](https://github.com/Pitu)

View File

@ -1,103 +0,0 @@
module.exports = {
/*
If set to true the user will need to specify the auto-generated token
on each API call, meaning random strangers wont be able to use the service
unless they have the token lolisafe provides you with.
If it's set to false, then upload will be public for anyone to use.
*/
private: true,
// If true, users will be able to create accounts and access their uploaded files
enableUserAccounts: true,
/*
Here you can decide if you want lolisafe to serve the files or if you prefer doing so via nginx.
The main difference between the two is the ease of use and the chance of analytics in the future.
If you set it to `true`, the uploaded files will be located after the host like:
https://lolisafe.moe/yourFile.jpg
If you set it to `false`, you need to set nginx to directly serve whatever folder it is you are serving your
downloads in. This also gives you the ability to serve them, for example, like this:
https://files.lolisafe.moe/yourFile.jpg
Both cases require you to type the domain where the files will be served on the `domain` key below.
Which one you use is ultimately up to you.
*/
serveFilesWithNode: false,
domain: 'https://lolisafe.moe',
// Port on which to run the server
port: 9999,
// Pages to process for the frontend
pages: ['home', 'auth', 'dashboard', 'faq'],
// Add file extensions here which should be blocked
blockedExtensions: [
'.jar',
'.exe',
'.msi',
'.com',
'.bat',
'.cmd',
'.nt',
'.scr',
'.ps1',
'.psm1',
'.sh',
'.bash',
'.bsh',
'.csh',
'.bash_profile',
'.bashrc',
'.profile'
],
// Uploads config
uploads: {
// Folder where images should be stored
folder: 'uploads',
/*
Max file size allowed. Needs to be in MB
Note: When maxSize is greater than 1 MiB, you must set the client_max_body_size to the same as maxSize.
*/
maxSize: '512MB',
// The length of the random generated name for the uploaded files
fileLength: 32,
/*
This option will limit how many times it will try to generate random names
for uploaded files. If this value is higher than 1, it will help in cases
where files with the same name already exists (higher chance with shorter file name length).
*/
maxTries: 1,
/*
NOTE: Thumbnails are only for the admin panel and they require you
to install a separate binary called graphicsmagick (http://www.graphicsmagick.org)
for images and ffmpeg (https://ffmpeg.org/) for video files
*/
generateThumbnails: false,
/*
Allows users to download a .zip file of all files in an album.
The file is generated when the user clicks the download button in the view
and is re-used if the album has not changed between download requests
*/
generateZips: true
},
// Folder where to store logs
logsFolder: 'logs',
// The following values shouldn't be touched
database: {
client: 'sqlite3',
connection: { filename: './database/db' },
useNullAsDefault: true
}
}

View File

@ -1,179 +0,0 @@
const config = require('../config.js');
const db = require('knex')(config.database);
const randomstring = require('randomstring');
const utils = require('./utilsController.js');
const path = require('path');
const fs = require('fs');
const Zip = require('jszip');
const albumsController = {};
albumsController.list = async (req, res, next) => {
const user = await utils.authorize(req, res);
const fields = ['id', 'name'];
if (req.params.sidebar === undefined) {
fields.push('timestamp');
fields.push('identifier');
}
const albums = await db.table('albums').select(fields).where({ enabled: 1, userid: user.id });
if (req.params.sidebar !== undefined) {
return res.json({ success: true, albums });
}
let ids = [];
for (let album of albums) {
album.date = new Date(album.timestamp * 1000);
album.date = utils.getPrettyDate(album.date);
album.identifier = `${config.domain}/a/${album.identifier}`;
ids.push(album.id);
}
const files = await db.table('files').whereIn('albumid', ids).select('albumid');
const albumsCount = {};
for (let id of ids) albumsCount[id] = 0;
for (let file of files) albumsCount[file.albumid] += 1;
for (let album of albums) album.files = albumsCount[album.id];
return res.json({ success: true, albums });
};
albumsController.create = async (req, res, next) => {
const user = await utils.authorize(req, res);
const name = req.body.name;
if (name === undefined || name === '') {
return res.json({ success: false, description: 'No album name specified' });
}
const album = await db.table('albums').where({
name: name,
enabled: 1,
userid: user.id
}).first();
if (album) {
return res.json({ success: false, description: 'There\'s already an album with that name' });
}
await db.table('albums').insert({
name: name,
enabled: 1,
userid: user.id,
identifier: randomstring.generate(8),
timestamp: Math.floor(Date.now() / 1000)
});
return res.json({ success: true });
};
albumsController.delete = async (req, res, next) => {
const user = await utils.authorize(req, res);
const id = req.body.id;
if (id === undefined || id === '') {
return res.json({ success: false, description: 'No album specified' });
}
await db.table('albums').where({ id: id, userid: user.id }).update({ enabled: 0 });
return res.json({ success: true });
};
albumsController.rename = async (req, res, next) => {
const user = await utils.authorize(req, res);
const id = req.body.id;
if (id === undefined || id === '') {
return res.json({ success: false, description: 'No album specified' });
}
const name = req.body.name;
if (name === undefined || name === '') {
return res.json({ success: false, description: 'No name specified' });
}
const album = await db.table('albums').where({ name: name, userid: user.id }).first();
if (album) {
return res.json({ success: false, description: 'Name already in use' });
}
await db.table('albums').where({ id: id, userid: user.id }).update({ name: name });
return res.json({ success: true });
};
albumsController.get = async (req, res, next) => {
const identifier = req.params.identifier;
if (identifier === undefined) return res.status(401).json({ success: false, description: 'No identifier provided' });
const album = await db.table('albums').where({ identifier, enabled: 1 }).first();
if (!album) return res.json({ success: false, description: 'Album not found' });
const title = album.name;
const files = await db.table('files').select('name').where('albumid', album.id).orderBy('id', 'DESC');
for (let file of files) {
file.file = `${config.domain}/${file.name}`;
const ext = path.extname(file.name).toLowerCase();
if (utils.imageExtensions.includes(ext) || utils.videoExtensions.includes(ext)) {
file.thumb = `${config.domain}/thumbs/${file.name.slice(0, -ext.length)}.png`;
}
}
return res.json({
success: true,
title: title,
count: files.length,
files
});
};
albumsController.generateZip = async (req, res, next) => {
const identifier = req.params.identifier;
if (identifier === undefined) return res.status(401).json({ success: false, description: 'No identifier provided' });
if (!config.uploads.generateZips) return res.status(401).json({ success: false, description: 'Zip generation disabled' });
const album = await db.table('albums').where({ identifier, enabled: 1 }).first();
if (!album) return res.json({ success: false, description: 'Album not found' });
if (album.zipGeneratedAt > album.editedAt) {
const filePath = path.join(config.uploads.folder, 'zips', `${identifier}.zip`);
const fileName = `${album.name}.zip`;
return res.download(filePath, fileName);
} else {
console.log(`Generating zip for album identifier: ${identifier}`);
const files = await db.table('files').select('name').where('albumid', album.id);
if (files.length === 0) return res.json({ success: false, description: 'There are no files in the album' });
const zipPath = path.join(__dirname, '..', config.uploads.folder, 'zips', `${album.identifier}.zip`);
let archive = new Zip();
for (let file of files) {
try {
const exists = fs.statSync(path.join(__dirname, '..', config.uploads.folder, file.name));
archive.file(file.name, fs.readFileSync(path.join(__dirname, '..', config.uploads.folder, file.name)));
} catch (err) {
console.log(err);
}
}
archive
.generateNodeStream({ type: 'nodebuffer', streamFiles: true })
.pipe(fs.createWriteStream(zipPath))
.on('finish', async () => {
console.log(`Generated zip for album identifier: ${identifier}`);
await db.table('albums')
.where('id', album.id)
.update({ zipGeneratedAt: Math.floor(Date.now() / 1000) });
const filePath = path.join(config.uploads.folder, 'zips', `${identifier}.zip`);
const fileName = `${album.name}.zip`;
return res.download(filePath, fileName);
});
}
};
module.exports = albumsController;

View File

@ -1,91 +0,0 @@
const config = require('../config.js');
const db = require('knex')(config.database);
const bcrypt = require('bcrypt');
const randomstring = require('randomstring');
const utils = require('./utilsController.js');
let authController = {};
authController.verify = async (req, res, next) => {
const username = req.body.username;
const password = req.body.password;
if (username === undefined) return res.json({ success: false, description: 'No username provided' });
if (password === undefined) return res.json({ success: false, description: 'No password provided' });
const user = await db.table('users').where('username', username).first();
if (!user) return res.json({ success: false, description: 'Username doesn\'t exist' });
if (user.enabled === false || user.enabled === 0) return res.json({
success: false,
description: 'This account has been disabled'
});
bcrypt.compare(password, user.password, (err, result) => {
if (err) {
console.log(err);
return res.json({ success: false, description: 'There was an error' });
}
if (result === false) return res.json({ success: false, description: 'Wrong password' });
return res.json({ success: true, token: user.token });
});
};
authController.register = async (req, res, next) => {
if (config.enableUserAccounts === false) {
return res.json({ success: false, description: 'Register is disabled at the moment' });
}
const username = req.body.username;
const password = req.body.password;
if (username === undefined) return res.json({ success: false, description: 'No username provided' });
if (password === undefined) return res.json({ success: false, description: 'No password provided' });
if (username.length < 4 || username.length > 32) {
return res.json({ success: false, description: 'Username must have 4-32 characters' });
}
if (password.length < 6 || password.length > 64) {
return res.json({ success: false, description: 'Password must have 6-64 characters' });
}
const user = await db.table('users').where('username', username).first();
if (user) return res.json({ success: false, description: 'Username already exists' });
bcrypt.hash(password, 10, async (err, hash) => {
if (err) {
console.log(err);
return res.json({ success: false, description: 'Error generating password hash (╯°□°)╯︵ ┻━┻' });
}
const token = randomstring.generate(64);
await db.table('users').insert({
username: username,
password: hash,
token: token,
enabled: 1
});
return res.json({ success: true, token: token });
});
};
authController.changePassword = async (req, res, next) => {
const user = await utils.authorize(req, res);
let password = req.body.password;
if (password === undefined) return res.json({ success: false, description: 'No password provided' });
if (password.length < 6 || password.length > 64) {
return res.json({ success: false, description: 'Password must have 6-64 characters' });
}
bcrypt.hash(password, 10, async (err, hash) => {
if (err) {
console.log(err);
return res.json({ success: false, description: 'Error generating password hash (╯°□°)╯︵ ┻━┻' });
}
await db.table('users').where('id', user.id).update({ password: hash });
return res.json({ success: true });
});
};
module.exports = authController;

View File

@ -1,34 +0,0 @@
const config = require('../config.js');
const db = require('knex')(config.database);
const randomstring = require('randomstring');
const utils = require('./utilsController.js');
const tokenController = {};
tokenController.verify = async (req, res, next) => {
const token = req.body.token;
if (token === undefined) return res.status(401).json({ success: false, description: 'No token provided' });
const user = await db.table('users').where('token', token).first();
if (!user) return res.status(401).json({ success: false, description: 'Invalid token' });
return res.json({ success: true, username: user.username });
};
tokenController.list = async (req, res, next) => {
const user = await utils.authorize(req, res);
return res.json({ success: true, token: user.token });
};
tokenController.change = async (req, res, next) => {
const user = await utils.authorize(req, res);
const newtoken = randomstring.generate(64);
await db.table('users').where('token', user.token).update({
token: newtoken,
timestamp: Math.floor(Date.now() / 1000)
});
res.json({ success: true, token: newtoken });
};
module.exports = tokenController;

View File

@ -1,311 +0,0 @@
const config = require('../config.js');
const path = require('path');
const multer = require('multer');
const randomstring = require('randomstring');
const db = require('knex')(config.database);
const crypto = require('crypto');
const fs = require('fs');
const utils = require('./utilsController.js');
const uploadsController = {};
// Let's default it to only 1 try
const maxTries = config.uploads.maxTries || 1;
const uploadDir = path.join(__dirname, '..', config.uploads.folder);
const storage = multer.diskStorage({
destination: function(req, file, cb) {
cb(null, uploadDir);
},
filename: function(req, file, cb) {
const access = i => {
const name = randomstring.generate(config.uploads.fileLength) + path.extname(file.originalname);
fs.access(path.join(uploadDir, name), err => {
if (err) return cb(null, name);
console.log(`A file named "${name}" already exists (${++i}/${maxTries}).`);
if (i < maxTries) return access(i);
return cb('Could not allocate a unique file name. Try again?');
});
};
access(0);
}
});
const upload = multer({
storage: storage,
limits: { fileSize: config.uploads.maxSize },
fileFilter: function(req, file, cb) {
if (config.blockedExtensions !== undefined) {
if (config.blockedExtensions.some(extension => path.extname(file.originalname).toLowerCase() === extension)) {
return cb('This file extension is not allowed');
}
return cb(null, true);
}
return cb(null, true);
}
}).array('files[]');
uploadsController.upload = async (req, res, next) => {
if (config.private === true) {
await utils.authorize(req, res);
}
const token = req.headers.token || '';
const user = await db.table('users').where('token', token).first();
if (user && (user.enabled === false || user.enabled === 0)) return res.json({
success: false,
description: 'This account has been disabled'
});
const albumid = req.headers.albumid || req.params.albumid;
if (albumid && user) {
const album = await db.table('albums').where({ id: albumid, userid: user.id }).first();
if (!album) {
return res.json({
success: false,
description: 'Album doesn\'t exist or it doesn\'t belong to the user'
});
}
return uploadsController.actuallyUpload(req, res, user, albumid);
}
return uploadsController.actuallyUpload(req, res, user, albumid);
};
uploadsController.actuallyUpload = async (req, res, userid, albumid) => {
upload(req, res, async err => {
if (err) {
console.error(err);
return res.json({ success: false, description: err });
}
if (req.files.length === 0) return res.json({ success: false, description: 'no-files' });
const files = [];
const existingFiles = [];
let iteration = 1;
req.files.forEach(async file => {
// Check if the file exists by checking hash and size
let hash = crypto.createHash('md5');
let stream = fs.createReadStream(path.join(__dirname, '..', config.uploads.folder, file.filename));
stream.on('data', data => {
hash.update(data, 'utf8');
});
stream.on('end', async () => {
const fileHash = hash.digest('hex');
const dbFile = await db.table('files')
.where(function() {
if (userid === undefined) this.whereNull('userid');
else this.where('userid', userid.id);
})
.where({
hash: fileHash,
size: file.size
})
.first();
if (!dbFile) {
files.push({
name: file.filename,
original: file.originalname,
type: file.mimetype,
size: file.size,
hash: fileHash,
ip: req.ip,
albumid: albumid,
userid: userid !== undefined ? userid.id : null,
timestamp: Math.floor(Date.now() / 1000)
});
} else {
uploadsController.deleteFile(file.filename).then(() => {}).catch(err => console.error(err));
existingFiles.push(dbFile);
}
if (iteration === req.files.length) {
return uploadsController.processFilesForDisplay(req, res, files, existingFiles, albumid);
}
iteration++;
});
});
});
};
uploadsController.processFilesForDisplay = async (req, res, files, existingFiles, albumid) => {
let basedomain = config.domain;
if (files.length === 0) {
return res.json({
success: true,
files: existingFiles.map(file => {
return {
name: file.name,
size: file.size,
url: `${basedomain}/${file.name}`
};
})
});
}
await db.table('files').insert(files);
for (let efile of existingFiles) files.push(efile);
for (let file of files) {
let ext = path.extname(file.name).toLowerCase();
if (utils.imageExtensions.includes(ext) || utils.videoExtensions.includes(ext)) {
file.thumb = `${basedomain}/thumbs/${file.name.slice(0, -ext.length)}.png`;
utils.generateThumbs(file);
}
}
let albumSuccess = true;
if (albumid) {
const editedAt = Math.floor(Date.now() / 1000)
albumSuccess = await db.table('albums')
.where('id', albumid)
.update('editedAt', editedAt)
.then(() => true)
.catch(error => {
console.log(error);
return false;
});
}
return res.json({
success: albumSuccess,
description: albumSuccess ? null : 'Warning: Error updating album.',
files: files.map(file => {
return {
name: file.name,
size: file.size,
url: `${basedomain}/${file.name}`
};
})
});
};
uploadsController.delete = async (req, res) => {
const user = await utils.authorize(req, res);
const id = req.body.id;
if (id === undefined || id === '') {
return res.json({ success: false, description: 'No file specified' });
}
const file = await db.table('files')
.where('id', id)
.where(function() {
if (user.username !== 'root') {
this.where('userid', user.id);
}
})
.first();
try {
await uploadsController.deleteFile(file.name);
await db.table('files').where('id', id).del();
if (file.albumid) {
await db.table('albums').where('id', file.albumid).update('editedAt', Math.floor(Date.now() / 1000));
}
} catch (err) {
console.log(err);
}
return res.json({ success: true });
};
uploadsController.deleteFile = function(file) {
const ext = path.extname(file).toLowerCase();
return new Promise((resolve, reject) => {
fs.stat(path.join(__dirname, '..', config.uploads.folder, file), (err, stats) => {
if (err) { return reject(err); }
fs.unlink(path.join(__dirname, '..', config.uploads.folder, file), err => {
if (err) { return reject(err); }
if (!utils.imageExtensions.includes(ext) && !utils.videoExtensions.includes(ext)) {
return resolve();
}
file = file.substr(0, file.lastIndexOf('.')) + '.png';
fs.stat(path.join(__dirname, '..', config.uploads.folder, 'thumbs/', file), (err, stats) => {
if (err) {
console.log(err);
return resolve();
}
fs.unlink(path.join(__dirname, '..', config.uploads.folder, 'thumbs/', file), err => {
if (err) { return reject(err); }
return resolve();
});
});
});
});
});
};
uploadsController.list = async (req, res) => {
const user = await utils.authorize(req, res);
let offset = req.params.page;
if (offset === undefined) offset = 0;
const files = await db.table('files')
.where(function() {
if (req.params.id === undefined) this.where('id', '<>', '');
else this.where('albumid', req.params.id);
})
.where(function() {
if (user.username !== 'root') this.where('userid', user.id);
})
.orderBy('id', 'DESC')
.limit(25)
.offset(25 * offset)
.select('id', 'albumid', 'timestamp', 'name', 'userid');
const albums = await db.table('albums');
let basedomain = config.domain;
let userids = [];
for (let file of files) {
file.file = `${basedomain}/${file.name}`;
file.date = new Date(file.timestamp * 1000);
file.date = utils.getPrettyDate(file.date);
file.album = '';
if (file.albumid !== undefined) {
for (let album of albums) {
if (file.albumid === album.id) {
file.album = album.name;
}
}
}
// Only push usernames if we are root
if (user.username === 'root') {
if (file.userid !== undefined && file.userid !== null && file.userid !== '') {
userids.push(file.userid);
}
}
let ext = path.extname(file.name).toLowerCase();
if (utils.imageExtensions.includes(ext) || utils.videoExtensions.includes(ext)) {
file.thumb = `${basedomain}/thumbs/${file.name.slice(0, -ext.length)}.png`;
}
}
// If we are a normal user, send response
if (user.username !== 'root') return res.json({ success: true, files });
// If we are root but there are no uploads attached to a user, send response
if (userids.length === 0) return res.json({ success: true, files });
const users = await db.table('users').whereIn('id', userids);
for (let dbUser of users) {
for (let file of files) {
if (file.userid === dbUser.id) {
file.username = dbUser.username;
}
}
}
return res.json({ success: true, files });
};
module.exports = uploadsController;

View File

@ -1,67 +0,0 @@
const path = require('path');
const config = require('../config.js');
const fs = require('fs');
const gm = require('gm');
const ffmpeg = require('fluent-ffmpeg');
const db = require('knex')(config.database);
const utilsController = {};
utilsController.imageExtensions = ['.jpg', '.jpeg', '.bmp', '.gif', '.png'];
utilsController.videoExtensions = ['.webm', '.mp4', '.wmv', '.avi', '.mov'];
utilsController.getPrettyDate = function(date) {
return date.getFullYear() + '-'
+ (date.getMonth() + 1) + '-'
+ date.getDate() + ' '
+ (date.getHours() < 10 ? '0' : '')
+ date.getHours() + ':'
+ (date.getMinutes() < 10 ? '0' : '')
+ date.getMinutes() + ':'
+ (date.getSeconds() < 10 ? '0' : '')
+ date.getSeconds();
};
utilsController.authorize = async (req, res) => {
const token = req.headers.token;
if (token === undefined) return res.status(401).json({ success: false, description: 'No token provided' });
const user = await db.table('users').where('token', token).first();
if (!user) return res.status(401).json({ success: false, description: 'Invalid token' });
return user;
};
utilsController.generateThumbs = function(file, basedomain) {
if (config.uploads.generateThumbnails !== true) return;
const ext = path.extname(file.name).toLowerCase();
let thumbname = path.join(__dirname, '..', config.uploads.folder, 'thumbs', file.name.slice(0, -ext.length) + '.png');
fs.access(thumbname, err => {
if (err && err.code === 'ENOENT') {
if (utilsController.videoExtensions.includes(ext)) {
ffmpeg(path.join(__dirname, '..', config.uploads.folder, file.name))
.thumbnail({
timestamps: [0],
filename: '%b.png',
folder: path.join(__dirname, '..', config.uploads.folder, 'thumbs'),
size: '200x?'
})
.on('error', error => console.log('Error - ', error.message));
} else {
let size = {
width: 200,
height: 200
};
gm(path.join(__dirname, '..', config.uploads.folder, file.name))
.resize(size.width, size.height + '>')
.gravity('Center')
.extent(size.width, size.height)
.background('transparent')
.write(thumbname, error => {
if (error) console.log('Error - ', error);
});
}
}
});
};
module.exports = utilsController;

View File

@ -1,53 +0,0 @@
let init = function(db){
// Create the tables we need to store galleries and files
db.schema.createTableIfNotExists('albums', function (table) {
table.increments();
table.integer('userid');
table.string('name');
table.string('identifier');
table.integer('enabled');
table.integer('timestamp');
table.integer('editedAt');
table.integer('zipGeneratedAt');
}).then(() => {});
db.schema.createTableIfNotExists('files', function (table) {
table.increments();
table.integer('userid');
table.string('name');
table.string('original');
table.string('type');
table.string('size');
table.string('hash');
table.string('ip');
table.integer('albumid');
table.integer('timestamp');
}).then(() => {});
db.schema.createTableIfNotExists('users', function (table) {
table.increments();
table.string('username');
table.string('password');
table.string('token');
table.integer('enabled');
table.integer('timestamp');
}).then(() => {
db.table('users').where({username: 'root'}).then((user) => {
if(user.length > 0) return;
require('bcrypt').hash('root', 10, function(err, hash) {
if(err) console.error('Error generating password hash for root');
db.table('users').insert({
username: 'root',
password: hash,
token: require('randomstring').generate(64),
timestamp: Math.floor(Date.now() / 1000)
}).then(() => {});
});
});
});
};
module.exports = init;

View File

@ -1,13 +0,0 @@
const config = require('../config.js');
const db = require('knex')(config.database);
const migration = {};
migration.start = async () => {
await db.schema.table('albums', table => {
table.integer('editedAt');
table.integer('zipGeneratedAt');
});
console.log('Migration finished! Now start lolisafe normally');
};
migration.start();

View File

@ -1,58 +0,0 @@
const config = require('./config.js');
const api = require('./routes/api.js');
const album = require('./routes/album.js');
const express = require('express');
const helmet = require('helmet');
const bodyParser = require('body-parser');
const RateLimit = require('express-rate-limit');
const db = require('knex')(config.database);
const fs = require('fs');
const exphbs = require('express-handlebars');
const safe = express();
require('./database/db.js')(db);
fs.existsSync('./pages/custom' ) || fs.mkdirSync('./pages/custom');
fs.existsSync('./' + config.logsFolder) || fs.mkdirSync('./' + config.logsFolder);
fs.existsSync('./' + config.uploads.folder) || fs.mkdirSync('./' + config.uploads.folder);
fs.existsSync('./' + config.uploads.folder + '/thumbs') || fs.mkdirSync('./' + config.uploads.folder + '/thumbs');
fs.existsSync('./' + config.uploads.folder + '/zips') || fs.mkdirSync('./' + config.uploads.folder + '/zips')
safe.use(helmet());
safe.set('trust proxy', 1);
safe.engine('handlebars', exphbs({ defaultLayout: 'main' }));
safe.set('view engine', 'handlebars');
safe.enable('view cache');
let limiter = new RateLimit({ windowMs: 5000, max: 2 });
safe.use('/api/login/', limiter);
safe.use('/api/register/', limiter);
safe.use(bodyParser.urlencoded({ extended: true }));
safe.use(bodyParser.json());
if (config.serveFilesWithNode) {
safe.use('/', express.static(config.uploads.folder));
}
safe.use('/', express.static('./public'));
safe.use('/', album);
safe.use('/api', api);
for (let page of config.pages) {
let root = './pages/';
if (fs.existsSync(`./pages/custom/${page}.html`)) {
root = './pages/custom/';
}
if (page === 'home') {
safe.get('/', (req, res, next) => res.sendFile(`${page}.html`, { root: root }));
} else {
safe.get(`/${page}`, (req, res, next) => res.sendFile(`${page}.html`, { root: root }));
}
}
safe.use((req, res, next) => res.status(404).sendFile('404.html', { root: './pages/error/' }));
safe.use((req, res, next) => res.status(500).sendFile('500.html', { root: './pages/error/' }));
safe.listen(config.port, () => console.log(`lolisafe started on port ${config.port}`));

View File

@ -1,56 +0,0 @@
upstream backend {
server 127.0.0.1:9999; # Change to the port you specified on lolisafe
}
map $sent_http_content_type $charset {
~^text/ utf-8;
}
server {
listen 80;
listen [::]:80;
server_name lolisafe.moe;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name lolisafe.moe;
server_tokens off;
ssl_certificate /path/to/your/fullchain.pem;
ssl_certificate_key /path/to/your/privkey.pem;
ssl_trusted_certificate /path/to/your/fullchain.pem;
client_max_body_size 100M; # Change this to the max file size you want to allow
charset $charset;
charset_types *;
# Uncomment if you are running lolisafe behind CloudFlare.
# This requires NGINX compiled from source with:
# --with-http_realip_module
#include /path/to/lolisafe/real-ip-from-cf;
location / {
add_header Access-Control-Allow-Origin *;
root /path/to/your/uploads/folder;
try_files $uri @proxy;
}
location @proxy {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_pass http://backend;
proxy_redirect off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_redirect off;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

View File

@ -1,45 +0,0 @@
upstream backend {
server 127.0.0.1:9999; # Change to the port you specified on lolisafe
}
map $sent_http_content_type $charset {
~^text/ utf-8;
}
server {
listen 80;
listen [::]:80;
server_name lolisafe.moe;
server_tokens off;
client_max_body_size 100M; # Change this to the max file size you want to allow
charset $charset;
charset_types *;
# Uncomment if you are running lolisafe behind CloudFlare.
# This requires NGINX compiled from source with:
# --with-http_realip_module
#include /path/to/lolisafe/real-ip-from-cf;
location / {
add_header Access-Control-Allow-Origin *;
root /path/to/your/uploads/folder;
try_files $uri @proxy;
}
location @proxy {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_pass http://backend;
proxy_redirect off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_redirect off;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

View File

@ -1,48 +0,0 @@
{
"name": "lolisafe",
"version": "3.0.0",
"description": "Blazing fast file uploader and awesome bunker written in node! 🚀",
"author": "Pitu",
"repository": {
"type": "git",
"url": "https://github.com/WeebDev/lolisafe"
},
"bugs": {
"url": "https://github.com/WeebDev/lolisafe/issues"
},
"engines": {
"node": ">=7.0.0"
},
"license": "MIT",
"dependencies": {
"bcrypt": "^1.0.3",
"body-parser": "^1.18.2",
"express": "^4.16.1",
"express-handlebars": "^3.0.0",
"express-rate-limit": "^2.11.0",
"fluent-ffmpeg": "^2.1.2",
"gm": "^1.23.1",
"helmet": "^3.11.0",
"jszip": "^3.1.5",
"knex": "^0.14.4",
"multer": "^1.3.0",
"randomstring": "^1.1.5",
"sqlite3": "^3.1.13"
},
"devDependencies": {
"eslint": "^4.18.1",
"eslint-config-aqua": "^1.4.1"
},
"eslintConfig": {
"extends": [
"aqua"
],
"env": {
"browser": true,
"node": true
},
"rules": {
"func-names": 0
}
}
}

View File

@ -1,60 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="A pomf-like file uploading service that doesn't suck.">
<meta name="keywords" content="upload,lolisafe,file,images,hosting">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="apple-touch-icon" sizes="180x180" href="https://lolisafe.moe/images/icons/apple-touch-icon.png?v=XBreOJMe24">
<link rel="icon" type="image/png" href="https://lolisafe.moe/images/icons/favicon-32x32.png?v=XBreOJMe24" sizes="32x32">
<link rel="icon" type="image/png" href="https://lolisafe.moe/images/icons/favicon-16x16.png?v=XBreOJMe24" sizes="16x16">
<link rel="manifest" href="https://lolisafe.moe/images/icons/manifest.json?v=XBreOJMe24">
<link rel="mask-icon" href="https://lolisafe.moe/images/icons/safari-pinned-tab.svg?v=XBreOJMe24" color="#5bbad5">
<link rel="shortcut icon" href="https://lolisafe.moe/images/icons/favicon.ico?v=XBreOJMe24">
<meta name="apple-mobile-web-app-title" content="lolisafe">
<meta name="application-name" content="lolisafe">
<meta name="msapplication-config" content="https://lolisafe.moe/images/icons/browserconfig.xml?v=XBreOJMe24">
<meta name="theme-color" content="#ffffff">
<meta property="og:url" content="https://lolisafe.moe" />
<meta property="og:type" content="website" />
<meta property="og:title" content="lolisafe.moe | A small safe worth protecting." />
<meta property="og:description" content="A pomf-like file uploading service that doesn't suck." />
<meta property="og:image" content="http://lolisafe.moe/images/logo_square.png" />
<meta property="og:image:secure_url" content="https://lolisafe.moe/images/logo_square.png" />
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="lolisafe.moe | A small safe worth protecting.">
<meta name="twitter:description" content="A pomf-like file uploading service that doesn't suck.">
<meta name="twitter:image" content="https://lolisafe.moe/files/images/logo_square.png">
<meta name="twitter:image:src" content="https://lolisafe.moe/images/logo_square.png">
<title>lolisafe - A small safe worth protecting.</title>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.3.0/css/bulma.min.css">
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/sweetalert/1.1.3/sweetalert.min.css">
<link rel="stylesheet" type="text/css" href="/css/style.css">
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/sweetalert/1.1.3/sweetalert.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.15.3/axios.min.js"></script>
<script type="text/javascript" src="/js/album.js"></script>
</head>
<body>
<section class="hero is-fullheight">
<div class="hero-head">
<div class="container">
<h1 class="title" id='title' style='margin-top: 1.5rem;'></h1>
<h1 class="subtitle" id='count'></h1>
<hr>
</div>
</div>
<div class="hero-body">
<div class="container" id='container'>
</div>
</div>
</section>
</body>
</html>

View File

@ -1,87 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="A pomf-like file uploading service that doesn't suck.">
<meta name="keywords" content="upload,lolisafe,file,images,hosting">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="apple-touch-icon" sizes="180x180" href="https://lolisafe.moe/images/icons/apple-touch-icon.png?v=XBreOJMe24">
<link rel="icon" type="image/png" href="https://lolisafe.moe/images/icons/favicon-32x32.png?v=XBreOJMe24" sizes="32x32">
<link rel="icon" type="image/png" href="https://lolisafe.moe/images/icons/favicon-16x16.png?v=XBreOJMe24" sizes="16x16">
<link rel="manifest" href="https://lolisafe.moe/images/icons/manifest.json?v=XBreOJMe24">
<link rel="mask-icon" href="https://lolisafe.moe/images/icons/safari-pinned-tab.svg?v=XBreOJMe24" color="#5bbad5">
<link rel="shortcut icon" href="https://lolisafe.moe/images/icons/favicon.ico?v=XBreOJMe24">
<meta name="apple-mobile-web-app-title" content="lolisafe">
<meta name="application-name" content="lolisafe">
<meta name="msapplication-config" content="https://lolisafe.moe/images/icons/browserconfig.xml?v=XBreOJMe24">
<meta name="theme-color" content="#ffffff">
<meta property="og:url" content="https://lolisafe.moe" />
<meta property="og:type" content="website" />
<meta property="og:title" content="lolisafe.moe | A small safe worth protecting." />
<meta property="og:description" content="A pomf-like file uploading service that doesn't suck." />
<meta property="og:image" content="http://lolisafe.moe/images/logo_square.png" />
<meta property="og:image:secure_url" content="https://lolisafe.moe/images/logo_square.png" />
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="lolisafe.moe | A small safe worth protecting.">
<meta name="twitter:description" content="A pomf-like file uploading service that doesn't suck.">
<meta name="twitter:image" content="https://lolisafe.moe/files/images/logo_square.png">
<meta name="twitter:image:src" content="https://lolisafe.moe/images/logo_square.png">
<title>lolisafe - A small safe worth protecting.</title>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.3.0/css/bulma.min.css">
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/sweetalert/1.1.3/sweetalert.min.css">
<link rel="stylesheet" type="text/css" href="/css/style.css">
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/sweetalert/1.1.3/sweetalert.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.15.3/axios.min.js"></script>
<script type="text/javascript" src="https://use.fontawesome.com/cd26baa9bd.js"></script>
<script type="text/javascript" src="/js/auth.js"></script>
</head>
<body>
<style type="text/css">
section#login {
background-color: #f5f6f8;
}
</style>
<section id='login' class="hero is-fullheight">
<div class="hero-body">
<div class="container">
<h1 class="title">
Dashboard Access
</h1>
<h2 class="subtitle">
Login or register
</h2>
<div class="columns">
<div class="column">
<p class="control">
<input id='user' class="input" type="text" placeholder="Your username">
</p>
<p class="control">
<input id='pass' class="input" type="password" placeholder="Your password">
</p>
<p class="control has-addons is-pulled-right">
<a class="button" id='registerBtn' onclick="page.do('register')">
<span>Register</span>
</a>
<a class="button" id='loginBtn' onclick="page.do('login')">
<span>Log in</span>
</a>
</p>
</div>
<div class="column is-hidden-mobile"></div>
<div class="column is-hidden-mobile"></div>
</div>
</div>
</div>
</section>
</body>
</html>

View File

@ -1,100 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="A pomf-like file uploading service that doesn't suck.">
<meta name="keywords" content="upload,lolisafe,file,images,hosting">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="apple-touch-icon" sizes="180x180" href="https://lolisafe.moe/images/icons/apple-touch-icon.png?v=XBreOJMe24">
<link rel="icon" type="image/png" href="https://lolisafe.moe/images/icons/favicon-32x32.png?v=XBreOJMe24" sizes="32x32">
<link rel="icon" type="image/png" href="https://lolisafe.moe/images/icons/favicon-16x16.png?v=XBreOJMe24" sizes="16x16">
<link rel="manifest" href="https://lolisafe.moe/images/icons/manifest.json?v=XBreOJMe24">
<link rel="mask-icon" href="https://lolisafe.moe/images/icons/safari-pinned-tab.svg?v=XBreOJMe24" color="#5bbad5">
<link rel="shortcut icon" href="https://lolisafe.moe/images/icons/favicon.ico?v=XBreOJMe24">
<meta name="apple-mobile-web-app-title" content="lolisafe">
<meta name="application-name" content="lolisafe">
<meta name="msapplication-config" content="https://lolisafe.moe/images/icons/browserconfig.xml?v=XBreOJMe24">
<meta name="theme-color" content="#ffffff">
<meta property="og:url" content="https://lolisafe.moe" />
<meta property="og:type" content="website" />
<meta property="og:title" content="lolisafe.moe | A small safe worth protecting." />
<meta property="og:description" content="A pomf-like file uploading service that doesn't suck." />
<meta property="og:image" content="http://lolisafe.moe/images/logo_square.png" />
<meta property="og:image:secure_url" content="https://lolisafe.moe/images/logo_square.png" />
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="lolisafe.moe | A small safe worth protecting.">
<meta name="twitter:description" content="A pomf-like file uploading service that doesn't suck.">
<meta name="twitter:image" content="https://lolisafe.moe/files/images/logo_square.png">
<meta name="twitter:image:src" content="https://lolisafe.moe/images/logo_square.png">
<title>lolisafe - A small safe worth protecting.</title>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.3.0/css/bulma.min.css">
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/sweetalert/1.1.3/sweetalert.min.css">
<link rel="stylesheet" type="text/css" href="/css/style.css">
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/sweetalert/1.1.3/sweetalert.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.15.3/axios.min.js"></script>
<script type="text/javascript" src="https://use.fontawesome.com/cd26baa9bd.js"></script>
<script type="text/javascript" src="/js/dashboard.js"></script>
</head>
<body>
<section id='auth' class="hero is-light is-fullheight">
<div class="hero-body">
<div class="container">
<h1 class="title">
Admin dashboard
</h1>
<h2 class="subtitle">
<p class="control has-addons">
<input id='token' class="input is-danger" type="text" placeholder="Your admin token">
<a id='tokenSubmit' class="button is-danger is-outlined">Check</a>
</p>
</h2>
</div>
</div>
</section>
<section id='dashboard' class="section">
<div id="panel" class="container">
<h1 class="title">Dashboard</h1>
<h2 class="subtitle">A simple <strong>dashboard</strong>, to sort your uploaded stuff</h2>
<hr>
<div class="columns">
<div class="column is-3">
<aside class="menu" id="menu">
<p class="menu-label">General</p>
<ul class="menu-list">
<li><a href="/">Frontpage</a></li>
<li><a id="itemUploads" onclick="panel.getUploads()">Uploads</a></li>
</ul>
<p class="menu-label">Albums</p>
<ul class="menu-list">
<li><a id="itemManageGallery" onclick="panel.getAlbums()">Manage your albums</a></li>
<li>
<ul id='albumsContainer'></ul>
</li>
</ul>
<p class="menu-label">Administration</p>
<ul class="menu-list">
<li><a id="itemTokens" onclick="panel.changeToken()">Change your token</a></li>
<li><a id="itemPassword" onclick="panel.changePassword()">Change your password</a></li>
<li><a id="itemLogout"onclick="panel.logout()">Logout</a></li>
</ul>
</aside>
</div>
<div class="column has-text-centered" id='page'>
<img src="/images/logo.png">
</div>
</div>
</div>
</section>
</body>
</html>

View File

@ -1,47 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>lolisafe</title>
<link href='//fonts.googleapis.com/css?family=Lato:100' rel='stylesheet' type='text/css'>
<style>
html, body {
height: 100%;
}
body {
margin: 0;
padding: 0;
width: 100%;
color: #B0BEC5;
display: table;
font-weight: 100;
font-family: 'Lato';
}
.container {
text-align: center;
display: table-cell;
vertical-align: middle;
}
.content {
text-align: center;
display: inline-block;
}
.title {
font-size: 72px;
margin-bottom: 40px;
}
</style>
</head>
<body>
<div class="container">
<div class="content">
<div class="title">Page not found.</div>
</div>
</div>
</body>
</html>

View File

@ -1,47 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>lolisafe</title>
<link href='//fonts.googleapis.com/css?family=Lato:100' rel='stylesheet' type='text/css'>
<style>
html, body {
height: 100%;
}
body {
margin: 0;
padding: 0;
width: 100%;
color: #B0BEC5;
display: table;
font-weight: 100;
font-family: 'Lato';
}
.container {
text-align: center;
display: table-cell;
vertical-align: middle;
}
.content {
text-align: center;
display: inline-block;
}
.title {
font-size: 72px;
margin-bottom: 40px;
}
</style>
</head>
<body>
<div class="container">
<div class="content">
<div class="title">Internal server error.</div>
</div>
</div>
</body>
</html>

View File

@ -1,83 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="A pomf-like file uploading service that doesn't suck.">
<meta name="keywords" content="upload,lolisafe,file,images,hosting">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="apple-touch-icon" sizes="180x180" href="https://lolisafe.moe/images/icons/apple-touch-icon.png?v=XBreOJMe24">
<link rel="icon" type="image/png" href="https://lolisafe.moe/images/icons/favicon-32x32.png?v=XBreOJMe24" sizes="32x32">
<link rel="icon" type="image/png" href="https://lolisafe.moe/images/icons/favicon-16x16.png?v=XBreOJMe24" sizes="16x16">
<link rel="manifest" href="https://lolisafe.moe/images/icons/manifest.json?v=XBreOJMe24">
<link rel="mask-icon" href="https://lolisafe.moe/images/icons/safari-pinned-tab.svg?v=XBreOJMe24" color="#5bbad5">
<link rel="shortcut icon" href="https://lolisafe.moe/images/icons/favicon.ico?v=XBreOJMe24">
<meta name="apple-mobile-web-app-title" content="lolisafe">
<meta name="application-name" content="lolisafe">
<meta name="msapplication-config" content="https://lolisafe.moe/images/icons/browserconfig.xml?v=XBreOJMe24">
<meta name="theme-color" content="#ffffff">
<meta property="og:url" content="https://lolisafe.moe" />
<meta property="og:type" content="website" />
<meta property="og:title" content="lolisafe.moe | A small safe worth protecting." />
<meta property="og:description" content="A pomf-like file uploading service that doesn't suck." />
<meta property="og:image" content="http://lolisafe.moe/images/logo_square.png" />
<meta property="og:image:secure_url" content="https://lolisafe.moe/images/logo_square.png" />
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="lolisafe.moe | A small safe worth protecting.">
<meta name="twitter:description" content="A pomf-like file uploading service that doesn't suck.">
<meta name="twitter:image" content="https://lolisafe.moe/files/images/logo_square.png">
<meta name="twitter:image:src" content="https://lolisafe.moe/images/logo_square.png">
<title>lolisafe - A small safe worth protecting.</title>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.3.0/css/bulma.min.css">
<link rel="stylesheet" type="text/css" href="/css/style.css">
</head>
<body>
<section class="hero is-fullheight has-text-centered" id="home">
<div class="hero-body">
<div class="container has-text-left">
<h2 class='subtitle'>What is lolisafe?</h2>
<article class="message">
<div class="message-body">
lolisafe is an easy to use, open source and completely free file upload service. We accept your files, photos, documents, anything, and give you back a shareable link for you to send to others.
</div>
</article>
<h2 class='subtitle'>Will you keep my files forever?</h2>
<article class="message">
<div class="message-body">
Unless we receive a copyright complain or some other bullshit, we will.
</div>
</article>
<h2 class='subtitle'>How can I keep track of my uploads?</h2>
<article class="message">
<div class="message-body">
Simply create a user on the site and every upload will be associated with your account, granting you access to your uploaded files through our dashboard.
</div>
</article>
<h2 class='subtitle'>What are albums?</h2>
<article class="message">
<div class="message-body">
Albums are a simple way of sorting uploads together. Right now you can create albums through the dashboard and use them only with <a target="_blank" href="https://chrome.google.com/webstore/detail/lolisafe-uploader/enkkmplljfjppcdaancckgilmgoiofnj">our chrome extension</a> which will enable you to <strong>right click -> send to lolisafe</strong> or to a desired album if you have any.
</div>
</article>
<h2 class='subtitle'>Why should I use this?</h2>
<article class="message">
<div class="message-body">
There are too many file upload services out there, and a lot of them rely on the foundations of pomf which is ancient. In a desperate and unsuccessful attempt of finding a good file uploader that's easily extendable, lolisafe was born. We give you control over your files, we give you a way to sort your uploads into albums for ease of access and we give you an api to use with ShareX or any other thing that let's you make POST requests. Awesome isn't it? Just like you.
</div>
</article>
</div>
</div>
</section>
</body>
</html>

View File

@ -1,93 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="A pomf-like file uploading service that doesn't suck.">
<meta name="keywords" content="upload,lolisafe,file,images,hosting">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="apple-touch-icon" sizes="180x180" href="https://lolisafe.moe/images/icons/apple-touch-icon.png?v=XBreOJMe24">
<link rel="icon" type="image/png" href="https://lolisafe.moe/images/icons/favicon-32x32.png?v=XBreOJMe24" sizes="32x32">
<link rel="icon" type="image/png" href="https://lolisafe.moe/images/icons/favicon-16x16.png?v=XBreOJMe24" sizes="16x16">
<link rel="manifest" href="https://lolisafe.moe/images/icons/manifest.json?v=XBreOJMe24">
<link rel="mask-icon" href="https://lolisafe.moe/images/icons/safari-pinned-tab.svg?v=XBreOJMe24" color="#5bbad5">
<link rel="shortcut icon" href="https://lolisafe.moe/images/icons/favicon.ico?v=XBreOJMe24">
<meta name="apple-mobile-web-app-title" content="lolisafe">
<meta name="application-name" content="lolisafe">
<meta name="msapplication-config" content="https://lolisafe.moe/images/icons/browserconfig.xml?v=XBreOJMe24">
<meta name="theme-color" content="#ffffff">
<meta property="og:url" content="https://lolisafe.moe" />
<meta property="og:type" content="website" />
<meta property="og:title" content="lolisafe.moe | A small safe worth protecting." />
<meta property="og:description" content="A pomf-like file uploading service that doesn't suck." />
<meta property="og:image" content="http://lolisafe.moe/images/logo_square.png" />
<meta property="og:image:secure_url" content="https://lolisafe.moe/images/logo_square.png" />
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="lolisafe.moe | A small safe worth protecting.">
<meta name="twitter:description" content="A pomf-like file uploading service that doesn't suck.">
<meta name="twitter:image" content="https://lolisafe.moe/files/images/logo_square.png">
<meta name="twitter:image:src" content="https://lolisafe.moe/images/logo_square.png">
<title>lolisafe - A small safe worth protecting.</title>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.3.0/css/bulma.min.css">
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/sweetalert/1.1.3/sweetalert.min.css">
<link rel="stylesheet" type="text/css" href="/css/style.css">
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/sweetalert/1.1.3/sweetalert.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/dropzone/4.3.0/min/dropzone.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.15.3/axios.min.js"></script>
<script type="text/javascript" src="/js/home.js"></script>
</head>
<body>
<section class="hero is-fullheight has-text-centered" id="home">
<div class="hero-body">
<div class="container">
<p id="b">
<img class="logo" src="/images/logo_smol.png">
</p>
<h1 class="title">lolisafe</h1>
<h2 class="subtitle">A <strong>modern</strong> self-hosted file upload service</h2>
<h3 class="subtitle" id="maxFileSize"></h3>
<div class="columns">
<div class="column is-hidden-mobile"></div>
<div class="column" id="uploadContainer">
<a id="loginToUpload" href="/auth" class="button is-danger">Running in private mode. Log in to upload.</a>
<div class="field" id="albumDiv" style="display: none">
<p class="control select-wrapper">
<span class="select">
<select id="albumSelect">
<option value="">Upload to album</option>
</select>
</span>
</p>
</div>
</div>
<div class="column is-hidden-mobile"></div>
</div>
<div id="uploads">
<div id="template" class="columns">
<div class="column is-hidden-mobile"></div>
<div class="column">
<progress class="progress is-small is-danger" value="0" max="100" data-dz-uploadprogress></progress>
<p data-dz-errormessage></p>
<p class="link"></p>
</div>
<div class="column is-hidden-mobile"></div>
</div>
</div>
<h3 class="subtitle"><a href="/auth" id="loginLinkText"></a></h3>
<h3 id="links">
<a href="https://github.com/WeebDev/lolisafe" target="_blank" class="is-danger">View on GitHub</a><span>|</span><a id="ShareX" href="https://lolisafe.moe/sharex.txt">ShareX</a><span>|</span><a href="https://chrome.google.com/webstore/detail/lolisafe-uploader/enkkmplljfjppcdaancckgilmgoiofnj" target="_blank" class="is-danger">Chrome extension</a><span>|</span><a href="/faq" class="is-danger">FAQ</a><span>|</span><a href="/auth" target="_blank" class="is-danger">Dashboard</a>
</h3>
</div>
</div>
</section>
</body>
</html>

View File

@ -1,113 +0,0 @@
/* ------------------
HOME
------------------ */
section#home #b {
-webkit-animation-delay: 0.5s;
animation-delay: 0.5s;
-webkit-animation-duration: 1.5s;
animation-duration: 1.5s;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
-webkit-animation-name: floatUp;
animation-name: floatUp;
-webkit-animation-timing-function: cubic-bezier(0, 0.71, 0.29, 1);
animation-timing-function: cubic-bezier(0, 0.71, 0.29, 1);
border-radius: 24px;
display: inline-block;
height: 240px;
margin-bottom: 40px;
position: relative;
vertical-align: top;
width: 240px;
box-shadow: 0 20px 60px rgba(10, 10, 10, 0.05), 0 5px 10px rgba(10, 10, 10, 0.1), 0 1px 1px rgba(10, 10, 10, 0.2);
}
section#home div#dropzone {
border: 1px solid #dbdbdb;
background-color: rgba(0, 0, 0, 0);
border-color: #ff3860;
color: #ff3860;
display: none;
width: 100%;
border-radius: 3px;
box-shadow: none;
height: 2.5em;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
user-select: none;
justify-content: center;
padding-left: .75em;
padding-right: .75em;
text-align: center;
cursor: pointer;
}
section#home div#uploads, section#home p#tokenContainer, section#home a#panel { display: none; }
section#home div#dropzone:hover { background-color: #ff3860; border-color: #ff3860; color: #fff; }
section#home h3#maxFileSize { font-size: 14px; }
section#home h3#links span { padding-left: 5px; padding-right: 5px; }
section#home img.logo { height: 200px; margin-top: 20px; }
section#home .dz-preview .dz-details { display: flex; }
section#home .dz-preview .dz-details .dz-size, section#home .dz-preview .dz-details .dz-filename { flex: 1; }
section#home .dz-preview img, section#home .dz-preview .dz-success-mark, section#home .dz-preview .dz-error-mark { display: none; }
section#home div#uploads { margin-bottom: 25px; }
@keyframes floatUp {
0% {
opacity: 0;
box-shadow: 0 0 0 rgba(10, 10, 10, 0), 0 0 0 rgba(10, 10, 10, 0), 0 0 0 rgba(10, 10, 10, 0);
-webkit-transform: scale(0.86);
transform: scale(0.86);
}
25% { opacity: 100; }
67% {
box-shadow: 0 0 0 rgba(10, 10, 10, 0), 0 5px 10px rgba(10, 10, 10, 0.1), 0 1px 1px rgba(10, 10, 10, 0.2);
-webkit-transform: scale(1);
transform: scale(1);
}
100% {
box-shadow: 0 20px 60px rgba(10, 10, 10, 0.05), 0 5px 10px rgba(10, 10, 10, 0.1), 0 1px 1px rgba(10, 10, 10, 0.2);
-webkit-transform: scale(1);
transform: scale(1);
}
}
/* ------------------
PANEL
------------------ */
section#login input, section#login p.control a.button {
border-left: 0px;
border-top: 0px;
border-right: 0px;
border-radius: 0px;
box-shadow: 0 0 0;
}
section#login p.control a.button { margin-left: 10px; }
section#login p.control a#loginBtn { border-right: 0px; }
section#login p.control a#registerBtn { border-left: 0px; }
section#auth, section#dashboard { display: none }
section#auth input { background: rgba(0, 0, 0, 0); }
section#auth input, section#auth a {
border-left: 0px;
border-top: 0px;
border-right: 0px;
border-radius: 0px;
box-shadow: 0 0 0;
}
section#dashboard .table { font-size: 12px }
section#dashboard div#table div.column { display:flex; width: 200px; height: 220px; margin: 9px; background: #f9f9f9; overflow: hidden; flex-wrap: wrap; align-items: center; }
section#dashboard div#table div.column a { width: 100%; }
section#dashboard div#table div.column a:first-child { height: 180px; }
section#dashboard div#table div.column a img { width:200px; }
.select-wrapper {
text-align: center;
margin-bottom: 10px;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/images/icons/mstile-150x150.png?v=XBreOJMe24"/>
<TileColor>#00aba9</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 920 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@ -1,18 +0,0 @@
{
"name": "lolisafe",
"icons": [
{
"src": "/images/icons/android-chrome-192x192.png?v=XBreOJMe24",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/images/icons/android-chrome-384x384.png?v=XBreOJMe24",
"sizes": "384x384",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

View File

@ -1,47 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="16.000000pt" height="16.000000pt" viewBox="0 0 16.000000 16.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.11, written by Peter Selinger 2001-2013
</metadata>
<g transform="translate(0.000000,16.000000) scale(0.003765,-0.003765)"
fill="#000000" stroke="none">
<path d="M2397 4143 c-3 -3 -19 -7 -34 -8 -162 -16 -259 -56 -351 -144 -65
-64 -123 -152 -156 -239 -22 -61 -27 -80 -40 -148 -6 -29 -18 -161 -20 -209
-1 -40 -2 -40 -81 -80 l-81 -40 58 -50 c32 -28 70 -55 85 -62 l28 -11 -25 -10
-25 -10 29 -13 28 -13 -6 -106 c-3 -58 -7 -114 -9 -125 -3 -11 -8 -45 -11 -76
-4 -31 -9 -67 -11 -80 -3 -13 -12 -60 -20 -104 -21 -108 -99 -327 -177 -497
-29 -64 -32 -67 -62 -64 -17 2 -36 4 -42 5 -25 2 -95 171 -108 259 -9 62 -14
55 -73 -98 -35 -89 -48 -133 -59 -190 -2 -14 -6 -34 -9 -45 -21 -99 -22 -255
-2 -370 8 -44 14 -81 13 -82 -5 -6 -97 76 -131 117 -47 57 -97 151 -111 205
-30 123 -36 217 -19 300 8 41 -6 24 -48 -60 -36 -72 -72 -169 -81 -221 -3 -16
-10 -44 -16 -63 -10 -36 -7 -255 4 -304 4 -16 15 -51 26 -79 11 -28 17 -54 15
-58 -14 -22 -177 11 -235 48 l-25 16 16 -25 c27 -42 141 -127 204 -153 33 -14
86 -32 117 -41 48 -14 58 -20 63 -44 3 -15 31 -65 61 -112 31 -46 68 -109 84
-139 15 -31 41 -67 57 -80 16 -14 45 -45 64 -70 19 -25 37 -47 40 -50 3 -3 34
-43 69 -90 109 -146 240 -259 412 -353 25 -14 65 -38 89 -53 48 -30 175 -79
229 -88 117 -19 211 -37 250 -49 47 -14 85 -14 160 -3 131 21 171 87 171 281
-1 73 -3 93 -12 110 -4 6 -9 22 -13 37 -6 26 -54 109 -159 278 -26 41 -55 85
-63 97 -14 20 -14 23 -2 23 9 0 32 -21 52 -47 99 -128 366 -377 506 -473 12
-8 43 -30 68 -49 38 -29 57 -35 116 -41 128 -13 166 5 214 103 81 161 76 270
-19 438 -23 41 -45 76 -48 79 -3 3 -31 34 -61 70 -30 36 -57 67 -61 70 -15 13
-169 222 -169 231 0 6 7 23 15 38 26 51 56 185 61 271 3 47 7 93 9 103 5 27
-8 20 -38 -22 -15 -22 -45 -54 -67 -71 l-40 -32 -1 89 c0 48 -3 81 -7 73 -11
-25 -41 -64 -46 -59 -2 2 -7 33 -11 69 -7 65 -7 66 22 80 15 7 30 14 33 14 3
1 26 13 52 28 56 34 121 97 157 153 22 35 30 41 47 34 47 -17 103 -115 91
-160 -6 -22 8 -26 17 -4 3 8 8 36 12 63 10 79 -28 143 -98 165 -15 5 -15 10
-4 49 31 104 15 279 -35 384 -19 40 -29 78 -29 107 0 48 -12 61 -43 45 -9 -5
-23 -5 -31 -1 -31 19 -74 61 -91 91 -13 21 -35 38 -62 48 -24 10 -42 22 -40
28 3 6 9 10 15 9 9 -2 52 84 67 137 4 14 25 42 47 62 22 20 37 42 33 48 -3 5
-11 7 -17 4 -6 -4 -6 1 1 14 6 12 16 24 22 27 6 4 23 25 39 47 29 40 29 41 10
57 -17 13 -18 22 -11 54 5 21 7 50 6 65 -2 15 -6 52 -9 82 -10 114 -77 279
-158 387 -46 63 -167 162 -247 202 -97 49 -263 91 -276 70 -4 -5 -13 -5 -24 1
-10 5 -21 7 -24 3z"/>
<path d="M3111 3011 c-13 -13 -21 -35 -22 -58 0 -21 -1 -53 -2 -71 -1 -17 2
-32 6 -32 4 0 29 11 55 24 74 37 87 86 27 98 -12 2 -19 10 -17 20 1 8 -4 21
-12 27 -12 10 -19 8 -35 -8z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

View File

@ -1,56 +0,0 @@
var page = {};
page.do = function(dest){
var user = document.getElementById('user').value;
var pass = document.getElementById('pass').value;
if(user === undefined || user === null || user === '')
return swal('Error', 'You need to specify a username', 'error');
if(pass === undefined || pass === null || pass === '')
return swal('Error', 'You need to specify a username', 'error');
axios.post('/api/' + dest, {
username: user,
password: pass
})
.then(function (response) {
if(response.data.success === false)
return swal('Error', response.data.description, 'error');
localStorage.token = response.data.token;
window.location = '/dashboard';
})
.catch(function (error) {
return swal('An error ocurred', 'There was an error with the request, please check the console for more information.', 'error');
console.log(error);
});
};
page.verify = function(){
page.token = localStorage.token;
if(page.token === undefined) return;
axios.post('/api/tokens/verify', {
token: page.token
})
.then(function (response) {
if(response.data.success === false)
return swal('Error', response.data.description, 'error');
window.location = '/dashboard';
})
.catch(function (error) {
return swal('An error ocurred', 'There was an error with the request, please check the console for more information.', 'error');
console.log(error);
});
};
window.onload = function () {
page.verify();
};

View File

@ -1,620 +0,0 @@
let panel = {};
panel.page;
panel.username;
panel.token = localStorage.token;
panel.filesView = localStorage.filesView;
panel.preparePage = function(){
if(!panel.token) return window.location = '/auth';
panel.verifyToken(panel.token, true);
};
panel.verifyToken = function(token, reloadOnError){
if(reloadOnError === undefined)
reloadOnError = false;
axios.post('/api/tokens/verify', {
token: token
})
.then(function (response) {
if(response.data.success === false){
swal({
title: "An error ocurred",
text: response.data.description,
type: "error"
}, function(){
if(reloadOnError){
localStorage.removeItem("token");
location.location = '/auth';
}
});
return;
}
axios.defaults.headers.common['token'] = token;
localStorage.token = token;
panel.token = token;
panel.username = response.data.username;
return panel.prepareDashboard();
})
.catch(function (error) {
return swal("An error ocurred", 'There was an error with the request, please check the console for more information.', "error");
console.log(error);
});
};
panel.prepareDashboard = function(){
panel.page = document.getElementById('page');
document.getElementById('auth').style.display = 'none';
document.getElementById('dashboard').style.display = 'block';
document.getElementById('itemUploads').addEventListener('click', function(){
panel.setActiveMenu(this);
});
document.getElementById('itemManageGallery').addEventListener('click', function(){
panel.setActiveMenu(this);
});
document.getElementById('itemTokens').addEventListener('click', function(){
panel.setActiveMenu(this);
});
document.getElementById('itemPassword').addEventListener('click', function(){
panel.setActiveMenu(this);
});
document.getElementById('itemLogout').innerHTML = `Logout ( ${panel.username} )`;
panel.getAlbumsSidebar();
};
panel.logout = function(){
localStorage.removeItem("token");
location.reload('/');
};
panel.getUploads = function(album = undefined, page = undefined){
if(page === undefined) page = 0;
let url = '/api/uploads/' + page;
if(album !== undefined)
url = '/api/album/' + album + '/' + page;
axios.get(url).then(function (response) {
if(response.data.success === false){
if(response.data.description === 'No token provided') return panel.verifyToken(panel.token);
else return swal("An error ocurred", response.data.description, "error");
}
var prevPage = 0;
var nextPage = page + 1;
if(response.data.files.length < 25)
nextPage = page;
if(page > 0) prevPage = page - 1;
panel.page.innerHTML = '';
var container = document.createElement('div');
var pagination = `<nav class="pagination is-centered">
<a class="pagination-previous" onclick="panel.getUploads(${album}, ${prevPage} )">Previous</a>
<a class="pagination-next" onclick="panel.getUploads(${album}, ${nextPage} )">Next page</a>
</nav>`;
var listType = `
<div class="columns">
<div class="column">
<a class="button is-small is-outlined is-danger" title="List view" onclick="panel.setFilesView('list', ${album}, ${page})">
<span class="icon is-small">
<i class="fa fa-list-ul"></i>
</span>
</a>
<a class="button is-small is-outlined is-danger" title="List view" onclick="panel.setFilesView('thumbs', ${album}, ${page})">
<span class="icon is-small">
<i class="fa fa-th-large"></i>
</span>
</a>
</div>
</div>`;
if(panel.filesView === 'thumbs'){
container.innerHTML = `
${pagination}
<hr>
${listType}
<div class="columns is-multiline is-mobile" id="table">
</div>
${pagination}
`;
panel.page.appendChild(container);
var table = document.getElementById('table');
for(var item of response.data.files){
var div = document.createElement('div');
div.className = "column is-2";
if(item.thumb !== undefined)
div.innerHTML = `<a href="${item.file}" target="_blank"><img src="${item.thumb}"/></a><a class="button is-small is-danger is-outlined" title="Delete file" onclick="panel.deleteFile(${item.id})"><span class="icon is-small"><i class="fa fa-trash-o"></i></span></a>`;
else
div.innerHTML = `<a href="${item.file}" target="_blank"><h1 class="title">.${item.file.split('.').pop()}</h1></a><a class="button is-small is-danger is-outlined" title="Delete file" onclick="panel.deleteFile(${item.id})"><span class="icon is-small"><i class="fa fa-trash-o"></i></span></a>`;
table.appendChild(div);
}
}else{
var albumOrUser = 'Album';
if(panel.username === 'root')
albumOrUser = 'User';
container.innerHTML = `
${pagination}
<hr>
${listType}
<table class="table is-striped is-narrow is-left">
<thead>
<tr>
<th>File</th>
<th>${albumOrUser}</th>
<th>Date</th>
<th></th>
</tr>
</thead>
<tbody id="table">
</tbody>
</table>
<hr>
${pagination}
`;
panel.page.appendChild(container);
var table = document.getElementById('table');
for(var item of response.data.files){
var tr = document.createElement('tr');
var displayAlbumOrUser = item.album;
if(panel.username === 'root'){
displayAlbumOrUser = '';
if(item.username !== undefined)
displayAlbumOrUser = item.username;
}
tr.innerHTML = `
<tr>
<th><a href="${item.file}" target="_blank">${item.file}</a></th>
<th>${displayAlbumOrUser}</th>
<td>${item.date}</td>
<td>
<a class="button is-small is-danger is-outlined" title="Delete album" onclick="panel.deleteFile(${item.id})">
<span class="icon is-small">
<i class="fa fa-trash-o"></i>
</span>
</a>
</td>
</tr>
`;
table.appendChild(tr);
}
}
})
.catch(function (error) {
return swal("An error ocurred", 'There was an error with the request, please check the console for more information.', "error");
console.log(error);
});
};
panel.setFilesView = function(view, album, page){
localStorage.filesView = view;
panel.filesView = view;
panel.getUploads(album, page);
};
panel.deleteFile = function(id){
swal({
title: "Are you sure?",
text: "You wont be able to recover the file!",
type: "warning",
showCancelButton: true,
confirmButtonColor: "#ff3860",
confirmButtonText: "Yes, delete it!",
closeOnConfirm: false
},
function(){
axios.post('/api/upload/delete', {
id: id
})
.then(function (response) {
if(response.data.success === false){
if(response.data.description === 'No token provided') return panel.verifyToken(panel.token);
else return swal("An error ocurred", response.data.description, "error");
}
swal("Deleted!", "The file has been deleted.", "success");
panel.getUploads();
})
.catch(function (error) {
return swal("An error ocurred", 'There was an error with the request, please check the console for more information.', "error");
console.log(error);
});
}
);
};
panel.getAlbums = function(){
axios.get('/api/albums').then(function (response) {
if(response.data.success === false){
if(response.data.description === 'No token provided') return panel.verifyToken(panel.token);
else return swal("An error ocurred", response.data.description, "error");
}
panel.page.innerHTML = '';
var container = document.createElement('div');
container.className = "container";
container.innerHTML = `
<h2 class="subtitle">Create new album</h2>
<p class="control has-addons has-addons-centered">
<input id="albumName" class="input" type="text" placeholder="Name">
<a id="submitAlbum" class="button is-primary">Submit</a>
</p>
<h2 class="subtitle">List of albums</h2>
<table class="table is-striped is-narrow">
<thead>
<tr>
<th>Name</th>
<th>Files</th>
<th>Created At</th>
<th>Public link</th>
<th></th>
</tr>
</thead>
<tbody id="table">
</tbody>
</table>`;
panel.page.appendChild(container);
var table = document.getElementById('table');
for(var item of response.data.albums){
var tr = document.createElement('tr');
tr.innerHTML = `
<tr>
<th>${item.name}</th>
<th>${item.files}</th>
<td>${item.date}</td>
<td><a href="${item.identifier}" target="_blank">Album link</a></td>
<td>
<a class="button is-small is-primary is-outlined" title="Edit name" onclick="panel.renameAlbum(${item.id})">
<span class="icon is-small">
<i class="fa fa-pencil"></i>
</span>
</a>
<a class="button is-small is-danger is-outlined" title="Delete album" onclick="panel.deleteAlbum(${item.id})">
<span class="icon is-small">
<i class="fa fa-trash-o"></i>
</span>
</a>
</td>
</tr>
`;
table.appendChild(tr);
}
document.getElementById('submitAlbum').addEventListener('click', function(){
panel.submitAlbum();
});
})
.catch(function (error) {
return swal("An error ocurred", 'There was an error with the request, please check the console for more information.', "error");
console.log(error);
});
};
panel.renameAlbum = function(id){
swal({
title: "Rename album",
text: "New name you want to give the album:",
type: "input",
showCancelButton: true,
closeOnConfirm: false,
animation: "slide-from-top",
inputPlaceholder: "My super album"
},function(inputValue){
if (inputValue === false) return false;
if (inputValue === "") {
swal.showInputError("You need to write something!");
return false;
}
axios.post('/api/albums/rename', {
id: id,
name: inputValue
})
.then(function (response) {
if(response.data.success === false){
if(response.data.description === 'No token provided') return panel.verifyToken(panel.token);
else if(response.data.description === 'Name already in use') swal.showInputError("That name is already in use!");
else swal("An error ocurred", response.data.description, "error");
return;
}
swal("Success!", "Your album was renamed to: " + inputValue, "success");
panel.getAlbumsSidebar();
panel.getAlbums();
})
.catch(function (error) {
return swal("An error ocurred", 'There was an error with the request, please check the console for more information.', "error");
console.log(error);
});
});
};
panel.deleteAlbum = function(id){
swal({
title: "Are you sure?",
text: "This won't delete your files, only the album!",
type: "warning",
showCancelButton: true,
confirmButtonColor: "#ff3860",
confirmButtonText: "Yes, delete it!",
closeOnConfirm: false
},
function(){
axios.post('/api/albums/delete', {
id: id
})
.then(function (response) {
if(response.data.success === false){
if(response.data.description === 'No token provided') return panel.verifyToken(panel.token);
else return swal("An error ocurred", response.data.description, "error");
}
swal("Deleted!", "Your album has been deleted.", "success");
panel.getAlbumsSidebar();
panel.getAlbums();
})
.catch(function (error) {
return swal("An error ocurred", 'There was an error with the request, please check the console for more information.', "error");
console.log(error);
});
}
);
};
panel.submitAlbum = function(){
axios.post('/api/albums', {
name: document.getElementById('albumName').value
})
.then(function (response) {
if(response.data.success === false){
if(response.data.description === 'No token provided') return panel.verifyToken(panel.token);
else return swal("An error ocurred", response.data.description, "error");
}
swal("Woohoo!", "Album was added successfully", "success");
panel.getAlbumsSidebar();
panel.getAlbums();
})
.catch(function (error) {
return swal("An error ocurred", 'There was an error with the request, please check the console for more information.', "error");
console.log(error);
});
};
panel.getAlbumsSidebar = function(){
axios.get('/api/albums/sidebar')
.then(function (response) {
if(response.data.success === false){
if(response.data.description === 'No token provided') return panel.verifyToken(panel.token);
else return swal("An error ocurred", response.data.description, "error");
}
var albumsContainer = document.getElementById('albumsContainer');
albumsContainer.innerHTML = '';
if(response.data.albums === undefined) return;
for(var album of response.data.albums){
li = document.createElement('li');
a = document.createElement('a');
a.id = album.id;
a.innerHTML = album.name;
a.addEventListener('click', function(){
panel.getAlbum(this);
});
li.appendChild(a);
albumsContainer.appendChild(li);
}
})
.catch(function (error) {
return swal("An error ocurred", 'There was an error with the request, please check the console for more information.', "error");
console.log(error);
});
};
panel.getAlbum = function(item){
panel.setActiveMenu(item);
panel.getUploads(item.id);
};
panel.changeToken = function(){
axios.get('/api/tokens')
.then(function (response) {
if(response.data.success === false){
if(response.data.description === 'No token provided') return panel.verifyToken(panel.token);
else return swal("An error ocurred", response.data.description, "error");
}
panel.page.innerHTML = '';
var container = document.createElement('div');
container.className = "container";
container.innerHTML = `
<h2 class="subtitle">Manage your token</h2>
<label class="label">Your current token:</label>
<p class="control has-addons">
<input id="token" readonly class="input is-expanded" type="text" placeholder="Your token" value="${response.data.token}">
<a id="getNewToken" class="button is-primary">Request new token</a>
</p>
`;
panel.page.appendChild(container);
document.getElementById('getNewToken').addEventListener('click', function(){
panel.getNewToken();
});
})
.catch(function (error) {
return swal("An error ocurred", 'There was an error with the request, please check the console for more information.', "error");
console.log(error);
});
};
panel.getNewToken = function(){
axios.post('/api/tokens/change')
.then(function (response) {
if(response.data.success === false){
if(response.data.description === 'No token provided') return panel.verifyToken(panel.token);
else return swal("An error ocurred", response.data.description, "error");
}
swal({
title: "Woohoo!",
text: 'Your token was changed successfully.',
type: "success"
}, function(){
localStorage.token = response.data.token;
location.reload();
});
})
.catch(function (error) {
return swal("An error ocurred", 'There was an error with the request, please check the console for more information.', "error");
console.log(error);
});
};
panel.changePassword = function(){
panel.page.innerHTML = '';
var container = document.createElement('div');
container.className = "container";
container.innerHTML = `
<h2 class="subtitle">Change your password</h2>
<label class="label">New password:</label>
<p class="control has-addons">
<input id="password" class="input is-expanded" type="password" placeholder="Your new password">
</p>
<label class="label">Confirm password:</label>
<p class="control has-addons">
<input id="passwordConfirm" class="input is-expanded" type="password" placeholder="Verify your new password">
<a id="sendChangePassword" class="button is-primary">Set new password</a>
</p>
`;
panel.page.appendChild(container);
document.getElementById('sendChangePassword').addEventListener('click', function(){
if (document.getElementById('password').value === document.getElementById('passwordConfirm').value) {
panel.sendNewPassword(document.getElementById('password').value);
} else {
swal({
title: "Password mismatch!",
text: 'Your passwords do not match, please try again.',
type: "error"
}, function() {
panel.changePassword();
});
}
});
};
panel.sendNewPassword = function(pass){
axios.post('/api/password/change', {password: pass})
.then(function (response) {
if(response.data.success === false){
if(response.data.description === 'No token provided') return panel.verifyToken(panel.token);
else return swal("An error ocurred", response.data.description, "error");
}
swal({
title: "Woohoo!",
text: 'Your password was changed successfully.',
type: "success"
}, function(){
location.reload();
});
})
.catch(function (error) {
return swal("An error ocurred", 'There was an error with the request, please check the console for more information.', "error");
console.log(error);
});
};
panel.setActiveMenu = function(item){
var menu = document.getElementById('menu');
var items = menu.getElementsByTagName('a');
for(var i = 0; i < items.length; i++)
items[i].className = "";
item.className = 'is-active';
};
window.onload = function () {
panel.preparePage();
};

View File

@ -1,220 +0,0 @@
var upload = {};
upload.isPrivate = true;
upload.token = localStorage.token;
upload.maxFileSize;
// add the album var to the upload so we can store the album id in there
upload.album;
upload.myDropzone;
upload.checkIfPublic = function(){
axios.get('/api/check')
.then(function (response) {
upload.isPrivate= response.data.private;
upload.maxFileSize = response.data.maxFileSize;
upload.preparePage();
})
.catch(function (error) {
swal("An error ocurred", 'There was an error with the request, please check the console for more information.', "error");
return console.log(error);
});
}
upload.preparePage = function(){
if(!upload.isPrivate) return upload.prepareUpload();
if(!upload.token) return document.getElementById('loginToUpload').style.display = 'inline-flex';
upload.verifyToken(upload.token, true);
};
upload.verifyToken = function(token, reloadOnError){
if(reloadOnError === undefined)
reloadOnError = false;
axios.post('/api/tokens/verify', {
token: token
})
.then(function (response) {
if(response.data.success === false){
swal({
title: "An error ocurred",
text: response.data.description,
type: "error"
}, function(){
if(reloadOnError){
localStorage.removeItem("token");
location.reload();
}
});
return;
}
localStorage.token = token;
upload.token = token;
return upload.prepareUpload();
})
.catch(function (error) {
swal("An error ocurred", 'There was an error with the request, please check the console for more information.', "error");
return console.log(error);
});
};
upload.prepareUpload = function(){
// I think this fits best here because we need to check for a valid token before we can get the albums
if (upload.token) {
var select = document.getElementById('albumSelect');
select.addEventListener('change', function() {
upload.album = select.value;
});
axios.get('/api/albums', { headers: { token: upload.token }})
.then(function(res) {
var albums = res.data.albums;
// if the user doesn't have any albums we don't really need to display
// an album selection
if (albums.length === 0) return;
// loop through the albums and create an option for each album
for (var i = 0; i < albums.length; i++) {
var opt = document.createElement('option');
opt.value = albums[i].id;
opt.innerHTML = albums[i].name;
select.appendChild(opt);
}
// display the album selection
document.getElementById('albumDiv').style.display = 'block';
})
.catch(function(e) {
swal("An error ocurred", 'There was an error with the request, please check the console for more information.', "error");
return console.log(e);
});
}
div = document.createElement('div');
div.id = 'dropzone';
div.innerHTML = 'Click here or drag and drop files';
div.style.display = 'flex';
document.getElementById('maxFileSize').innerHTML = 'Maximum upload size per file is ' + upload.maxFileSize;
document.getElementById('loginToUpload').style.display = 'none';
if(upload.token === undefined)
document.getElementById('loginLinkText').innerHTML = 'Create an account and keep track of your uploads';
document.getElementById('uploadContainer').appendChild(div);
upload.prepareDropzone();
};
upload.prepareDropzone = function(){
var previewNode = document.querySelector('#template');
previewNode.id = '';
var previewTemplate = previewNode.parentNode.innerHTML;
previewNode.parentNode.removeChild(previewNode);
var dropzone = new Dropzone('div#dropzone', {
url: '/api/upload',
paramName: 'files[]',
maxFilesize: upload.maxFileSize.slice(0, -2),
parallelUploads: 2,
uploadMultiple: false,
previewsContainer: 'div#uploads',
previewTemplate: previewTemplate,
createImageThumbnails: false,
maxFiles: 1000,
autoProcessQueue: true,
headers: {
'token': upload.token
},
init: function() {
upload.myDropzone = this;
this.on('addedfile', function(file) {
document.getElementById('uploads').style.display = 'block';
});
// add the selected albumid, if an album is selected, as a header
this.on('sending', function(file, xhr) {
if (upload.album) {
xhr.setRequestHeader('albumid', upload.album);
}
});
}
});
// Update the total progress bar
dropzone.on('uploadprogress', function(file, progress) {
file.previewElement.querySelector('.progress').setAttribute('value', progress);
file.previewElement.querySelector('.progress').innerHTML = progress + '%';
});
dropzone.on('success', function(file, response) {
// Handle the responseText here. For example, add the text to the preview element:
if (response.success === false) {
var p = document.createElement('p');
p.innerHTML = response.description;
file.previewTemplate.querySelector('.link').appendChild(p);
}
if (response.files[0].url) {
a = document.createElement('a');
a.href = response.files[0].url;
a.target = '_blank';
a.innerHTML = response.files[0].url;
file.previewTemplate.querySelector('.link').appendChild(a);
file.previewTemplate.querySelector('.progress').style.display = 'none';
}
});
upload.prepareShareX();
};
upload.prepareShareX = function(){
if (upload.token) {
var sharex_element = document.getElementById("ShareX");
var sharex_file = "{\r\n\
\"Name\": \"" + location.hostname + "\",\r\n\
\"DestinationType\": \"ImageUploader, FileUploader\",\r\n\
\"RequestType\": \"POST\",\r\n\
\"RequestURL\": \"" + location.origin + "/api/upload\",\r\n\
\"FileFormName\": \"files[]\",\r\n\
\"Headers\": {\r\n\
\"token\": \"" + upload.token + "\"\r\n\
},\r\n\
\"ResponseType\": \"Text\",\r\n\
\"URL\": \"$json:files[0].url$\",\r\n\
\"ThumbnailURL\": \"$json:files[0].url$\"\r\n\
}";
var sharex_blob = new Blob([sharex_file], {type: "application/octet-binary"});
sharex_element.setAttribute("href", URL.createObjectURL(sharex_blob));
sharex_element.setAttribute("download", location.hostname + ".sxcu");
}
};
//Handle image paste event
window.addEventListener('paste', function(event) {
var items = (event.clipboardData || event.originalEvent.clipboardData).items;
for (index in items) {
var item = items[index];
if (item.kind === 'file') {
var blob = item.getAsFile();
console.log(blob.type);
var file = new File([blob], "pasted-image."+blob.type.match(/(?:[^\/]*\/)([^;]*)/)[1]);
file.type = blob.type;
console.log(file);
upload.myDropzone.addFile(file);
}
}
});
window.onload = function () {
upload.checkIfPublic();
};

View File

@ -1,30 +0,0 @@
# https://www.cloudflare.com/ips/
# IPv4 Ranges
# https://www.cloudflare.com/ips-v4/
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 104.16.0.0/12;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 131.0.72.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
# IPv6 Ranges
# https://www.cloudflare.com/ips-v6/
set_real_ip_from 2400:cb00::/32;
set_real_ip_from 2405:8100::/32;
set_real_ip_from 2405:b500::/32;
set_real_ip_from 2606:4700::/32;
set_real_ip_from 2803:f800::/32;
set_real_ip_from 2c0f:f248::/32;
set_real_ip_from 2a06:98c0::/29;
real_ip_header CF-Connecting-IP;

View File

@ -1,56 +0,0 @@
const config = require('../config.js');
const routes = require('express').Router();
const db = require('knex')(config.database);
const path = require('path');
const utils = require('../controllers/utilsController.js');
routes.get('/a/:identifier', async (req, res, next) => {
let identifier = req.params.identifier;
if (identifier === undefined) return res.status(401).json({ success: false, description: 'No identifier provided' });
const album = await db.table('albums').where({ identifier, enabled: 1 }).first();
if (!album) return res.status(404).sendFile('404.html', { root: './pages/error/' });
const files = await db.table('files').select('name').where('albumid', album.id).orderBy('id', 'DESC');
let thumb = '';
const basedomain = config.domain;
for (let file of files) {
file.file = `${basedomain}/${file.name}`;
let ext = path.extname(file.name).toLowerCase();
if (utils.imageExtensions.includes(ext) || utils.videoExtensions.includes(ext)) {
file.thumb = `${basedomain}/thumbs/${file.name.slice(0, -ext.length)}.png`;
/*
If thumbnail for album is still not set, do it.
A potential improvement would be to let the user upload a specific image as an album cover
since embedding the first image could potentially result in nsfw content when pasting links.
*/
if (thumb === '') {
thumb = file.thumb;
}
file.thumb = `<img src="${file.thumb}"/>`;
} else {
file.thumb = `<h1 class="title">.${ext}</h1>`;
}
}
let enableDownload = false;
if (config.uploads.generateZips) enableDownload = true;
return res.render('album', {
layout: false,
title: album.name,
count: files.length,
thumb,
files,
identifier,
enableDownload
});
});
module.exports = routes;

View File

@ -1,37 +0,0 @@
const config = require('../config.js');
const routes = require('express').Router();
const uploadController = require('../controllers/uploadController');
const albumsController = require('../controllers/albumsController');
const tokenController = require('../controllers/tokenController');
const authController = require('../controllers/authController');
routes.get('/check', (req, res, next) => {
return res.json({
private: config.private,
maxFileSize: config.uploads.maxSize
});
});
routes.post('/login', (req, res, next) => authController.verify(req, res, next));
routes.post('/register', (req, res, next) => authController.register(req, res, next));
routes.post('/password/change', (req, res, next) => authController.changePassword(req, res, next));
routes.get('/uploads', (req, res, next) => uploadController.list(req, res, next));
routes.get('/uploads/:page', (req, res, next) => uploadController.list(req, res, next));
routes.post('/upload', (req, res, next) => uploadController.upload(req, res, next));
routes.post('/upload/delete', (req, res, next) => uploadController.delete(req, res, next));
routes.post('/upload/:albumid', (req, res, next) => uploadController.upload(req, res, next));
routes.get('/album/get/:identifier', (req, res, next) => albumsController.get(req, res, next));
routes.get('/album/zip/:identifier', (req, res, next) => albumsController.generateZip(req, res, next));
routes.get('/album/:id', (req, res, next) => uploadController.list(req, res, next));
routes.get('/album/:id/:page', (req, res, next) => uploadController.list(req, res, next));
routes.get('/albums', (req, res, next) => albumsController.list(req, res, next));
routes.get('/albums/:sidebar', (req, res, next) => albumsController.list(req, res, next));
routes.post('/albums', (req, res, next) => albumsController.create(req, res, next));
routes.post('/albums/delete', (req, res, next) => albumsController.delete(req, res, next));
routes.post('/albums/rename', (req, res, next) => albumsController.rename(req, res, next));
routes.get('/albums/test', (req, res, next) => albumsController.test(req, res, next));
routes.get('/tokens', (req, res, next) => tokenController.list(req, res, next));
routes.post('/tokens/verify', (req, res, next) => tokenController.verify(req, res, next));
routes.post('/tokens/change', (req, res, next) => tokenController.change(req, res, next));
module.exports = routes;

View File

@ -1,74 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="A pomf-like file uploading service that doesn't suck.">
<meta name="keywords" content="upload,lolisafe,file,images,hosting">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="apple-touch-icon" sizes="180x180" href="https://lolisafe.moe/images/icons/apple-touch-icon.png?v=XBreOJMe24">
<link rel="icon" type="image/png" href="https://lolisafe.moe/images/icons/favicon-32x32.png?v=XBreOJMe24" sizes="32x32">
<link rel="icon" type="image/png" href="https://lolisafe.moe/images/icons/favicon-16x16.png?v=XBreOJMe24" sizes="16x16">
<link rel="manifest" href="https://lolisafe.moe/images/icons/manifest.json?v=XBreOJMe24">
<link rel="mask-icon" href="https://lolisafe.moe/images/icons/safari-pinned-tab.svg?v=XBreOJMe24" color="#5bbad5">
<link rel="shortcut icon" href="https://lolisafe.moe/images/icons/favicon.ico?v=XBreOJMe24">
<meta name="apple-mobile-web-app-title" content="lolisafe">
<meta name="application-name" content="lolisafe">
<meta name="msapplication-config" content="https://lolisafe.moe/images/icons/browserconfig.xml?v=XBreOJMe24">
<meta name="theme-color" content="#ffffff">
<meta property="og:url" content="https://lolisafe.moe" />
<meta property="og:type" content="website" />
<meta property="og:title" content="{{ title }} | {{ count }} files" />
<meta property="og:description" content="lolisafe.moe | A small safe worth protecting." />
<meta property="og:image" content="{{ thumb }}" />
<meta property="og:image:secure_url" content="{{ thumb }}" />
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="{{ title }} | {{ count }} files">
<meta name="twitter:description" content="lolisafe.moe | A small safe worth protecting.">
<meta name="twitter:image" content="{{ thumb }}">
<meta name="twitter:image:src" content="{{ thumb }}">
<title>{{ title }}</title>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.3.0/css/bulma.min.css">
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/sweetalert/1.1.3/sweetalert.min.css">
<link rel="stylesheet" type="text/css" href="/css/style.css">
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/sweetalert/1.1.3/sweetalert.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.15.3/axios.min.js"></script>
</head>
<body>
<section class="hero is-fullheight">
<div class="hero-head">
<div class="container">
<div class="columns">
<div class="column is-9">
<h1 class="title" id='title' style='margin-top: 1.5rem;'>{{ title }}</h1>
<h1 class="subtitle" id='count'>{{ count }} files</h1>
</div>
<div class="column is-3" style="text-align: right; padding-top: 45px;">
{{#if enableDownload}}
<a class="button is-primary is-outlined" title="Download album" href="/api/album/zip/{{ identifier }}">Download Album</a>
{{/if}}
</div>
</div>
<hr>
</div>
</div>
<div class="hero-body">
<div class="container" id='container'>
<div class="columns is-multiline is-mobile" id="table">
{{#each files}}
<div class="column is-2">
<a href="{{ this.file }}" target="_blank">{{{ this.thumb }}}</a>
</div>
{{/each}}
</div>
</div>
</div>
</section>
</body>
</html>

2965
yarn.lock

File diff suppressed because it is too large Load Diff