diff --git a/CodeChallenge/__init__.py b/CodeChallenge/__init__.py index fb6424f..f7681f7 100644 --- a/CodeChallenge/__init__.py +++ b/CodeChallenge/__init__.py @@ -99,7 +99,7 @@ def send_landing(path): def catch_all(path): # show landing page - if core.current_rank() == -1 and not path or path == "home": + if core.current_rank() == -1 and (not path or path == "home"): return redirect("/landing") return send_from_directory(app.config["DIST_DIR"], "index.html") diff --git a/CodeChallenge/api/questions.py b/CodeChallenge/api/questions.py index 44ec7ee..032dc82 100644 --- a/CodeChallenge/api/questions.py +++ b/CodeChallenge/api/questions.py @@ -1,3 +1,5 @@ +import os +from hashlib import blake2s from hmac import compare_digest as str_cmp from flask import Blueprint, current_app, jsonify, request @@ -52,10 +54,21 @@ def next_question(): return jsonify(status="error", reason=f"no questions for rank {rank!r}"), 404 + # make filename less predictable + data = bytes(current_app.config["SECRET_KEY"] + str(q.rank), "ascii") + filename = blake2s(data).hexdigest() + + asset = f"assets/{filename}{q.asset_ext}" + asset_path = os.path.join(current_app.config["APP_DIR"], asset) + + if not os.path.isfile(asset_path): + with open(asset_path, "wb") as fhandle: + fhandle.write(q.asset) + return jsonify(status="success", question=q.title, rank=rank, - asset=f"assets/{q.asset}"), 200 + asset=asset), 200 def answer_limit_attempts(): diff --git a/CodeChallenge/cli/questions.py b/CodeChallenge/cli/questions.py index 70ae47d..4375f74 100644 --- a/CodeChallenge/cli/questions.py +++ b/CodeChallenge/cli/questions.py @@ -1,3 +1,5 @@ +import os.path + import click from flask import Blueprint from tabulate import tabulate @@ -22,6 +24,7 @@ def q_add(title, answer, rank, asset): ASSET is a path to a file to upload for a question """ + asset = os.path.abspath(asset) qid = add_question(title, answer, rank, asset) click.echo(f"added question id {qid}") @@ -33,11 +36,11 @@ def q_ls(tablefmt): """List all questions in the database""" table = [] - for q in Question.query.all(): - table.append((q.id, q.title, q.answer, q.rank, q.asset)) + for q in Question.query.all(): # type: Question + table.append((q.id, q.title, q.answer, q.rank, f"{len(q.asset)} length blob", q.asset_ext)) click.echo(tabulate(table, - ("id", "title", "answer", "rank", "asset"), + ("id", "title", "answer", "rank", "asset", "asset_ext"), tablefmt=tablefmt)) if not table: @@ -71,7 +74,7 @@ def q_replace(title, answer, rank, asset): success = del_question(oldq.id) if not success: - print("error occured while trying to delete original question") + print("error occurred while trying to delete original question") print(f"old question id was: {oldq.id}") return else: diff --git a/CodeChallenge/config.py b/CodeChallenge/config.py index 68f990e..1164b1a 100644 --- a/CodeChallenge/config.py +++ b/CodeChallenge/config.py @@ -53,6 +53,7 @@ class ProductionConfig(DefaultConfig): class DevelopmentConfig(ProductionConfig): SQLALCHEMY_DATABASE_URI = "mysql://cc-user:password@localhost" \ "/code_challenge_local" + # SQLALCHEMY_DATABASE_URI = "mysql+mysqldb://codechallenge:cHALcw9Z0HqB2gD9B1Kkmy83GvTI19x0NzRNO3zqZhqbIKqY9P@learndb002.cm1f2l4z67tv.us-west-2.rds.amazonaws.com/code_challenge" JWT_COOKIE_SECURE = False CODE_CHALLENGE_START = os.getenv("CODE_CHALLENGE_START", "1578596347") JWT_SECRET_KEY = "SuperSecret" diff --git a/CodeChallenge/manage.py b/CodeChallenge/manage.py index 524e28c..c6523d4 100644 --- a/CodeChallenge/manage.py +++ b/CodeChallenge/manage.py @@ -1,8 +1,4 @@ import os -import secrets -import shutil - -from flask import current_app from .models import Question, db @@ -14,19 +10,14 @@ def add_question(title, answer, rank, asset) -> Question: if q is not None: raise ValueError(f"a question with rank {rank} already exists") - ext = os.path.splitext(asset)[1] - asset_dir = os.path.join(current_app.config["APP_DIR"], "assets") - - filename = secrets.token_urlsafe() + ext - - save_path = os.path.join(asset_dir, filename) - - shutil.copyfile(asset, save_path) - q = Question() q.title = title q.answer = answer - q.asset = filename + + with open(asset, "rb") as fhandle: + q.asset = fhandle.read() + + q.asset_ext = os.path.splitext(asset)[1] q.rank = rank db.session.add(q) @@ -42,17 +33,6 @@ def del_question(question_id): if q is None: return False - if q.asset is not None: - - asset_dir = os.path.join(current_app.config["APP_DIR"], "assets") - filename = os.path.join(asset_dir, q.asset) - try: - os.remove(filename) - except FileNotFoundError: - print("warning: could not delete asset; " - f"file not found: {filename}") - pass - Question.query.filter_by(id=q.id).delete() return True diff --git a/CodeChallenge/models.py b/CodeChallenge/models.py index c425542..415f591 100644 --- a/CodeChallenge/models.py +++ b/CodeChallenge/models.py @@ -16,7 +16,8 @@ class Question(db.Model): title = db.Column(db.String(5000), nullable=False) answer = db.Column(db.String(255), nullable=False) rank = db.Column(db.Integer, nullable=False) - asset = db.Column(db.String(255)) + asset = db.Column(db.BLOB) + asset_ext = db.Column(db.String(10)) def __repr__(self): return '' % self.id diff --git a/alembic.ini b/alembic.ini deleted file mode 100644 index bfcc3c7..0000000 --- a/alembic.ini +++ /dev/null @@ -1,85 +0,0 @@ -# A generic, single database configuration. - -[alembic] -# path to migration scripts -script_location = alembic - -# template used to generate migration files -# file_template = %%(rev)s_%%(slug)s - -# timezone to use when rendering the date -# within the migration file as well as the filename. -# string value is passed to dateutil.tz.gettz() -# leave blank for localtime -# timezone = - -# max length of characters to apply to the -# "slug" field -# truncate_slug_length = 40 - -# set to 'true' to run the environment during -# the 'revision' command, regardless of autogenerate -# revision_environment = false - -# set to 'true' to allow .pyc and .pyo files without -# a source .py file to be detected as revisions in the -# versions/ directory -# sourceless = false - -# version location specification; this defaults -# to alembic/versions. When using multiple version -# directories, initial revisions must be specified with --version-path -# version_locations = %(here)s/bar %(here)s/bat alembic/versions - -# the output encoding used when revision files -# are written from script.py.mako -# output_encoding = utf-8 - -sqlalchemy.url = driver://user:pass@localhost/dbname - - -[post_write_hooks] -# post_write_hooks defines scripts or Python functions that are run -# on newly generated revision scripts. See the documentation for further -# detail and examples - -# format using "black" - use the console_scripts runner, against the "black" entrypoint -# hooks=black -# black.type=console_scripts -# black.entrypoint=black -# black.options=-l 79 - -# Logging configuration -[loggers] -keys = root,sqlalchemy,alembic - -[handlers] -keys = console - -[formatters] -keys = generic - -[logger_root] -level = WARN -handlers = console -qualname = - -[logger_sqlalchemy] -level = WARN -handlers = -qualname = sqlalchemy.engine - -[logger_alembic] -level = INFO -handlers = -qualname = alembic - -[handler_console] -class = StreamHandler -args = (sys.stderr,) -level = NOTSET -formatter = generic - -[formatter_generic] -format = %(levelname)-5.5s [%(name)s] %(message)s -datefmt = %H:%M:%S