|
@@ -3,14 +3,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
-from io import BytesIO
|
|
|
+
|
|
|
import time
|
|
|
from bottle import (
|
|
|
- Bottle,
|
|
|
- default_app,
|
|
|
route, request, response,
|
|
|
- redirect, abort,
|
|
|
- template, static_file,
|
|
|
+ redirect, abort, static_file,
|
|
|
HTTPResponse,
|
|
|
)
|
|
|
from itertools import chain
|
|
@@ -19,9 +16,10 @@ from linkpreview import link_preview
|
|
|
|
|
|
from .hash_util import B32REGEX, normalize_base32, blake, bytes_to_base32
|
|
|
from .qr import get_qr_code
|
|
|
+from .bar import get_bar_code
|
|
|
from json import dumps, load
|
|
|
|
|
|
-from sqlite3 import connect, Row
|
|
|
+from sqlite3 import connect
|
|
|
|
|
|
SCHEME = "https://"
|
|
|
HOST = ""
|
|
@@ -29,9 +27,29 @@ DOMAIN = "shandan.one"
|
|
|
PORT = ""
|
|
|
LOCATION = SCHEME + (f"{HOST}." if HOST else "") + DOMAIN + (f":{PORT}" if PORT else "")
|
|
|
|
|
|
-@route('/preview', method=['GET'])
|
|
|
-def get_preview():
|
|
|
+def parse_data_uri(content):
|
|
|
+
|
|
|
+ _, *media, data = chain.from_iterable(map(
|
|
|
+ lambda x: x.split(',', 1), content.split(':', 1)
|
|
|
+ ))
|
|
|
+ media = media and media[0]
|
|
|
+ mimetype, *params, encoding = media.split(';')
|
|
|
+ if '=' in encoding:
|
|
|
+ params.append(encoding)
|
|
|
+ encoding = None
|
|
|
+
|
|
|
+ return {
|
|
|
+ 'mimetype': mimetype,
|
|
|
+ 'params': dict(map(lambda x: x.split('='), params)),
|
|
|
+ 'encoding': None,
|
|
|
+ 'data': data,
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+@route('/goto/preview', method=['GET'])
|
|
|
+def get_goto_preview():
|
|
|
link = request.params.link
|
|
|
+ response.content_type = 'application/json'
|
|
|
if not link:
|
|
|
return dumps(None)
|
|
|
try:
|
|
@@ -45,16 +63,32 @@ def get_preview():
|
|
|
except:
|
|
|
return dumps(None)
|
|
|
|
|
|
-@route('/hash', method=['POST'])
|
|
|
+@route('/code/preview', method=['GET'])
|
|
|
+def get_code_preview():
|
|
|
+ body = request.body
|
|
|
+ response.content_type = 'image/svg+xml'
|
|
|
+ if not body:
|
|
|
+ abort(404, 'Empty request')
|
|
|
+
|
|
|
+ return get_bar_code(body).decode('utf-8')
|
|
|
+
|
|
|
+
|
|
|
+@route('/<route:re:(clip|goto|upload|code)>/hash', method=['POST'])
|
|
|
def get_hash():
|
|
|
- data = dict(map(
|
|
|
- lambda x: (x[0], x[1].encode('utf-8')),
|
|
|
- load(request.body).items()
|
|
|
- ))
|
|
|
- _bytes = blake(**data)
|
|
|
+ body = load(request.body)
|
|
|
+ data = body['data']
|
|
|
+ person = body.get('person', None)
|
|
|
+ if route == 'upload':
|
|
|
+ data = parse_data_uri(data)
|
|
|
+ assert data['encoding'] == 'base64', f"unsupported encoding: {data['encoding']}"
|
|
|
+ data = b64decode(data['data'] + '==')
|
|
|
+ else:
|
|
|
+ data = data.encode('utf-8')
|
|
|
+
|
|
|
+ _bytes = blake(data, person = person and person.encode('utf-8'))
|
|
|
return bytes_to_base32(_bytes)
|
|
|
|
|
|
-@route('/normalize', method=['GET'])
|
|
|
+@route('/<route:re:(clip|goto|upload|code)>/normalize', method=['GET'])
|
|
|
def normalize():
|
|
|
_hash = request.params.hash
|
|
|
response.content_type = 'application/json'
|
|
@@ -63,10 +97,20 @@ def normalize():
|
|
|
'o': normalize_base32(_hash) if _hash else None,
|
|
|
})
|
|
|
|
|
|
-@route('/qr', method=['POST'])
|
|
|
-def get_qr():
|
|
|
- data = load(request.body)
|
|
|
- return get_qr_code(**data).decode('utf-8')
|
|
|
+@route('/<route:re:(clip|goto|upload)>/qr', method=['POST'])
|
|
|
+def get_qr(route):
|
|
|
+ body = load(request.body)
|
|
|
+ data = body['data']
|
|
|
+ fallback = body.get('fallback', None)
|
|
|
+ if route == 'upload':
|
|
|
+ data = parse_data_uri(data)
|
|
|
+ assert data['encoding'] == 'base64', f"unsupported encoding: {data['encoding']}"
|
|
|
+ data = b64decode(data['data'] + '==')
|
|
|
+ else:
|
|
|
+ data = data.encode('utf-8')
|
|
|
+
|
|
|
+ response.content_type = 'image/svg+xml'
|
|
|
+ return get_qr_code(data, fallback = fallback).decode('utf-8')
|
|
|
|
|
|
@route('/static/<filename:path>')
|
|
|
def send_static(filename):
|
|
@@ -76,22 +120,24 @@ def send_static(filename):
|
|
|
def _get_clip(route):
|
|
|
return redirect(f'/{route}/open.sql')
|
|
|
|
|
|
-@route('/<route:re:(clip|goto|upload)>', method=['GET', 'POST'])
|
|
|
+@route('/<route:re:(clip|goto|upload|code)>', method=['GET', 'POST'])
|
|
|
def clip(route):
|
|
|
return redirect(f'/{route}.sql')
|
|
|
|
|
|
-@route(f'/<route:re:(clip|goto)>/<hash:re:{B32REGEX}{{1,5}}>', method='GET')
|
|
|
+@route(f'/<route:re:(clip|goto|code)>/<hash:re:{B32REGEX}{{1,5}}>', method='GET')
|
|
|
def get_clip(route, hash):
|
|
|
+ hash = hash and normalize_base32(hash)
|
|
|
return redirect(f'/{route}.sql?hash={hash}&go=true')
|
|
|
|
|
|
+
|
|
|
@route(f'/upload/<hash:re:{B32REGEX}{{1,5}}>', method='GET')
|
|
|
def get_upload(hash):
|
|
|
hash = hash and normalize_base32(hash)
|
|
|
con = connect('util.db')
|
|
|
fname, mimetype, content = (None, None, None)
|
|
|
try:
|
|
|
- fname, mimetype, content = con.cursor().execute(f"""
|
|
|
-SELECT name, mime, content
|
|
|
+ fname, mimetype, content, created = con.cursor().execute(f"""
|
|
|
+SELECT name, mime, content, created
|
|
|
FROM upload
|
|
|
WHERE hash = '{hash}'
|
|
|
LIMIT 1;
|
|
@@ -99,33 +145,25 @@ LIMIT 1;
|
|
|
finally:
|
|
|
con.close()
|
|
|
|
|
|
-
|
|
|
- _, mime, encoding, content = chain.from_iterable(map(
|
|
|
- lambda x: x.split(','), chain.from_iterable(map(
|
|
|
- lambda x: x.split(';'), content.split(':')
|
|
|
- ))
|
|
|
- ))
|
|
|
- assert encoding == 'base64', f'unsupported encoding: {encoding}'
|
|
|
- content = b64decode(content + '==')
|
|
|
+ data = parse_data_uri(content)
|
|
|
+ assert data['mimetype'].split(';', 1)[0] == mimetype.split(';', 1)[0], f"mimetype in db and data uri differ"
|
|
|
+ charset = data['params'].get('charset', None)
|
|
|
+ assert data['encoding'] == 'base64', f"unsupported encoding: {data['encoding']}"
|
|
|
+ content = b64decode(data['data'] + '==')
|
|
|
|
|
|
- if request.params.download == "false":
|
|
|
- download = False
|
|
|
headers = {}
|
|
|
headers['Content-Length'] = len(content)
|
|
|
|
|
|
|
|
|
headers['Content-Disposition'] = 'attachment; filename="%s"' % (fname or hash)
|
|
|
headers['Content-Encoding'] = 'application/octet-stream'
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
+
|
|
|
+ if mimetype:
|
|
|
+ if mimetype[:5] == 'text/' and charset and 'charset' not in mimetype:
|
|
|
+ mimetype += '; charset=%s' % charset
|
|
|
+ headers['Content-Type'] = mimetype
|
|
|
+ lm = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(created))
|
|
|
+ headers['Last-Modified'] = lm
|
|
|
return HTTPResponse(content, **headers)
|
|
|
|
|
|
@route('/<any>/', method='GET')
|