Submitted to PyLints needs :3

This commit is contained in:
Michał Gdula 2023-03-04 13:45:26 +00:00
parent 4cfcd178f1
commit 7ed3b455dd
12 changed files with 509 additions and 460 deletions

View file

@ -1,115 +1,131 @@
print(""" """
___ _ _ ___ _ _
/ _ \\ _ __ | |_ _| | ___ __ _ ___ / _ \ _ __ | |_ _| | ___ __ _ ___
| | | | '_ \\| | | | | | / _ \\/ _` / __| | | | | '_ \| | | | | | / _ \/ _` / __|
| |_| | | | | | |_| | |__| __/ (_| \\__ \\ | |_| | | | | | |_| | |__| __/ (_| \__ \
\\___/|_| |_|_|\\__, |_____\\___|\\__, |___/ \___/|_| |_|_|\__, |_____\___|\__, |___/
|___/ |___/ |___/ |___/
Created by Fluffy Bean - Version 23.03.03 Created by Fluffy Bean - Version 23.03.04
""") """
from flask import Flask, render_template # Import system modules
import os
import sys
import logging
# Flask
from flask_compress import Compress from flask_compress import Compress
from flask import Flask, render_template
# Configuration
from dotenv import load_dotenv from dotenv import load_dotenv
import platformdirs import platformdirs
from gallery.logger import logger
logger.innit_logger()
import yaml import yaml
import os
from . import theme_manager
USER_DIR = platformdirs.user_config_dir('onlylegs')
INSTANCE_PATH = os.path.join(USER_DIR, 'instance')
# Check if any of the required files are missing # Check if any of the required files are missing
if not os.path.exists(platformdirs.user_config_dir('onlylegs')): if not os.path.exists(platformdirs.user_config_dir('onlylegs')):
from .setup import setup from . import setup
setup() setup.SetupApp()
user_dir = platformdirs.user_config_dir('onlylegs')
instance_path = os.path.join(user_dir, 'instance')
# Get environment variables # Get environment variables
if os.path.exists(os.path.join(user_dir, '.env')): if os.path.exists(os.path.join(USER_DIR, '.env')):
load_dotenv(os.path.join(user_dir, '.env')) load_dotenv(os.path.join(USER_DIR, '.env'))
print("Loaded environment variables") print("Loaded environment variables")
else: else:
print("No environment variables found!") print("No environment variables found!")
exit(1) sys.exit(1)
# Get config file # Get config file
if os.path.exists(os.path.join(user_dir, 'conf.yml')): if os.path.exists(os.path.join(USER_DIR, 'conf.yml')):
with open(os.path.join(user_dir, 'conf.yml'), 'r') as f: with open(os.path.join(USER_DIR, 'conf.yml'), encoding='utf-8') as f:
conf = yaml.load(f, Loader=yaml.FullLoader) conf = yaml.load(f, Loader=yaml.FullLoader)
print("Loaded gallery config") print("Loaded gallery config")
else: else:
print("No config file found!") print("No config file found!")
exit(1) sys.exit(1)
# Setup the logging config
LOGS_PATH = os.path.join(platformdirs.user_config_dir('onlylegs'), 'logs')
if not os.path.isdir(LOGS_PATH):
os.mkdir(LOGS_PATH)
logging.getLogger('werkzeug').disabled = True
logging.basicConfig(
filename=os.path.join(LOGS_PATH, 'only.log'),
level=logging.INFO,
datefmt='%Y-%m-%d %H:%M:%S',
format='%(asctime)s %(levelname)s %(name)s %(threadName)s : %(message)s',
encoding='utf-8')
def create_app(test_config=None): def create_app(test_config=None):
# create and configure the app """
app = Flask(__name__,instance_path=instance_path) Create and configure the main app
"""
app = Flask(__name__,instance_path=INSTANCE_PATH)
compress = Compress() compress = Compress()
# App configuration # App configuration
app.config.from_mapping( app.config.from_mapping(
SECRET_KEY=os.environ.get('FLASK_SECRET'), SECRET_KEY=os.environ.get('FLASK_SECRET'),
DATABASE=os.path.join(app.instance_path, 'gallery.sqlite'), DATABASE=os.path.join(app.instance_path, 'gallery.sqlite'),
UPLOAD_FOLDER=os.path.join(user_dir, 'uploads'), UPLOAD_FOLDER=os.path.join(USER_DIR, 'uploads'),
ALLOWED_EXTENSIONS=conf['upload']['allowed-extensions'], ALLOWED_EXTENSIONS=conf['upload']['allowed-extensions'],
MAX_CONTENT_LENGTH=1024 * 1024 * conf['upload']['max-size'], MAX_CONTENT_LENGTH=1024 * 1024 * conf['upload']['max-size'],
WEBSITE=conf['website'], WEBSITE=conf['website'],
) )
if test_config is None: if test_config is None:
# load the instance config, if it exists, when not testing
app.config.from_pyfile('config.py', silent=True) app.config.from_pyfile('config.py', silent=True)
else: else:
# load the test config if passed in
app.config.from_mapping(test_config) app.config.from_mapping(test_config)
# ensure the instance folder exists
try: try:
os.makedirs(app.instance_path) os.makedirs(app.instance_path)
except OSError: except OSError:
pass pass
# Load theme theme_manager.CompileTheme('default', app.root_path)
from . import sassy
sassy.compile('default', app.root_path)
@app.errorhandler(405) @app.errorhandler(405)
def method_not_allowed(e): def method_not_allowed(err):
error = '405' error = '405'
msg = e.description msg = err.description
return render_template('error.html', error=error, msg=e), 404 return render_template('error.html', error=error, msg=msg), 404
@app.errorhandler(404) @app.errorhandler(404)
def page_not_found(e): def page_not_found(err):
error = '404' error = '404'
msg = e.description msg = err.description
return render_template('error.html', error=error, msg=msg), 404 return render_template('error.html', error=error, msg=msg), 404
@app.errorhandler(403) @app.errorhandler(403)
def forbidden(e): def forbidden(err):
error = '403' error = '403'
msg = e.description msg = err.description
return render_template('error.html', error=error, msg=msg), 403 return render_template('error.html', error=error, msg=msg), 403
@app.errorhandler(410) @app.errorhandler(410)
def gone(e): def gone(err):
error = '410' error = '410'
msg = e.description msg = err.description
return render_template('error.html', error=error, msg=msg), 410 return render_template('error.html', error=error, msg=msg), 410
@app.errorhandler(500) @app.errorhandler(500)
def internal_server_error(e): def internal_server_error(err):
error = '500' error = '500'
msg = e.description msg = err.description
return render_template('error.html', error=error, msg=msg), 500 return render_template('error.html', error=error, msg=msg), 500
# Load login, registration and logout manager # Load login, registration and logout manager
@ -130,4 +146,4 @@ def create_app(test_config=None):
app.register_blueprint(api.blueprint) app.register_blueprint(api.blueprint)
compress.init_app(app) compress.init_app(app)
return app return app

View file

@ -1,38 +1,48 @@
from flask import Blueprint, current_app, send_from_directory, send_file, request, g, abort, flash, jsonify """
Onlylegs - API endpoints
Used intermally by the frontend and possibly by other applications
"""
from uuid import uuid4
import os
import io
import logging
from flask import (
Blueprint, current_app, send_from_directory, send_file, request, g, abort, flash, jsonify)
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from PIL import Image, ImageOps # ImageFilter
from sqlalchemy.orm import sessionmaker
from gallery.auth import login_required from gallery.auth import login_required
from . import db from . import db # Import db to create a session
from sqlalchemy.orm import sessionmaker
db_session = sessionmaker(bind=db.engine)
db_session = db_session()
from PIL import Image, ImageOps, ImageFilter
from . import metadata as mt from . import metadata as mt
from .logger import logger
from uuid import uuid4
import io
import os
blueprint = Blueprint('api', __name__, url_prefix='/api') blueprint = Blueprint('api', __name__, url_prefix='/api')
db_session = sessionmaker(bind=db.engine)
db_session = db_session()
@blueprint.route('/uploads/<file>', methods=['GET']) @blueprint.route('/uploads/<file>', methods=['GET'])
def uploads(file): def uploads(file):
"""
Returns a file from the uploads folder
w and h are the width and height of the image for resizing
f is whether to apply filters to the image, such as blurring NSFW images
"""
# Get args # Get args
width = request.args.get('w', default=0, type=int) # Width of image width = request.args.get('w', default=0, type=int) # Width of image
height = request.args.get('h', default=0, type=int) # Height of image height = request.args.get('h', default=0, type=int) # Height of image
filtered = request.args.get('f', default=False, type=bool) # Whether to apply filters to image, filtered = request.args.get('f', default=False, type=bool) # Whether to apply filters
# such as blur for NSFW images
# if no args are passed, return the raw file # if no args are passed, return the raw file
if width == 0 and height == 0 and not filtered: if width == 0 and height == 0 and not filtered:
return send_from_directory(current_app.config['UPLOAD_FOLDER'], if not os.path.exists(os.path.join(current_app.config['UPLOAD_FOLDER'],
secure_filename(file), secure_filename(file))):
as_attachment=True) abort(404)
return send_from_directory(current_app.config['UPLOAD_FOLDER'], file ,as_attachment=True)
# Of either width or height is 0, set it to the other value to keep aspect ratio # Of either width or height is 0, set it to the other value to keep aspect ratio
if width > 0 and height == 0: if width > 0 and height == 0:
@ -45,29 +55,28 @@ def uploads(file):
# Open image and set extension # Open image and set extension
try: try:
img = Image.open( img = Image.open(os.path.join(current_app.config['UPLOAD_FOLDER'],file))
os.path.join(current_app.config['UPLOAD_FOLDER'], except FileNotFoundError:
secure_filename(file))) logging.error('File not found: %s, possibly broken upload', file)
except Exception as e: abort(404)
logger.server(600, f"Error opening image: {e}") except Exception as err:
logging.error('Error opening image: %s', err)
abort(500) abort(500)
img_ext = os.path.splitext(secure_filename(file))[-1].lower().replace( img_ext = os.path.splitext(file)[-1].lower().replace('.', '')
'.', '')
img_ext = set_ext[img_ext] img_ext = set_ext[img_ext]
img_icc = img.info.get( # Get ICC profile as it alters colours when saving
"icc_profile") # Get ICC profile as it alters colours img_icc = img.info.get("icc_profile")
# Resize image and orientate correctly # Resize image and orientate correctly
img.thumbnail((width, height), Image.LANCZOS) img.thumbnail((width, height), Image.LANCZOS)
img = ImageOps.exif_transpose(img) img = ImageOps.exif_transpose(img)
# TODO: Add filters
# If has NSFW tag, blur image, etc. # If has NSFW tag, blur image, etc.
if filtered: if filtered:
#pass #img = img.filter(ImageFilter.GaussianBlur(20))
img = img.filter(ImageFilter.GaussianBlur(20)) pass
try: try:
img.save(buff, img_ext, icc_profile=img_icc) img.save(buff, img_ext, icc_profile=img_icc)
except OSError: except OSError:
@ -75,8 +84,8 @@ def uploads(file):
# Convert to RGB and try again # Convert to RGB and try again
img = img.convert('RGB') img = img.convert('RGB')
img.save(buff, img_ext, icc_profile=img_icc) img.save(buff, img_ext, icc_profile=img_icc)
except: except Exception as err:
logger.server(600, f"Error resizing image: {file}") logging.error('Could not resize image %s, error: %s', file, err)
abort(500) abort(500)
img.close() img.close()
@ -89,47 +98,51 @@ def uploads(file):
@blueprint.route('/upload', methods=['POST']) @blueprint.route('/upload', methods=['POST'])
@login_required @login_required
def upload(): def upload():
"""
Uploads an image to the server and saves it to the database
"""
form_file = request.files['file'] form_file = request.files['file']
form = request.form form = request.form
if not form_file: if not form_file:
return abort(404) return abort(404)
img_ext = os.path.splitext(secure_filename( img_ext = os.path.splitext(form_file.filename)[-1].replace('.', '').lower()
form_file.filename))[-1].replace('.', '').lower() img_name = f"GWAGWA_{str(uuid4())}.{img_ext}"
img_name = f"GWAGWA_{uuid4().__str__()}.{img_ext}"
if not img_ext in current_app.config['ALLOWED_EXTENSIONS'].keys(): if not img_ext in current_app.config['ALLOWED_EXTENSIONS'].keys():
logger.add(303, f"File extension not allowed: {img_ext}") logging.info('File extension not allowed: %s', img_ext)
abort(403) abort(403)
if os.path.isdir(current_app.config['UPLOAD_FOLDER']) == False: if os.path.isdir(current_app.config['UPLOAD_FOLDER']) is False:
os.mkdir(current_app.config['UPLOAD_FOLDER']) os.mkdir(current_app.config['UPLOAD_FOLDER'])
# Save to database # Save to database
try: try:
tr = db.posts(img_name, form['description'], form['alt'], g.user.id) db_session.add(db.posts(img_name, form['description'], form['alt'], g.user.id))
db_session.add(tr)
db_session.commit() db_session.commit()
except Exception as e: except Exception as err:
logger.server(600, f"Error saving to database: {e}") logging.error('Could not save to database: %s', err)
abort(500) abort(500)
# Save file # Save file
try: try:
form_file.save( form_file.save(
os.path.join(current_app.config['UPLOAD_FOLDER'], img_name)) os.path.join(current_app.config['UPLOAD_FOLDER'], img_name))
except Exception as e: except Exception as err:
logger.server(600, f"Error saving file: {e}") logging.error('Could not save file: %s', err)
abort(500) abort(500)
return 'Gwa Gwa' return 'Gwa Gwa'
@blueprint.route('/remove/<int:id>', methods=['POST']) @blueprint.route('/remove/<int:img_id>', methods=['POST'])
@login_required @login_required
def remove(id): def remove(img_id):
img = db_session.query(db.posts).filter_by(id=id).first() """
Deletes an image from the server and database
"""
img = db_session.query(db.posts).filter_by(id=img_id).first()
if img is None: if img is None:
abort(404) abort(404)
@ -137,28 +150,32 @@ def remove(id):
abort(403) abort(403)
try: try:
os.remove( os.remove(os.path.join(current_app.config['UPLOAD_FOLDER'],img.file_name))
os.path.join(current_app.config['UPLOAD_FOLDER'], except FileNotFoundError:
img.file_name)) # File was already deleted or doesn't exist
except Exception as e: logging.warning('File not found: %s, already deleted or never existed', img.file_name)
logger.server(600, f"Error removing file: {e}") except Exception as err:
logging.error('Could not remove file: %s', err)
abort(500) abort(500)
try: try:
db_session.query(db.posts).filter_by(id=id).delete() db_session.query(db.posts).filter_by(id=img_id).delete()
db_session.commit() db_session.commit()
except Exception as e: except Exception as err:
logger.server(600, f"Error removing from database: {e}") logging.error('Could not remove from database: %s', err)
abort(500) abort(500)
logger.server(301, f"Removed image {id}") logging.info('Removed image (%s) %s', img_id, img.file_name)
flash(['Image was all in Le Head!', 1]) flash(['Image was all in Le Head!', 1])
return 'Gwa Gwa' return 'Gwa Gwa'
@blueprint.route('/metadata/<int:id>', methods=['GET']) @blueprint.route('/metadata/<int:img_id>', methods=['GET'])
def metadata(id): def metadata(img_id):
img = db_session.query(db.posts).filter_by(id=id).first() """
Yoinks metadata from an image
"""
img = db_session.query(db.posts).filter_by(id=img_id).first()
if img is None: if img is None:
abort(404) abort(404)
@ -172,12 +189,15 @@ def metadata(id):
@blueprint.route('/logfile') @blueprint.route('/logfile')
@login_required @login_required
def logfile(): def logfile():
filename = logger.filename() """
Gets the log file and returns it as a JSON object
"""
filename = logging.getLoggerClass().root.handlers[0].baseFilename
log_dict = {} log_dict = {}
i = 0 i = 0
with open(filename) as f: with open(filename, encoding='utf-8') as file:
for line in f: for line in file:
line = line.split(' : ') line = line.split(' : ')
event = line[0].strip().split(' ') event = line[0].strip().split(' ')
@ -194,11 +214,14 @@ def logfile():
'code': int(message[1:4]), 'code': int(message[1:4]),
'message': message[5:].strip() 'message': message[5:].strip()
} }
except: except ValueError:
message_data = {'code': 0, 'message': message} message_data = {'code': 0, 'message': message}
except Exception as err:
logging.error('Could not parse log file: %s', err)
abort(500)
log_dict[i] = {'event': event_data, 'message': message_data} log_dict[i] = {'event': event_data, 'message': message_data}
i += 1 # Line number, starts at 0 i += 1 # Line number, starts at 0
return jsonify(log_dict) return jsonify(log_dict)

View file

@ -1,56 +1,84 @@
"""
OnlyLegs - Authentification
User registration, login and logout and locking access to pages behind a login
"""
import re
import uuid
import logging
import functools import functools
from flask import Blueprint, flash, g, redirect, request, session, url_for, abort, jsonify, current_app from flask import Blueprint, flash, g, redirect, request, session, url_for, abort, jsonify
from werkzeug.security import check_password_hash, generate_password_hash from werkzeug.security import check_password_hash, generate_password_hash
from gallery import db
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from sqlalchemy import exc
from gallery import db
blueprint = Blueprint('auth', __name__, url_prefix='/auth')
db_session = sessionmaker(bind=db.engine) db_session = sessionmaker(bind=db.engine)
db_session = db_session() db_session = db_session()
from .logger import logger
import re def login_required(view):
import uuid """
Decorator to check if a user is logged in before accessing a page
"""
@functools.wraps(view)
def wrapped_view(**kwargs):
if g.user is None or session.get('uuid') is None:
logging.error('Authentification failed')
session.clear()
return redirect(url_for('gallery.index'))
blueprint = Blueprint('auth', __name__, url_prefix='/auth') return view(**kwargs)
return wrapped_view
@blueprint.before_app_request @blueprint.before_app_request
def load_logged_in_user(): def load_logged_in_user():
"""
Runs before every request and checks if a user is logged in
"""
user_id = session.get('user_id') user_id = session.get('user_id')
user_uuid = session.get('uuid') user_uuid = session.get('uuid')
if user_id is None or user_uuid is None: if user_id is None or user_uuid is None:
# This is not needed as the user is not logged in anyway, also spams the server logs with useless data
#add_log(103, 'Auth error before app request')
g.user = None g.user = None
session.clear() session.clear()
else: else:
is_alive = db_session.query(db.sessions).filter_by(session_uuid=user_uuid).first() is_alive = db_session.query(db.sessions).filter_by(session_uuid=user_uuid).first()
if is_alive is None: if is_alive is None:
logger.add(103, 'Session expired') logging.info('Session expired')
flash(['Session expired!', '3']) flash(['Session expired!', '3'])
session.clear() session.clear()
else: else:
g.user = db_session.query(db.users).filter_by(id=user_id).first() g.user = db_session.query(db.users).filter_by(id=user_id).first()
@blueprint.route('/register', methods=['POST']) @blueprint.route('/register', methods=['POST'])
def register(): def register():
"""
Register a new user
"""
username = request.form['username'] username = request.form['username']
email = request.form['email'] email = request.form['email']
password = request.form['password'] password = request.form['password']
password_repeat = request.form['password-repeat'] password_repeat = request.form['password-repeat']
error = [] error = []
if not username: email_regex = re.compile(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b')
error.append('Username is empty!') username_regex = re.compile(r'\b[A-Za-z0-9._%+-]+\b')
if not email:
error.append('Email is empty!') if not username or not username_regex.match(username):
elif not re.match( error.append('Username is invalid!')
r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', email):
if not email or not email_regex.match(email):
error.append('Email is invalid!') error.append('Email is invalid!')
if not password: if not password:
@ -59,73 +87,77 @@ def register():
error.append('Password is too short! Longer than 8 characters pls') error.append('Password is too short! Longer than 8 characters pls')
if not password_repeat: if not password_repeat:
error.append('Password repeat is empty!') error.append('Enter password again!')
elif password_repeat != password: elif password_repeat != password:
error.append('Passwords do not match!') error.append('Passwords do not match!')
if not error: if error:
try: return jsonify(error)
tr = db.users(username, email, generate_password_hash(password))
db_session.add(tr)
db_session.commit()
except Exception as e:
error.append(f"User {username} is already registered!")
else:
logger.add(103, f"User {username} registered")
return 'gwa gwa'
return jsonify(error)
try:
db_session.add(db.users(username, email, generate_password_hash(password)))
db_session.commit()
except exc.IntegrityError:
return f'User {username} is already registered!'
except Exception as err:
logging.error('User %s could not be registered: %s', username, err)
return 'Something went wrong!'
logging.info('User %s registered', username)
return 'gwa gwa'
@blueprint.route('/login', methods=['POST']) @blueprint.route('/login', methods=['POST'])
def login(): def login():
"""
Log in a registered user by adding the user id to the session
"""
username = request.form['username'] username = request.form['username']
password = request.form['password'] password = request.form['password']
error = None
user = db_session.query(db.users).filter_by(username=username).first() user = db_session.query(db.users).filter_by(username=username).first()
error = []
if user is None: if user is None:
logger.add(101, f"User {username} does not exist from {request.remote_addr}") logging.error('User %s does not exist. Login attempt from %s',
abort(403) username, request.remote_addr)
error.append('Username or Password is incorrect!')
elif not check_password_hash(user.password, password): elif not check_password_hash(user.password, password):
logger.add(102, f"User {username} password error from {request.remote_addr}") logging.error('User %s entered wrong password. Login attempt from %s',
username, request.remote_addr)
error.append('Username or Password is incorrect!')
if error:
abort(403) abort(403)
try: try:
session.clear() session.clear()
session['user_id'] = user.id session['user_id'] = user.id
session['uuid'] = str(uuid.uuid4()) session['uuid'] = str(uuid.uuid4())
tr = db.sessions(user.id, session.get('uuid'), request.remote_addr, request.user_agent.string, 1) db_session.add(db.sessions(user.id,
db_session.add(tr) session.get('uuid'),
request.remote_addr,
request.user_agent.string,
1))
db_session.commit() db_session.commit()
except error as err: except Exception as err:
logger.add(105, f"User {username} auth error: {err}") logging.error('User %s could not be logged in: %s', username, err)
abort(500) abort(500)
if error is None: logging.info('User %s logged in from %s', username, request.remote_addr)
logger.add(100, f"User {username} logged in from {request.remote_addr}") flash(['Logged in successfully!', '4'])
flash(['Logged in successfully!', '4']) return 'gwa gwa'
return 'gwa gwa'
abort(500)
@blueprint.route('/logout') @blueprint.route('/logout')
def logout(): def logout():
logger.add(103, f"User {g.user.username} - id: {g.user.id} logged out") """
Clear the current session, including the stored user id
"""
logging.info('User (%s) %s logged out', session.get('user_id'), g.user.username)
session.clear() session.clear()
return redirect(url_for('index')) return redirect(url_for('index'))
def login_required(view):
@functools.wraps(view)
def wrapped_view(**kwargs):
if g.user is None or session.get('uuid') is None:
logger.add(103, "Auth error")
session.clear()
return redirect(url_for('gallery.index'))
return view(**kwargs)
return wrapped_view

View file

@ -1,6 +1,10 @@
"""
OnlyLegs - Database
Database models and functions for SQLAlchemy
"""
import os import os
import platformdirs
from datetime import datetime from datetime import datetime
import platformdirs
from sqlalchemy import create_engine, Column, Integer, String, Boolean, DateTime, ForeignKey from sqlalchemy import create_engine, Column, Integer, String, Boolean, DateTime, ForeignKey
from sqlalchemy.orm import declarative_base, relationship from sqlalchemy.orm import declarative_base, relationship
@ -11,7 +15,11 @@ engine = create_engine(f'sqlite:///{path_to_db}', echo=False)
base = declarative_base() base = declarative_base()
class users (base): class users (base): # pylint: disable=too-few-public-methods, C0103
"""
User table
Joins with post, groups, session and log
"""
__tablename__ = 'users' __tablename__ = 'users'
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
@ -19,7 +27,7 @@ class users (base):
email = Column(String, unique=True, nullable=False) email = Column(String, unique=True, nullable=False)
password = Column(String, nullable=False) password = Column(String, nullable=False)
created_at = Column(DateTime, nullable=False) created_at = Column(DateTime, nullable=False)
posts = relationship('posts') posts = relationship('posts')
groups = relationship('groups') groups = relationship('groups')
session = relationship('sessions') session = relationship('sessions')
@ -30,19 +38,24 @@ class users (base):
self.email = email self.email = email
self.password = password self.password = password
self.created_at = datetime.now() self.created_at = datetime.now()
class posts (base):
class posts (base): # pylint: disable=too-few-public-methods, C0103
"""
Post table
Joins with group_junction
"""
__tablename__ = 'posts' __tablename__ = 'posts'
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
file_name = Column(String, unique=True, nullable=False) file_name = Column(String, unique=True, nullable=False)
description = Column(String, nullable=False) description = Column(String, nullable=False)
alt = Column(String, nullable=False) alt = Column(String, nullable=False)
author_id = Column(Integer, ForeignKey('users.id')) author_id = Column(Integer, ForeignKey('users.id'))
created_at = Column(DateTime, nullable=False) created_at = Column(DateTime, nullable=False)
junction = relationship('group_junction') junction = relationship('group_junction')
def __init__(self, file_name, description, alt, author_id): def __init__(self, file_name, description, alt, author_id):
self.file_name = file_name self.file_name = file_name
self.description = description self.description = description
@ -50,84 +63,108 @@ class posts (base):
self.author_id = author_id self.author_id = author_id
self.created_at = datetime.now() self.created_at = datetime.now()
class groups (base):
class groups (base): # pylint: disable=too-few-public-methods, C0103
"""
Group table
Joins with group_junction
"""
__tablename__ = 'groups' __tablename__ = 'groups'
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
name = Column(String, nullable=False) name = Column(String, nullable=False)
description = Column(String, nullable=False) description = Column(String, nullable=False)
author_id = Column(Integer, ForeignKey('users.id')) author_id = Column(Integer, ForeignKey('users.id'))
created_at = Column(DateTime, nullable=False) created_at = Column(DateTime, nullable=False)
junction = relationship('group_junction') junction = relationship('group_junction')
def __init__(self, name, description, author_id): def __init__(self, name, description, author_id):
self.name = name self.name = name
self.description = description self.description = description
self.author_id = author_id self.author_id = author_id
self.created_at = datetime.now() self.created_at = datetime.now()
class group_junction (base):
class group_junction (base): # pylint: disable=too-few-public-methods, C0103
"""
Junction table for posts and groups
Joins with posts and groups
"""
__tablename__ = 'group_junction' __tablename__ = 'group_junction'
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
group_id = Column(Integer, ForeignKey('groups.id')) group_id = Column(Integer, ForeignKey('groups.id'))
post_id = Column(Integer, ForeignKey('posts.id')) post_id = Column(Integer, ForeignKey('posts.id'))
def __init__(self, group_id, post_id): def __init__(self, group_id, post_id):
self.group_id = group_id self.group_id = group_id
self.post_id = post_id self.post_id = post_id
class sessions (base):
class sessions (base): # pylint: disable=too-few-public-methods, C0103
"""
Session table
Joins with user
"""
__tablename__ = 'sessions' __tablename__ = 'sessions'
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey('users.id')) user_id = Column(Integer, ForeignKey('users.id'))
session_uuid = Column(String, nullable=False) session_uuid = Column(String, nullable=False)
ip = Column(String, nullable=False) ip_address = Column(String, nullable=False)
user_agent = Column(String, nullable=False) user_agent = Column(String, nullable=False)
active = Column(Boolean, nullable=False) active = Column(Boolean, nullable=False)
created_at = Column(DateTime, nullable=False) created_at = Column(DateTime, nullable=False)
def __init__(self, user_id, session_uuid, ip, user_agent, active): def __init__(self, user_id, session_uuid, ip_address, user_agent, active): # pylint: disable=too-many-arguments, C0103
self.user_id = user_id self.user_id = user_id
self.session_uuid = session_uuid self.session_uuid = session_uuid
self.ip = ip self.ip_address = ip_address
self.user_agent = user_agent self.user_agent = user_agent
self.active = active self.active = active
self.created_at = datetime.now() self.created_at = datetime.now()
class logs (base):
class logs (base): # pylint: disable=too-few-public-methods, C0103
"""
Log table
Joins with user
"""
__tablename__ = 'logs' __tablename__ = 'logs'
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey('users.id')) user_id = Column(Integer, ForeignKey('users.id'))
ip = Column(String, nullable=False) ip_address = Column(String, nullable=False)
code = Column(Integer, nullable=False) code = Column(Integer, nullable=False)
msg = Column(String, nullable=False) msg = Column(String, nullable=False)
created_at = Column(DateTime, nullable=False) created_at = Column(DateTime, nullable=False)
def __init__(self, user_id, ip, code, msg): def __init__(self, user_id, ip_address, code, msg):
self.user_id = user_id self.user_id = user_id
self.ip = ip self.ip_address = ip_address
self.code = code self.code = code
self.msg = msg self.msg = msg
self.created_at = datetime.now() self.created_at = datetime.now()
class bans (base):
class bans (base): # pylint: disable=too-few-public-methods, C0103
"""
Bans table
"""
__tablename__ = 'bans' __tablename__ = 'bans'
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
ip = Column(String, nullable=False) ip_address = Column(String, nullable=False)
code = Column(Integer, nullable=False) code = Column(Integer, nullable=False)
msg = Column(String, nullable=False) msg = Column(String, nullable=False)
created_at = Column(DateTime, nullable=False) created_at = Column(DateTime, nullable=False)
def __init__(self, ip, code, msg): def __init__(self, ip_address, code, msg):
self.ip = ip self.ip_address = ip_address
self.code = code self.code = code
self.msg = msg self.msg = msg
self.created_at = datetime.now() self.created_at = datetime.now()
base.metadata.create_all(engine) base.metadata.create_all(engine)

View file

@ -1,112 +0,0 @@
import logging
import os
from datetime import datetime
import platformdirs
# Prevent werkzeug from logging
logging.getLogger('werkzeug').disabled = True
class logger:
def innit_logger():
filepath = os.path.join(platformdirs.user_config_dir('onlylegs'), 'logs')
#filename = f'onlylogs_{datetime.now().strftime("%Y%m%d")}.log'
filename = 'only.log'
if not os.path.isdir(filepath):
os.mkdir(filepath)
logging.basicConfig(
filename=os.path.join(filepath, filename),
level=logging.INFO,
datefmt='%Y-%m-%d %H:%M:%S',
format=
'%(asctime)s %(levelname)s %(name)s %(threadName)s : %(message)s',
encoding='utf-8')
"""
Login and Auth error codes
--------------------------
100: Login
101: Login attempt
102: Login attempt (password error)
103: Logout
104: Registration
105: Auth error
Account error codes - User actions
----------------------------------
200: Account password reset
201: Account email change
202: Account delete
203: Account error
Image error codes
-----------------
300: Image upload
301: Image delete
302: Image edit
303: Image error
Group error codes
-----------------
400: Group create
401: Group delete
402: Group edit
403: Group error
User error codes - Admin actions
--------------------------------
500: User delete
501: User edit
502: User ban
503: User unban
504: User permission change
505: User error
Server and Website errors - Internal
------------------------------------
600: Server error
601: Server crash
602: Website error
603: Website crash
604: Maintenance
605: Startup
606: Other
621: :3
"""
def add(error, message):
# Allowed error codes, as listed above
log_levels = [
100, 101, 102, 103, 104, 105, 200, 201, 202, 203, 300, 301, 302,
303, 400, 401, 402, 403, 500, 501, 502, 503, 504, 505
]
if error in log_levels:
logging.log(logging.INFO, f'[{error}] {message}')
else:
logging.log(logging.WARN, f'[606] Improper use of error code {error}')
def server(error, message):
log_levels = {
600: logging.ERROR,
601: logging.CRITICAL,
602: logging.ERROR,
603: logging.CRITICAL,
604: logging.DEBUG,
605: logging.DEBUG,
606: logging.INFO,
621: logging.INFO,
}
if error in log_levels:
logging.log(log_levels[error], f'[{error}] {message}')
else:
logging.log(logging.WARN, f'[606] Invalid error code {error}')
def filename():
handler = logging.getLogger().handlers[0]
filename = handler.baseFilename
return filename

View file

@ -1,23 +1,27 @@
from flask import Blueprint, render_template, current_app """
from werkzeug.exceptions import abort Onlylegs Gallery - Routing
from werkzeug.utils import secure_filename """
from gallery.auth import login_required
from . import db
from sqlalchemy.orm import sessionmaker
db_session = sessionmaker(bind=db.engine)
db_session = db_session()
from . import metadata as mt
import os import os
from flask import Blueprint, render_template, current_app
from werkzeug.exceptions import abort
from sqlalchemy.orm import sessionmaker
from . import db
from . import metadata as mt
blueprint = Blueprint('gallery', __name__) blueprint = Blueprint('gallery', __name__)
db_session = sessionmaker(bind=db.engine)
db_session = db_session()
@blueprint.route('/') @blueprint.route('/')
def index(): def index():
"""
Home page of the website, shows the feed of latest images
"""
images = db_session.query(db.posts).order_by(db.posts.id.desc()).all() images = db_session.query(db.posts).order_by(db.posts.id.desc()).all()
return render_template('index.html', return render_template('index.html',
@ -26,10 +30,12 @@ def index():
name=current_app.config['WEBSITE']['name'], name=current_app.config['WEBSITE']['name'],
motto=current_app.config['WEBSITE']['motto']) motto=current_app.config['WEBSITE']['motto'])
@blueprint.route('/image/<int:image_id>')
@blueprint.route('/image/<int:id>') def image(image_id):
def image(id): """
img = db_session.query(db.posts).filter_by(id=id).first() Image view, shows the image and its metadata
"""
img = db_session.query(db.posts).filter_by(id=image_id).first()
if img is None: if img is None:
abort(404) abort(404)
@ -39,28 +45,30 @@ def image(id):
return render_template('image.html', image=img, exif=exif) return render_template('image.html', image=img, exif=exif)
@blueprint.route('/group') @blueprint.route('/group')
def groups(): def groups():
"""
Group overview, shows all image groups
"""
return render_template('group.html', group_id='gwa gwa') return render_template('group.html', group_id='gwa gwa')
@blueprint.route('/group/<int:group_id>')
@blueprint.route('/group/<int:id>') def group(group_id):
def group(id): """
return render_template('group.html', group_id=id) Group view, shows all images in a group
"""
return render_template('group.html', group_id=group_id)
@blueprint.route('/upload')
@login_required
def upload():
return render_template('upload.html')
@blueprint.route('/profile') @blueprint.route('/profile')
def profile(): def profile():
"""
Profile overview, shows all profiles on the onlylegs gallery
"""
return render_template('profile.html', user_id='gwa gwa') return render_template('profile.html', user_id='gwa gwa')
@blueprint.route('/profile/<int:user_id>')
@blueprint.route('/profile/<int:id>') def profile_id(user_id):
def profile_id(id): """
return render_template('profile.html', user_id=id) Shows user ofa given id, displays their uploads and other info
"""
return render_template('profile.html', user_id=user_id)

View file

@ -1,67 +0,0 @@
import datetime
now = datetime.datetime.now()
import sys
import shutil
import os
import sass
class compile():
def __init__(self, theme, dir):
print(f"Loading '{theme}' theme...")
theme_path = os.path.join(dir, 'themes', theme)
font_path = os.path.join(dir, 'themes', theme, 'fonts')
dest = os.path.join(dir, 'static', 'theme')
# print(f"Theme path: {theme_path}")
if os.path.exists(theme_path):
if os.path.exists(os.path.join(theme_path, 'style.scss')):
theme_path = os.path.join(theme_path, 'style.scss')
elif os.path.exists(os.path.join(theme_path, 'style.sass')):
theme_path = os.path.join(theme_path, 'style.sass')
else:
print("Theme does not contain a style file!")
sys.exit(1)
self.sass = sass
self.loadTheme(theme_path, dest)
self.loadFonts(font_path, dest)
else:
print("No theme found!")
sys.exit(1)
print(f"{now.hour}:{now.minute}:{now.second} - Done!\n")
def loadTheme(self, theme, dest):
with open(os.path.join(dest, 'style.css'), 'w') as f:
try:
f.write(
self.sass.compile(filename=theme,
output_style='compressed'))
print("Compiled successfully!")
except self.sass.CompileError as e:
print("Failed to compile!\n", e)
sys.exit(1)
def loadFonts(self, source, dest):
dest = os.path.join(dest, 'fonts')
if os.path.exists(dest):
print("Updating fonts...")
try:
shutil.rmtree(dest)
except Exception as e:
print("Failed to remove old fonts!\n", e)
sys.exit(1)
try:
shutil.copytree(source, dest)
# print("Copied fonts to:", dest)
print("Copied new fonts!")
except Exception as e:
print("Failed to copy fonts!\n", e)
sys.exit(1)

View file

@ -1,31 +1,42 @@
from flask import Blueprint, render_template, url_for """
from werkzeug.exceptions import abort OnlyLegs - Settings page
"""
from flask import Blueprint, render_template
from gallery.auth import login_required from gallery.auth import login_required
from datetime import datetime
now = datetime.now()
blueprint = Blueprint('settings', __name__, url_prefix='/settings') blueprint = Blueprint('settings', __name__, url_prefix='/settings')
@blueprint.route('/') @blueprint.route('/')
@login_required @login_required
def general(): def general():
"""
General settings page
"""
return render_template('settings/general.html') return render_template('settings/general.html')
@blueprint.route('/server') @blueprint.route('/server')
@login_required @login_required
def server(): def server():
"""
Server settings page
"""
return render_template('settings/server.html') return render_template('settings/server.html')
@blueprint.route('/account') @blueprint.route('/account')
@login_required @login_required
def account(): def account():
"""
Account settings page
"""
return render_template('settings/account.html') return render_template('settings/account.html')
@blueprint.route('/logs') @blueprint.route('/logs')
@login_required @login_required
def logs(): def logs():
return render_template('settings/logs.html') """
Logs settings page
"""
return render_template('settings/logs.html')

View file

@ -1,49 +1,66 @@
# Import dependencies """
import platformdirs OnlyLegs - Setup
Runs when the app detects that there is no user directory
"""
import os import os
import sys
import platformdirs
import yaml import yaml
class setup: USER_DIR = platformdirs.user_config_dir('onlylegs')
def __init__(self):
self.user_dir = platformdirs.user_config_dir('onlylegs')
class SetupApp:
"""
Setup the application on first run
"""
def __init__(self):
"""
Main setup function
"""
print("Running setup...") print("Running setup...")
if not os.path.exists(self.user_dir): if not os.path.exists(USER_DIR):
self.make_dir() self.make_dir()
if not os.path.exists(os.path.join(self.user_dir, '.env')): if not os.path.exists(os.path.join(USER_DIR, '.env')):
self.make_env() self.make_env()
if not os.path.exists(os.path.join(self.user_dir, 'conf.yml')): if not os.path.exists(os.path.join(USER_DIR, 'conf.yml')):
self.make_yaml() self.make_yaml()
def make_dir(self): def make_dir(self):
"""
Create the user directory
"""
try: try:
os.makedirs(self.user_dir) os.makedirs(USER_DIR)
os.makedirs(os.path.join(self.user_dir, 'instance')) os.makedirs(os.path.join(USER_DIR, 'instance'))
print("Created user directory at:", self.user_dir) print("Created user directory at:", USER_DIR)
except Exception as e: except Exception as err:
print("Error creating user directory:", e) print("Error creating user directory:", err)
exit(1) # exit with error code sys.exit(1) # exit with error code
def make_env(self): def make_env(self):
# Create .env file with default values """
Create the .env file with default values
"""
env_conf = { env_conf = {
'FLASK_SECRETE': 'dev', 'FLASK_SECRETE': 'dev',
} }
try: try:
with open(os.path.join(self.user_dir, '.env'), 'w') as f: with open(os.path.join(USER_DIR, '.env'), encoding='utf-8') as file:
for key, value in env_conf.items(): for key, value in env_conf.items():
f.write(f"{key}={value}\n") file.write(f"{key}={value}\n")
print("Created environment variables") print("Created environment variables")
except Exception as e: except Exception as err:
print("Error creating environment variables:", e) print("Error creating environment variables:", err)
exit(1) sys.exit(1)
print("Generated default .env file. EDIT IT BEFORE RUNNING THE APP AGAIN!") print("Generated default .env file. EDIT IT BEFORE RUNNING THE APP AGAIN!")
def make_yaml(self): def make_yaml(self):
# Create yaml config file with default values """
Create the YAML config file with default values
"""
yaml_conf = { yaml_conf = {
'admin': { 'admin': {
'name': 'Real Person', 'name': 'Real Person',
@ -71,11 +88,11 @@ class setup:
}, },
} }
try: try:
with open(os.path.join(self.user_dir, 'conf.yml'), 'w') as f: with open(os.path.join(USER_DIR, 'conf.yml'), encoding='utf-8') as file:
yaml.dump(yaml_conf, f, default_flow_style=False) yaml.dump(yaml_conf, file, default_flow_style=False)
print("Created default gallery config") print("Created default gallery config")
except Exception as e: except Exception as err:
print("Error creating default gallery config:", e) print("Error creating default gallery config:", err)
exit(1) sys.exit(1)
print("Generated default YAML config. EDIT IT BEFORE RUNNING THE APP AGAIN!") print("Generated default YAML config. EDIT IT BEFORE RUNNING THE APP AGAIN!")

78
gallery/theme_manager.py Normal file
View file

@ -0,0 +1,78 @@
"""
OnlyLegs - Theme Manager
"""
import os
import sys
import shutil
from datetime import datetime
import sass
class CompileTheme():
"""
Compiles the theme into the static folder
"""
def __init__(self, theme_name, app_path):
"""
Initialize the theme manager
Compiles the theme into the static folder and loads the fonts
"""
print(f"Loading '{theme_name}' theme...")
theme_path = os.path.join(app_path, 'themes', theme_name)
theme_dest = os.path.join(app_path, 'static', 'theme')
if not os.path.exists(theme_path):
print("Theme does not exist!")
sys.exit(1)
self.load_sass(theme_path, theme_dest)
self.load_fonts(theme_path, theme_dest)
now = datetime.now()
print(f"{now.hour}:{now.minute}:{now.second} - Done!\n")
def load_sass(self, source_path, css_dest):
"""
Compile the sass (or scss) file into css and save it to the static folder
"""
if os.path.join(source_path, 'style.sass'):
sass_path = os.path.join(source_path, 'style.sass')
elif os.path.join(source_path, 'style.scss'):
sass_path = os.path.join(source_path, 'style.scss')
else:
print("No sass file found!")
sys.exit(1)
with open(os.path.join(css_dest, 'style.css'), encoding='utf-8') as file:
try:
file.write(sass.compile(filename=sass_path,output_style='compressed'))
except sass.CompileError as err:
print("Failed to compile!\n", err)
sys.exit(1)
print("Compiled successfully!")
def load_fonts(self, source_path, font_dest):
"""
Copy the fonts folder to the static folder
"""
# Append fonts to the destination path
source_path = os.path.join(source_path, 'fonts')
font_dest = os.path.join(font_dest, 'fonts')
if os.path.exists(font_dest):
print("Updating fonts...")
try:
shutil.rmtree(font_dest)
except Exception as err:
print("Failed to remove old fonts!\n", err)
sys.exit(1)
try:
shutil.copytree(source_path, font_dest)
print("Copied new fonts!")
except Exception as err:
print("Failed to copy fonts!\n", err)
sys.exit(1)

6
poetry.lock generated
View file

@ -465,14 +465,14 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa
[[package]] [[package]]
name = "platformdirs" name = "platformdirs"
version = "3.0.0" version = "3.1.0"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "platformdirs-3.0.0-py3-none-any.whl", hash = "sha256:b1d5eb14f221506f50d6604a561f4c5786d9e80355219694a1b244bcd96f4567"}, {file = "platformdirs-3.1.0-py3-none-any.whl", hash = "sha256:13b08a53ed71021350c9e300d4ea8668438fb0046ab3937ac9a29913a1a1350a"},
{file = "platformdirs-3.0.0.tar.gz", hash = "sha256:8a1228abb1ef82d788f74139988b137e78692984ec7b08eaa6c65f1723af28f9"}, {file = "platformdirs-3.1.0.tar.gz", hash = "sha256:accc3665857288317f32c7bebb5a8e482ba717b474f3fc1d18ca7f9214be0cef"},
] ]
[package.extras] [package.extras]

View file

@ -24,3 +24,9 @@ SQLAlchemy = "^2.0.3"
[build-system] [build-system]
requires = ["poetry-core"] requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"
[tool.pylint.messages_control]
# C0415: Flask uses it to register blueprints
# W1401: Anomalous backslash in string used in __init__
# W0718: Exception are logged so we don't need to raise them
disable = "C0415, W1401, W0718"