Skip to content

Commit f6dba20

Browse files
committed
Create blueprints for files and urls.
1 parent fa63380 commit f6dba20

File tree

8 files changed

+209
-170
lines changed

8 files changed

+209
-170
lines changed

app/__init__.py

+7-5
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
from flask import Flask
22
from werkzeug.exceptions import HTTPException
33

4-
from app.config import Config
54
from app.core.utils import (
65
create_stdout_logger,
76
http_error_handler,
8-
setup_db
7+
setup_db,
8+
get_config
99
)
1010
from app.core.files import add_unsupported_mimetypes
1111

1212
db = setup_db()
1313
logger = create_stdout_logger()
14-
config = Config.from_env()
14+
config = get_config()
1515

1616
def create_app() -> Flask:
1717
"""Flask application factory."""
@@ -26,11 +26,13 @@ def handle_exception(e):
2626
return http_error_handler(e)
2727

2828
# Import blueprints
29-
from app.blueprints.api import api
3029
from app.blueprints.main import main
30+
from app.blueprints.files import files
31+
from app.blueprints.urls import urls
3132

3233
# Register blueprints
3334
app.register_blueprint(main)
34-
app.register_blueprint(api, url_prefix='/api')
35+
app.register_blueprint(files, url_prefix='/api')
36+
app.register_blueprint(urls, url_prefix='/api')
3537

3638
return app

app/blueprints/api.py

-160
This file was deleted.

app/blueprints/files.py

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
from http import HTTPStatus
2+
3+
from werkzeug.security import safe_join
4+
from flask import Blueprint, jsonify, url_for, abort, request
5+
6+
from app import config, logger
7+
from app.core.utils import (
8+
auth_required,
9+
create_hmac_hash,
10+
safe_str_comparison
11+
)
12+
from app.core.discord import (
13+
create_discord_webhooks,
14+
create_uploaded_file_embed,
15+
execute_webhooks_with_embed
16+
)
17+
from app.core.files import (
18+
get_file_extension_from_file,
19+
is_file_extension_allowed,
20+
create_directory,
21+
generate_filename,
22+
get_secure_filename,
23+
delete_file,
24+
)
25+
26+
files = Blueprint('files', __name__)
27+
28+
@files.get('/sharex/upload')
29+
def upload_config():
30+
return jsonify({
31+
"Name": "{} (File uploader)".format(request.host),
32+
"Version": "1.0.0",
33+
"DestinationType": "ImageUploader, FileUploader",
34+
"RequestMethod": "POST",
35+
"RequestURL": url_for('files.upload', _external=True),
36+
"Body": "MultipartFormData",
37+
"FileFormName": "file",
38+
"URL": "$json:url$",
39+
"DeletionURL": "$json:delete_url$",
40+
"Headers": {
41+
"Authorization": "YOUR-UPLOAD-PASSWORD-HERE",
42+
},
43+
"ErrorMessage": "$json:status$"
44+
})
45+
46+
@files.post('/upload')
47+
@auth_required
48+
def upload():
49+
f = request.files.get('file')
50+
51+
if f is None:
52+
abort(HTTPStatus.BAD_REQUEST, 'Invalid file.')
53+
54+
file_extension = get_file_extension_from_file(f.stream, config.magic_buffer_bytes)
55+
56+
if is_file_extension_allowed(file_extension, config.allowed_extensions) is False:
57+
abort(HTTPStatus.UNPROCESSABLE_ENTITY, 'Invalid file type.')
58+
59+
create_directory(config.upload_directory)
60+
61+
filename = generate_filename()
62+
63+
if config.use_original_filename:
64+
secure_filename = get_secure_filename(f.filename)
65+
filename = f'{filename}-{secure_filename}'
66+
67+
save_filename = filename + file_extension
68+
save_path = safe_join(config.upload_directory, save_filename)
69+
70+
f.save(save_path)
71+
72+
hmac_hash = create_hmac_hash(save_filename, config.flask_secret)
73+
file_url = url_for('main.uploads', filename=save_filename, _external=True)
74+
deletion_url = url_for('files.delete_file_with_hash', hmac_hash=hmac_hash, filename=save_filename, _external=True)
75+
76+
logger.info(f'Saved file: {save_filename}, URL: {file_url}, deletion URL: {deletion_url}')
77+
78+
# Send data to Discord webhooks
79+
discord_webhooks = create_discord_webhooks(config.discord_webhook_urls, config.discord_webhook_timeout)
80+
if discord_webhooks:
81+
embed = create_uploaded_file_embed(file_url, deletion_url)
82+
execute_webhooks_with_embed(discord_webhooks, embed)
83+
84+
# Return JSON
85+
return jsonify(url=file_url, delete_url=deletion_url)
86+
87+
@files.get('/delete-file/<hmac_hash>/<filename>')
88+
def delete_file_with_hash(hmac_hash, filename):
89+
new_hmac_hash = create_hmac_hash(filename, config.flask_secret)
90+
91+
# If digest is invalid
92+
if safe_str_comparison(hmac_hash, new_hmac_hash) is False:
93+
abort(HTTPStatus.NOT_FOUND)
94+
95+
file_path = safe_join(config.upload_directory, filename)
96+
file_deleted = delete_file(file_path)
97+
98+
if file_deleted is False:
99+
abort(HTTPStatus.GONE)
100+
101+
logger.info(f'Deleted a file {filename}')
102+
103+
return jsonify(message='This file has been deleted, you can now close this page.')

app/blueprints/urls.py

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
from http import HTTPStatus
2+
from secrets import token_urlsafe
3+
4+
from flask import Blueprint, jsonify, url_for, abort, request
5+
6+
from app import config, logger
7+
from app.core.utils import (
8+
auth_required, safe_str_comparison,
9+
create_hmac_hash
10+
)
11+
from app.core.discord import (
12+
create_short_url_embed,
13+
create_discord_webhooks,
14+
execute_webhooks_with_embed
15+
)
16+
from app.core.urls import (
17+
is_valid_url,
18+
add_https_scheme_to_url,
19+
save_url_and_token_to_database,
20+
delete_url_from_database_by_token
21+
)
22+
23+
urls = Blueprint('urls', __name__)
24+
25+
@urls.get('/sharex/shorten')
26+
def shorten_config():
27+
return jsonify({
28+
"Name": "{} (URL shortener)".format(request.host),
29+
"Version": "1.0.0",
30+
"DestinationType": "URLShortener",
31+
"RequestMethod": "POST",
32+
"Body": "MultipartFormData",
33+
"RequestURL": url_for('urls.shorten', _external=True),
34+
"Headers": {
35+
"Authorization": "YOUR-UPLOAD-PASSWORD-HERE"
36+
},
37+
"Arguments": {
38+
"url": "$input$"
39+
},
40+
"URL": "$json:url$",
41+
"ErrorMessage": "$json:status$"
42+
})
43+
44+
@urls.post('/shorten')
45+
@auth_required
46+
def shorten():
47+
url = request.form.get('url')
48+
49+
if is_valid_url(url) is False:
50+
abort(HTTPStatus.BAD_REQUEST, 'Invalid URL.')
51+
52+
url = add_https_scheme_to_url(url)
53+
token = token_urlsafe(config.url_token_bytes)
54+
55+
saved_to_database = save_url_and_token_to_database(url, token)
56+
57+
if saved_to_database is False:
58+
abort(HTTPStatus.INTERNAL_SERVER_ERROR, 'Unable to save URL to database.')
59+
60+
hmac_hash = create_hmac_hash(token, config.flask_secret)
61+
shortened_url = url_for('main.short_url', token=token, _external=True)
62+
deletion_url = url_for('urls.delete_short_url_with_hash', hmac_hash=hmac_hash, token=token, _external=True)
63+
64+
logger.info(f'Saved short URL: {shortened_url} for {url}, deletion URL: {deletion_url}')
65+
66+
# Send data to Discord webhooks
67+
discord_webhooks = create_discord_webhooks(config.discord_webhook_urls, config.discord_webhook_timeout)
68+
if discord_webhooks:
69+
embed = create_short_url_embed(url, shortened_url, deletion_url)
70+
execute_webhooks_with_embed(discord_webhooks, embed)
71+
72+
return jsonify(url=shortened_url)
73+
74+
@urls.get('/delete-short-url/<hmac_hash>/<token>')
75+
def delete_short_url_with_hash(hmac_hash, token):
76+
new_hmac_hash = create_hmac_hash(token, config.flask_secret)
77+
78+
# If digest is invalid
79+
if safe_str_comparison(hmac_hash, new_hmac_hash) is False:
80+
abort(HTTPStatus.NOT_FOUND)
81+
82+
if delete_url_from_database_by_token(token) is False:
83+
abort(HTTPStatus.GONE)
84+
85+
return jsonify(message='This short URL has been deleted, you can now close this page.')

app/config.py app/core/config.py

File renamed without changes.

app/core/files.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77

88
def is_file_extension_allowed(file_extension: str , allowed_file_extensions: list) -> bool:
99
"""Returns True if given file_extension is allowed."""
10-
return file_extension in allowed_file_extensions
10+
extension_without_dot = file_extension.replace('.', '')
11+
return extension_without_dot in allowed_file_extensions
1112

1213
def delete_file(file_path: str) -> bool:
1314
"""Deletes a given file if it exists."""

0 commit comments

Comments
 (0)