Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Ignore the virtual environment directory
.venv/

# Ignore other typical files and directories
*.pyc
*.pyo
__pycache__/
*.log
*.env
*.git
node_modules/
build/
dist/
*.md
38 changes: 38 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Use Alpine 3.21.0 as the base image
FROM alpine:3.21.0

# Install necessary dependencies using apk (Alpine package manager)
RUN apk update && apk upgrade && \
apk add --no-cache \
curl \
python3 \
libmagic \
&& rm -rf /var/cache/apk/*

# Install UV Astra (Python package manager)
RUN curl -LsSf https://astral.sh/uv/install.sh | sh

# Ensure the UV binary is in the PATH by setting it explicitly
ENV PATH="/root/.local/bin:${PATH}"

# Set the working directory for the application
WORKDIR /app

# Copy from the cache instead of linking since it's a mounted volume
ENV UV_LINK_MODE=copy

# Copy uv.lock and pyproject.toml before the application code to leverage Docker layer caching
COPY uv.lock pyproject.toml /app/

# Install the project's dependencies using UV Astra's sync command
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev

# Copy the rest of the application code
COPY . /app

# Expose a port
EXPOSE 8080

# Set the entrypoint to run your Python application with UV run command
ENTRYPOINT ["uv", "run", "gunicorn", "-c", "gunicorn_config.py", "blck:app"]
93 changes: 63 additions & 30 deletions blck.py
Original file line number Diff line number Diff line change
@@ -1,47 +1,88 @@
#!/usr/bin/env python3
# copyleft (c) 2017-2021 parazyd <parazyd@dyne.org>
# see LICENSE file for copyright and license details.

import logging
from io import BytesIO
from os import remove, rename
from os.path import isfile
from random import choice
from string import ascii_uppercase, ascii_lowercase
import sys

from flask import (Flask, Blueprint, render_template, request, safe_join,
send_file, abort)
from flask import Flask, Blueprint, render_template, request, send_file, abort, jsonify, redirect, url_for
from werkzeug.utils import safe_join
import magic

debug_mode = '-d' in sys.argv

log_level = logging.DEBUG if debug_mode else logging.INFO

logging.basicConfig(
level=log_level,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[logging.StreamHandler()]
)

if debug_mode:
logging.getLogger().addHandler(logging.FileHandler('blck.log', mode='a'))

bp = Blueprint('blck', __name__, template_folder='templates')


@bp.route("/health", methods=['GET'])
def health_check():
app.logger.debug('Health check called')
return jsonify({'status': 'Healthy'}), 200


@bp.route("/", methods=['GET', 'POST'])
def index():
app.logger.debug('Handling index route')
if request.method == 'GET':
return render_template('index.html', root=args.r)
return short(request.files)
return render_template('index.html', root='/')

if 'c' not in request.files or not request.files['c'].filename:
app.logger.error('No file selected or file is empty.')
abort(400, "No file selected")

@bp.route("<urlshort>")
upload_url = short(request.files['c'])
app.logger.debug(f'File uploaded successfully, redirecting to {upload_url}')
return redirect(url_for('blck.display_url', file_url=upload_url))


@bp.route("/url", methods=['GET'])
def display_url():
file_url = request.args.get('file_url')
if not file_url:
app.logger.error('File URL is missing in request.')
abort(400, "File URL is missing")
app.logger.debug(f'Displaying URL: {file_url}')
return f"<p>Your file is available at: <a href='{file_url}'>{file_url}</a></p>"


@bp.route("/<urlshort>")
def urlget(urlshort):
app.logger.debug(f"Fetching URL: {urlshort}")
fp = safe_join('files', urlshort)
if not isfile(fp):
app.logger.error(f"File {urlshort} not found.")
abort(404)

r = BytesIO()
mime = magic.from_file(fp, mime=True)
with open(fp, 'rb') as fo:
r.write(fo.read())
r.seek(0)
remove(fp)
app.logger.debug(f"Serving file {urlshort} with MIME type {mime}.")
return send_file(r, mimetype=mime)


def short(c):
if not c or not c['c']:
return abort(400)
app.logger.debug('Shortening file upload')
if not c:
app.logger.error('No file provided.')
abort(400)

s = genid()
f = c['c']
f = c
f.save(safe_join('files', s))

mimetype = f.mimetype
Expand All @@ -53,31 +94,23 @@ def short(c):
rename(safe_join('files', t), safe_join('files', s))

if request.headers.get('X-Forwarded-Proto') == 'https':
app.logger.debug('Request is using HTTPS')
return ''.join([
request.url_root.replace('http://', 'https://'),
args.r.lstrip('/'), s, '\n'
s
])
return ''.join([request.url_root + args.r.lstrip('/'), s, '\n'])
app.logger.debug('Request is using HTTP')
return ''.join([request.url_root, s])


def genid(size=4, chars=ascii_uppercase + ascii_lowercase):
return ''.join(choice(chars) for i in range(size))


app = Flask(__name__)
app.config['PREFERRED_URL_SCHEME'] = 'https'
app.register_blueprint(bp, url_prefix='/')

if __name__ == '__main__':
from argparse import ArgumentParser
parser = ArgumentParser()
parser.add_argument('-r', default='/', help='application root')
parser.add_argument('-l', default='localhost', help='listen host')
parser.add_argument('-p', default=13321, help='listen port')
parser.add_argument('-d', default=False, action='store_true', help='debug')
args = parser.parse_args()

app = Flask(__name__)
app.register_blueprint(bp, url_prefix=args.r)

if args.d:
app.run(host=args.l, port=args.p, threaded=True, debug=args.d)
else:
from bjoern import run
run(app, args.l, int(args.p))
app.logger.info('Application started.')
app.run(host='0.0.0.0', port=8080, debug=debug_mode)
11 changes: 11 additions & 0 deletions gunicorn_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import os
workers = int(os.environ.get('GUNICORN_PROCESSES', '2'))
threads = int(os.environ.get('GUNICORN_THREADS', '4'))
timeout = int(os.environ.get('GUNICORN_TIMEOUT', '120'))
bind = os.environ.get('GUNICORN_BIND', '0.0.0.0:8080')
forwarded_allow_ips = '*'
secure_scheme_headers = { 'X-Forwarded-Proto': 'https' }
project_root = os.path.dirname(os.path.abspath(__file__))
accesslog = os.path.join(project_root, 'access.log')
errorlog = os.path.join(project_root, 'error.log')
loglevel = os.environ.get('GUNICORN_LOG_LEVEL', 'debug')
11 changes: 11 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[project]
name = "blck"
version = "0.1.0"
description = "An ephemeral pastebin/URL-shortener"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"gunicorn==23.0.0",
"flask==3.1.0",
"python-magic==0.4.27",
]
103 changes: 103 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# This file was autogenerated by uv via the following command:
# uv pip compile --generate-hashes --output-file requirements.txt pyproject.toml
blinker==1.9.0 \
--hash=sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf \
--hash=sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc
# via flask
click==8.1.8 \
--hash=sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2 \
--hash=sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a
# via flask
flask==3.1.0 \
--hash=sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac \
--hash=sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136
# via blck (pyproject.toml)
gunicorn==23.0.0 \
--hash=sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d \
--hash=sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec
# via blck (pyproject.toml)
itsdangerous==2.2.0 \
--hash=sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef \
--hash=sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173
# via flask
jinja2==3.1.5 \
--hash=sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb \
--hash=sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb
# via flask
markupsafe==3.0.2 \
--hash=sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4 \
--hash=sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30 \
--hash=sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0 \
--hash=sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9 \
--hash=sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396 \
--hash=sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13 \
--hash=sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028 \
--hash=sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca \
--hash=sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557 \
--hash=sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832 \
--hash=sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0 \
--hash=sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b \
--hash=sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579 \
--hash=sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a \
--hash=sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c \
--hash=sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff \
--hash=sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c \
--hash=sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22 \
--hash=sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094 \
--hash=sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb \
--hash=sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e \
--hash=sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5 \
--hash=sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a \
--hash=sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d \
--hash=sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a \
--hash=sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b \
--hash=sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8 \
--hash=sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225 \
--hash=sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c \
--hash=sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144 \
--hash=sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f \
--hash=sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87 \
--hash=sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d \
--hash=sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93 \
--hash=sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf \
--hash=sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158 \
--hash=sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84 \
--hash=sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb \
--hash=sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48 \
--hash=sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171 \
--hash=sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c \
--hash=sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6 \
--hash=sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd \
--hash=sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d \
--hash=sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1 \
--hash=sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d \
--hash=sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca \
--hash=sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a \
--hash=sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29 \
--hash=sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe \
--hash=sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798 \
--hash=sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c \
--hash=sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8 \
--hash=sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f \
--hash=sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f \
--hash=sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a \
--hash=sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178 \
--hash=sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0 \
--hash=sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79 \
--hash=sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430 \
--hash=sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50
# via
# jinja2
# werkzeug
packaging==24.2 \
--hash=sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759 \
--hash=sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f
# via gunicorn
python-magic==0.4.27 \
--hash=sha256:c1ba14b08e4a5f5c31a302b7721239695b2f0f058d125bd5ce1ee36b9d9d3c3b \
--hash=sha256:c212960ad306f700aa0d01e5d7a325d20548ff97eb9920dcd29513174f0294d3
# via blck (pyproject.toml)
werkzeug==3.1.3 \
--hash=sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e \
--hash=sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746
# via flask
Loading