diff --git a/config.example.py b/config.example.py index 00f88e5..53b2a40 100644 --- a/config.example.py +++ b/config.example.py @@ -46,6 +46,9 @@ ENFORCE_MAIN_ANNOUNCE_URL = False MAIN_ANNOUNCE_URL = 'http://127.0.0.1:6881/announce' TRACKER_API_URL = 'http://127.0.0.1:6881/api' TRACKER_API_AUTH = 'topsecret' +# Torrents uploaded without an account must be at least this big in total (bytes) +# Set to 0 to disable +MINIMUM_ANONYMOUS_TORRENT_SIZE = 1 * 1024 * 1024 BACKUP_TORRENT_FOLDER = 'torrents' diff --git a/nyaa/api_handler.py b/nyaa/api_handler.py index ccc2d12..10a8608 100644 --- a/nyaa/api_handler.py +++ b/nyaa/api_handler.py @@ -99,22 +99,25 @@ def v2_api_upload(): upload_form.category.choices = _create_upload_category_choices() if upload_form.validate(): - torrent = backend.handle_torrent_upload(upload_form, flask.g.user) + try: + torrent = backend.handle_torrent_upload(upload_form, flask.g.user) - # Create a response dict with relevant data - torrent_metadata = { - 'url': flask.url_for('torrents.view', torrent_id=torrent.id, _external=True), - 'id': torrent.id, - 'name': torrent.display_name, - 'hash': torrent.info_hash.hex(), - 'magnet': torrent.magnet_uri - } + # Create a response dict with relevant data + torrent_metadata = { + 'url': flask.url_for('torrents.view', torrent_id=torrent.id, _external=True), + 'id': torrent.id, + 'name': torrent.display_name, + 'hash': torrent.info_hash.hex(), + 'magnet': torrent.magnet_uri + } - return flask.jsonify(torrent_metadata) - else: - # Map errors back from form fields into the api keys - mapped_errors = {UPLOAD_API_FORM_KEYMAP.get(k, k): v for k, v in upload_form.errors.items()} - return flask.jsonify({'errors': mapped_errors}), 400 + return flask.jsonify(torrent_metadata) + except backend.TorrentExtraValidationException: + pass + + # Map errors back from form fields into the api keys + mapped_errors = {UPLOAD_API_FORM_KEYMAP.get(k, k): v for k, v in upload_form.errors.items()} + return flask.jsonify({'errors': mapped_errors}), 400 # #################################### TEMPORARY #################################### diff --git a/nyaa/backend.py b/nyaa/backend.py index 5b56a52..8c03a18 100644 --- a/nyaa/backend.py +++ b/nyaa/backend.py @@ -15,6 +15,11 @@ from nyaa.extensions import db app = flask.current_app +class TorrentExtraValidationException(Exception): + def __init__(self, errors): + self.errors = errors + + @utils.cached_function def get_category_id_map(): ''' Reads database for categories and turns them into a dict with @@ -44,7 +49,37 @@ def _replace_utf8_values(dict_or_list): return did_change +def validate_torrent_post_upload(torrent, upload_form=None): + ''' Validates a Torrent instance before it's saved to the database. + Enforcing user-and-such-based validations is more flexible here vs WTForm context ''' + errors = { + 'torrent_file': [] + } + + # Encorce minimum size for userless uploads + minimum_anonymous_torrent_size = app.config['MINIMUM_ANONYMOUS_TORRENT_SIZE'] + if torrent.user is None and torrent.filesize < minimum_anonymous_torrent_size: + errors['torrent_file'].append('Torrent too small for an anonymous uploader') + + print(errors) + # Remove keys with empty lists + errors = {k: v for k, v in errors.items() if v} + if errors: + if upload_form: + # Add error messages to the form fields + for field_name, field_errors in errors.items(): + getattr(upload_form, field_name).errors.extend(field_errors) + # Clear out the wtforms dict to force a regeneration + upload_form._errors = None + + raise TorrentExtraValidationException(errors) + + def handle_torrent_upload(upload_form, uploading_user=None, fromAPI=False): + ''' Stores a torrent to the database. + May throw TorrentExtraValidationException if the form/torrent fails + post-WTForm validation! Exception messages will also be added to their + relevant fields on the given form. ''' torrent_data = upload_form.torrent_file.parsed_data # Delete exisiting torrent which is marked as deleted @@ -197,6 +232,9 @@ def handle_torrent_upload(upload_form, uploading_user=None, fromAPI=False): tracker_id=tracker.id, order=order) db.session.add(torrent_tracker) + # Before final commit, validate the torrent again + validate_torrent_post_upload(torrent, upload_form) + db.session.commit() # Store the actual torrent file as well diff --git a/nyaa/views/torrents.py b/nyaa/views/torrents.py index 7dc2c26..8a267af 100644 --- a/nyaa/views/torrents.py +++ b/nyaa/views/torrents.py @@ -307,13 +307,16 @@ def upload(): upload_form.category.choices = _create_upload_category_choices() if flask.request.method == 'POST' and upload_form.validate(): - torrent = backend.handle_torrent_upload(upload_form, flask.g.user) + try: + torrent = backend.handle_torrent_upload(upload_form, flask.g.user) - return flask.redirect(flask.url_for('torrents.view', torrent_id=torrent.id)) - else: - # If we get here with a POST, it means the form data was invalid: return a non-okay status - status_code = 400 if flask.request.method == 'POST' else 200 - return flask.render_template('upload.html', upload_form=upload_form), status_code + return flask.redirect(flask.url_for('torrents.view', torrent_id=torrent.id)) + except backend.TorrentExtraValidationException: + pass + + # If we get here with a POST, it means the form data was invalid: return a non-okay status + status_code = 400 if flask.request.method == 'POST' else 200 + return flask.render_template('upload.html', upload_form=upload_form), status_code @cached_function