Skip to content
Merged
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
39 changes: 28 additions & 11 deletions pb/paste/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
from uuid import uuid4
from hashlib import sha1

from mysql.connector import errors

def insert(content):
secret = uuid4().bytes
args = (secret, content, None)
Expand All @@ -25,22 +27,32 @@ def insert_private(content):
args = (secret, content, None)
(_, _, digest) = request.cur.callproc('paste_insert_private', args)
return bytes(digest) if digest else None, secret


def insert_vanity(label, content):
secret = uuid4().bytes
args = (label, secret, content)

try:
request.cur.callproc('paste_insert_vanity', args)
return secret
except errors.IntegrityError:
return None

def put(secret, content):
args = (secret, content, None, None)
(_, _, id, digest) = request.cur.callproc('paste_put', args)
return int(id) if id else None, bytes(digest) if digest else None
args = (secret, content, None, None, None)
(_, _, id, digest, label) = request.cur.callproc('paste_put', args)
return int(id) if id else None, bytes(digest) if digest else None, bytes(label) if label else None

def delete(uuid):
args = (uuid, None, None)
(_, id, digest) = request.cur.callproc('paste_delete', args)
return int(id) if id else None, bytes(digest) if digest else None
args = (uuid, None, None, None)
(_, id, digest, label) = request.cur.callproc('paste_delete', args)
return int(id) if id else None, bytes(digest) if digest else None, bytes(label) if label else None

def get_digest(content):
digest = sha1(content).digest()
args = (digest, None, None)
(_, id, exists) = request.cur.callproc('paste_get_digest', args)
return int(id) if id else None, digest if exists else None
args = (digest, None, None, None)
(_, id, label, exists) = request.cur.callproc('paste_get_digest', args)
return int(id) if id else None, digest if exists else None, bytes(label) if label else None

def get_content(id):
args = (id,) + (None,)
Expand All @@ -52,7 +64,12 @@ def get_content_digest(digest):
(_, content) = request.cur.callproc('paste_get_content_digest', args)
return content

def get_content_vanity(label):
args = (label, None)
(_, content) = request.cur.callproc('paste_get_content_vanity', args)
return content

def get_stats():
args = (None, None)
(count, length) = request.cur.callproc('paste_get_stats', args)
return int(count), int(length)
return int(count) if count else 0, int(length) if length else 0
42 changes: 26 additions & 16 deletions pb/paste/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

from pb.db import cursor
from pb.paste import model, handler as _handler
from pb.util import highlight, redirect, request_content, id_url, publish_parts
from pb.util import highlight, redirect, request_content, id_url, publish_parts, any_url

paste = Blueprint('paste', __name__)

Expand All @@ -43,21 +43,25 @@ def form():
return Response(render_template("form.html"), mimetype='text/html')

@paste.route('/', methods=['POST'])
@paste.route('/<label:vanity>', methods=['POST'])
@cursor
def post():
def post(vanity=None):
content, filename = request_content()
if not content:
return "Nope.\n", 400

uuid = None
id, digest = model.get_digest(content)
if not id and not digest:
if request.form.get('p'):
id, digest, label = model.get_digest(content)
if not any((id, digest, label)):
if vanity:
label, name = vanity
uuid = model.insert_vanity(label, content)
elif request.form.get('p'):
digest, uuid = model.insert_private(content)
else:
id, uuid = model.insert(content)

url = id_url(b66=(id, filename)) if id else id_url(sha1=(digest, filename))
url = any_url(id, digest, label, filename=filename)
uuid = str(UUID(bytes=uuid)) if uuid else '<redacted>'
return redirect(url, safe_dump(dict(url=url, uuid=uuid), default_flow_style=False))

Expand All @@ -68,24 +72,24 @@ def put(uuid):
if not content:
return "Nope.\n", 400

id, digest = model.get_digest(content)
if id or digest:
url = id_url(b66=id) if id else id_url(sha1=digest)
args = model.get_digest(content)
if any(args):
url = any_url(*args)
return redirect(url, "Paste already exists.\n", 409)

id, digest = model.put(uuid.bytes, content)
if id or digest:
url = id_url(b66=(id, filename)) if id else id_url(sha1=digest)
args = model.put(uuid.bytes, content)
if any(args):
url = any_url(*args)
return redirect(url, "{} updated.\n".format(url), 200)

return "Not found.\n", 404

@paste.route('/<uuid:uuid>', methods=['DELETE'])
@cursor
def delete(uuid):
id, digest = model.delete(uuid.bytes)
if id or digest:
url = id_url(b66=id) if id else id_url(sha1=digest)
args = model.delete(uuid.bytes)
if any(args):
url = any_url(*args)
return redirect(url, "{} deleted.\n".format(url), 200)
return "Not found.\n", 404

Expand All @@ -95,15 +99,21 @@ def delete(uuid):
@paste.route('/<sha1:sha1>')
@paste.route('/<sha1:sha1>/<string(minlength=0):lexer>')
@paste.route('/<string(length=1):handler>/<sha1:sha1>')
@paste.route('/<label:label>')
@paste.route('/<label:label>/<string(minlength=0):lexer>')
@paste.route('/<string(length=1):handler>/<label:label>')
@cursor
def get(b66=None, sha1=None, lexer=None, handler=None):
def get(b66=None, sha1=None, label=None, lexer=None, handler=None):
content = None
if b66:
id, name = b66
content = model.get_content(id)
if sha1:
digest, name = sha1
content = model.get_content_digest(digest)
if label:
label, name = label
content = model.get_content_vanity(label)

if not content:
return "Not found.\n", 404
Expand Down
18 changes: 18 additions & 0 deletions pb/pb.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,23 @@ def to_url(self, value):
ext = path.splitext(filename)[1] if filename else ''
return '{}{}'.format(hexlify(digest).decode('utf-8'), ext)

class LabelConverter(BaseConverter):
def __init__(self, map):
super().__init__(map)
self.regex = '(([^/.]{6,39})([.][^/]*)?)'
self.sre = re.compile(self.regex)

def to_python(self, value):
(name, label, _) = self.sre.match(value).groups()
return label.encode('utf-8'), name

def to_url(self, value):
if isinstance(value, bytes):
return value.decode('utf-8')
label, filename = value
ext = path.splitext(filename)[1] if filename else ''
return '{}{}'.format(label.decode('utf-8'), ext)

def load_yaml(app, filename):
for filename in BaseDirectory.load_config_paths('pb', filename):
with open(filename) as f:
Expand All @@ -71,6 +88,7 @@ def create_app(config_filename='config.yaml'):
app.response_class = TextResponse
app.url_map.converters['id'] = IDConverter
app.url_map.converters['sha1'] = SHA1Converter
app.url_map.converters['label'] = LabelConverter

load_yaml(app, config_filename)
init_db(app)
Expand Down
36 changes: 32 additions & 4 deletions pb/templates/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,25 @@ id
One of:

- a four character base66 paste id
- a four character base66 paste id, followed by a period-delimiter and a
mimetype extension
- a four character base66 paste id, followed by a period-delimiter and
a mimetype extension
- a 40 character sha1 hexdigest
- a 40 character sha1 hexdigest, followed by a period-delimiter and a
mimetype extension
- a 'vanity' label
- a 'vanity' label, followed by a period-delimiter
and a mimetype extension
- a three character base66 url redirect id

A mimetype extension, when specified, is first matched with a matching mimetype
known to the system, then returned in the HTTP response headers.

vanity
^^^^^^

Any unicode string excluding the characters '/' and '.' of 5 < length
< 40.

lexer
^^^^^

Expand Down Expand Up @@ -110,6 +119,12 @@ Unless the 'filename' disposition extension parameter is specified, the form
data is decoded. The value of the 'filename' parameter is split by
period-delimited extension, and appended to the location in the response.

``POST /<vanity>``
^^^^^^^^^^^^^^^^^^

Same as above, except the paste is a 'vanity' paste, where the GET URL
path is identical to the POST path.

``PUT /<uuid>``
^^^^^^^^^^^^^^^

Expand Down Expand Up @@ -153,8 +168,8 @@ examples

No really, how in the name of Gandalf's beard does this actually work? Show me!

normal paste
^^^^^^^^^^^^
creating pastes
^^^^^^^^^^^^^^^

Create a paste from the output of 'dmesg':

Expand Down Expand Up @@ -235,6 +250,19 @@ base66 id:
url: http://localhost:10002/1c5dd062b6a3359cf60989d0e1c8746944608304
uuid: e5860f7a-b074-4e5d-88d4-747cfacc1fcd

vanity pastes
^^^^^^^^^^^^^

Witness the gloriousness:

.. code:: console

$ curl -F c=@- https://ptpb.pw/polyzen <<< boats and hoes
url: https://ptpb.pw/polyzen
uuid: <redacted>
$ curl https://ptpb.pw/polyzen
boats and hoes

shell functions
---------------

Expand Down
8 changes: 8 additions & 0 deletions pb/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ def id_url(**kwargs):
scheme = proto if proto else request.scheme
return url_for('.get', _external=True, _scheme=scheme, **kwargs)

def any_url(id, digest, label, **kwargs):
if id:
return id_url(b66=id, **kwargs)
if digest:
return id_url(sha1=digest, **kwargs)
if label:
return id_url(label=label, **kwargs)

def publish_parts(source):
overrides = {'syntax_highlight': 'short'}
parts = core.publish_parts(source, writer_name='html', settings_overrides=overrides)
Expand Down
Loading