import json
+import logging
import os
import shutil
import sqlite3
import uuid
from dataclasses import dataclass
from datetime import datetime, timezone
+from logging.handlers import RotatingFileHandler
from pathlib import Path, PosixPath
import bcrypt
send_from_directory, flash
from werkzeug.utils import secure_filename
+DATA_DIR = Path(".")
+
################################################################################
-# Routes
+# Logging
################################################################################
-DATA_DIR = Path(".")
+handler = RotatingFileHandler(DATA_DIR / "app.log", maxBytes=1_000_000, backupCount=10)
+handler.setLevel(logging.INFO)
+handler.setFormatter(logging.Formatter('[%(asctime)s] %(levelname)s in %(module)s: %(message)s'))
+
+root_logger = logging.getLogger()
+root_logger.addHandler(handler)
+
+################################################################################
+# Routes
+################################################################################
app = Flask(__name__)
app.secret_key = "dev"
app.config["MAX_CONTENT_LENGTH"] = 50 * 1024 * 1024
+app.logger.addHandler(handler)
@app.route("/")
def index():
error = True
if error:
+ app.logger.info(f"Failed signup attempt: {get_flashed_messages()}")
return redirect(request.referrer)
password = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
get_db().commit()
flash("User created. Please sign in to continue.", "success")
+ app.logger.info(f"Created user {username}")
return redirect("/login")
session["username"] = username
session["userid"] = user_data["userid"]
session.permanent = True
+ app.logger.info(f"{username} logged in")
return redirect("/")
flash("Invalid username/password", "error")
+ app.logger.info(f"Failed login for {username}")
+
return render_template("login.html")
@app.get("/logout")
get_db().commit()
flash("Bio updated successfully")
+ app.logger.info(f"{session['username']} updated bio")
+
return redirect(f"/users/{session['username']}")
@app.get("/edit-song")
songid = int(request.args["songid"])
except ValueError:
# Invalid song id - file not found
+ app.logger.warning(f"Failed song edit - {session['username']} - invalid song ID {songid}")
abort(404)
try:
song = Song.by_id(songid)
if not song.userid == session["userid"]:
# Can't edit someone else's song - 401 unauthorized
+ app.logger.warning(f"Failed song edit - {session['username']} - attempted update for unowned song")
abort(401)
except ValueError:
# Song doesn't exist - 404 file not found
+ app.logger.warning(f"Failed song edit - {session['username']} - song doesn't exist ({songid})")
abort(404)
-
return render_template("edit-song.html", song=song)
@app.post("/upload-song")
if not error:
username = session["username"]
+ app.logger.info(f"{username} uploaded/modified a song - {get_flashed_messages()}")
return redirect(f"/users/{username}")
else:
+ app.logger.info(f"Failed song update - {username} - {get_flashed_messages()}")
return redirect(request.referrer)
def validate_song_form():
# Users can only delete their own songs
if int(userid) != session["userid"]:
+ app.logger.warning(f"Failed song delete - {session['username']} - user doesn't own song")
abort(401)
song_data = query_db("select * from songs where songid = ?", [songid], one=True)
if not song_data:
+ app.logger.warning(f"Failed song delete - {session['username']} - song doesn't exist")
abort(404) # Song doesn't exist
# Delete tags, collaborators
if songpath.exists():
os.remove(songpath)
+ app.logger.info(f"{session['username']} deleted song: {song_data['title']}")
flash(f"Deleted {song_data['title']}")
return redirect(request.referrer)
int(userid)
int(songid)
except ValueError:
+ app.logger.warning(f"Invalid song request: user: {userid}, song: {songid}")
abort(404)
return send_from_directory(DATA_DIR / "songs" / userid, songid + ".mp3")