From 3a9629e49b4c7e1d10c89bbffa04d18e96948116 Mon Sep 17 00:00:00 2001 From: Tomasz Kramkowski Date: Mon, 27 Mar 2023 20:11:37 +0100 Subject: POST request support --- paste/__init__.py | 21 ++++++++++++++++++++- paste/store.py | 26 +++++++++++++++++++++++++- tests/test_application.py | 17 +++++++++++++++++ 3 files changed, 62 insertions(+), 2 deletions(-) diff --git a/paste/__init__.py b/paste/__init__.py index 3ee4468..36ce0cb 100644 --- a/paste/__init__.py +++ b/paste/__init__.py @@ -1,12 +1,13 @@ import binascii import sys import traceback +import urllib.parse from base64 import b64decode, b64encode from collections.abc import Callable, Iterable from functools import wraps from sqlite3 import Connection from typing import Any, Optional, Protocol, runtime_checkable -from wsgiref.util import request_uri +from wsgiref.util import application_uri, request_uri from . import db, store @@ -239,6 +240,24 @@ def application(environ: Env, start_response: StartResponse) -> Response: ], ) return [] + elif environ["REQUEST_METHOD"] == "POST": + content_type = environ.get("CONTENT_TYPE", "text/plain") + content_length = int(environ["CONTENT_LENGTH"]) + content = environ["wsgi.input"].read(content_length) + path, content_hash = store.post(conn, name, content, content_type) + uri = application_uri(environ) + path = urllib.parse.quote(path) + if uri[-1] == "/" and path[:1] == "/": + uri += path[1:] + else: + uri += path + if environ.get("QUERY_STRING"): + uri += "?" + environ["QUERY_STRING"] + start_response( + "201 Created", + [("Location", uri), ("ETag", f'"{b64encode(content_hash).decode()}"')], + ) + return [] elif environ["REQUEST_METHOD"] == "DELETE": if store.delete(conn, name): start_response("204 No Content", []) diff --git a/paste/store.py b/paste/store.py index ed81560..dd00edd 100644 --- a/paste/store.py +++ b/paste/store.py @@ -1,4 +1,5 @@ -from sqlite3 import Connection +from secrets import token_urlsafe +from sqlite3 import Connection, IntegrityError def put(conn: Connection, name: str, content: bytes, content_type: str): @@ -25,6 +26,29 @@ def put(conn: Connection, name: str, content: bytes, content_type: str): return True, content_hash +def post(conn: Connection, prefix: str, content: bytes, content_type: str): + with conn: + conn.execute( + "INSERT OR IGNORE INTO file (content) VALUES (?)", + (content,), + ) + (content_hash,) = conn.execute("SELECT DATA_HASH(?)", (content,)).fetchone() + for _ in range(16): + name = prefix + token_urlsafe(5) + try: + conn.execute( + """INSERT INTO link (name, content_type, file_hash) + VALUES (?, ?, ?)""", + (name, content_type, content_hash), + ) + except IntegrityError: + continue + break + else: + raise RuntimeError("Could not insert a link in 16 attempts") + return name, content_hash + + def get(conn: Connection, name: str): row = conn.execute( """SELECT link.content_type, file.hash, file.content diff --git a/tests/test_application.py b/tests/test_application.py index a7d6861..5a7847e 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -238,3 +238,20 @@ def test_delete(app, token): "/test_key", headers={"Authorization": f"APIKey {token}"}, expect_errors=True ) assert res.status == "404 Not Found" + + +def test_post(app, token): + res = app.post( + "/test_key", + "Hello, World!", + headers={"Content-Type": "text/plain", "Authorization": f"APIKey {token}"}, + ) + assert res.status == "201 Created" + assert res.headers["Location"] != res.request.url + print(res.headers["Location"]) + print(res.request.url) + assert res.headers["Location"].startswith(res.request.url) + etag = res.headers["ETag"] + res = app.get(res.headers["Location"]) + assert res.status == "200 OK" + assert res.headers["ETag"] == etag -- cgit v1.2.3-54-g00ecf