Selaa lähdekoodia

add url shortening endpoint

Daniel Sheffield 1 vuosi sitten
vanhempi
säilyke
d9ed5493a3
1 muutettua tiedostoa jossa 111 lisäystä ja 62 poistoa
  1. 111 62
      app/rest/pyapi.py

+ 111 - 62
app/rest/pyapi.py

@@ -11,7 +11,7 @@ from bottle import (
     route, request, response,
     redirect, abort,
     template, static_file,
-    FormsDict, HTTPError,
+    FormsDict, HTTPError, LocalRequest,
 )
 from psycopg import Cursor, connect
 from psycopg.rows import TupleRow
@@ -96,6 +96,57 @@ DOMAIN = "shandan.one"
 PORT = ""
 LOCATION = SCHEME + (f"{HOST}." if HOST else "") + DOMAIN + (f":{PORT}" if PORT else "")
 
+def validate(filename: str) -> bytes:
+    ret = static_file('/'.join([filename,]*2) + '.file', root='app/rest/static')
+    if isinstance(ret, HTTPError):
+        return abort(404, f"No such paste: {filename}")
+
+    if ret.content_length > CLIP_SIZE_LIMIT:
+        return abort(418, f"Paste size exceeds {CLIP_SIZE_LIMIT}")
+
+    content: bytes = ret.body.read() if isinstance(ret.body, BufferedReader) else ret.body.encode('utf-8')
+
+    _bytes = blake(content, person='clip'.encode('utf-8'))
+    _b32 = bytes_to_base32(_bytes)
+    if _b32 != filename:
+        return abort(410, f"Paste content differs")
+    return content
+
+
+def validate_parameter(request: LocalRequest, name: str) -> bytes:
+    if name not in request.params:
+        return abort(400, f"Missing parameter: '{name}'")
+    
+    # TODO: what is correct overhead for form content?
+    OVERHEAD = 1024
+    content: bytes = request.query.get(name, None)
+    content_length = request.content_length
+    if content_length == -1:
+        return abort(418, f"Content-Length must be specified")
+    if content_length > CLIP_SIZE_LIMIT + OVERHEAD:
+        return abort(418, f"Content-Length can not exceed {CLIP_SIZE_LIMIT+OVERHEAD}")
+
+    # TODO: add test for both query/form param
+    content: bytes = (content or request.params[name]).encode('latin-1')
+    if len(content) > CLIP_SIZE_LIMIT:
+        return abort(418, f"Paste can not exceed {CLIP_SIZE_LIMIT}")
+    return content
+
+
+def save(content: bytes, root='app/rest/static') -> str:
+    _bytes = blake(content, person='clip'.encode('utf-8'))
+    _b32 = bytes_to_base32(_bytes)
+    directory = f'{root}/{_b32}'
+    try:
+        os.mkdir(directory, mode=0o700, dir_fd=None)
+    except FileExistsError:
+        pass
+    fd = os.open(f'{directory}/{_b32}.file', os.O_WRONLY | os.O_TRUNC | os.O_CREAT, 0o600)
+    with open(fd, "wb") as f:
+        f.write(content)
+    return _b32
+
+
 @route('/clip', method=['GET', 'POST'])
 @route('/clip/', method=['GET', 'POST'])
 def clip():
@@ -124,69 +175,28 @@ def clip():
         )
 
     if request.method == 'POST':
-        if 'paste' not in request.params:
-            return abort(400, "Missing parameter: 'paste'")
-
-        # TODO: what is correct overhead for form content?
-        OVERHEAD = 1024
-        content = request.query.get("paste", None)
-        content_length = request.content_length
-        if content_length == -1:
-            return abort(418, f"Content-Length must be specified")
-        if content_length > CLIP_SIZE_LIMIT + OVERHEAD:
-            return abort(418, f"Content-Length can not exceed {CLIP_SIZE_LIMIT+OVERHEAD}")
-
-        content: bytes = content or request.params["paste"].encode('latin-1')
-        if len(content) > CLIP_SIZE_LIMIT:
-            return abort(418, f"Paste can not exceed {CLIP_SIZE_LIMIT}")
-
-        if request.params.copy == 'true':
-            response.content_type = 'text/html; charset=utf-8'
-            form = template(
-                'clip-form',
-                action='/clip',
-                method='post',
-                content=content,
-                disabled=False
-            )
-            return template(
-                'paste',
-                form=form,
-                link=f'{LOCATION}/clip',
-                disabled=False,
-                download=None
-            )
-
-        _bytes = blake(content, person='clip'.encode('utf-8'))
-        _b32 = bytes_to_base32(_bytes)
-        directory = f'app/rest/static/{_b32}'
-        try:
-            os.mkdir(directory, mode=0o700, dir_fd=None)
-        except FileExistsError:
-            pass
-        fd = os.open(f'{directory}/{_b32}.file', os.O_WRONLY | os.O_TRUNC | os.O_CREAT, 0o600)
-        with open(fd, "wb") as f:
-            f.write(content)
-
-        form = template('clip-form', action='/clip', method='post', content=content)
-        response.content_type = 'text/html; charset=utf-8'
-        return redirect(f'/clip?hash={_b32}')
+        content = validate_parameter(request, 'paste')
 
-def validate(filename: str) -> bytes:
-    ret = static_file('/'.join([filename,]*2) + '.file', root='app/rest/static')
-    if isinstance(ret, HTTPError):
-        return abort(404, f"No such paste: {filename}")
-
-    if ret.content_length > CLIP_SIZE_LIMIT:
-        return abort(418, f"Paste size exceeds {CLIP_SIZE_LIMIT}")
-
-    content: bytes = ret.body.read() if isinstance(ret.body, BufferedReader) else ret.body.encode('utf-8')
+        if request.params.copy != 'true':
+            _b32 = save(content)
+            return redirect(f'/clip?hash={_b32}')
+        
+        response.content_type = 'text/html; charset=utf-8'
+        form = template(
+            'clip-form',
+            action='/clip',
+            method='post',
+            content=content,
+            disabled=False
+        )
+        return template(
+            'paste',
+            form=form,
+            link=f'{LOCATION}/clip',
+            disabled=False,
+            download=None
+        )
 
-    _bytes = blake(content, person='clip'.encode('utf-8'))
-    _b32 = bytes_to_base32(_bytes)
-    if _b32 != filename:
-        return abort(410, f"Paste content differs")
-    return content
 
 @route('/clip/<filename:path>', method='GET')
 def get_clip(filename):
@@ -200,3 +210,42 @@ def get_clip(filename):
         return ret
 
     return static_file('/'.join([filename,]*2) + '.file', root='app/rest/static')
+
+
+@route('/goto', method=['GET', 'POST'])
+@route('/goto/', method=['GET', 'POST'])
+def goto():
+    if request.method == 'GET':
+        _hash = request.params.hash
+        if _hash:
+            _hash = normalize_base32(_hash)
+            content = validate(_hash).decode('utf-8')
+        else:
+            content = None
+        
+        if content and request.params.go == 'true':
+            # TODO: urlencode this !?
+            return redirect(content)
+        
+        link = f'{LOCATION}/goto/{_hash}' if content else f'{LOCATION}/goto'
+        disabled = True if content else False
+        response.content_type = 'text/html; charset=utf-8'
+        form = template(
+            'goto-form',
+            action='/goto',
+            method='post',
+            content=content,
+            disabled=disabled
+        )
+        return template('goto', form=form, link=link, disabled=disabled)
+
+    if request.method == 'POST':
+        content = validate_parameter(request, 'url')
+        _b32 = save(content)
+        return redirect(f'/goto?hash={_b32}')
+
+
+@route('/goto/<filename:path>', method='GET')
+def redirect_goto(filename):
+    filename = normalize_base32(filename)
+    return redirect(f'/goto?hash={filename}&go=true')