pyapi.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. #
  2. # Copyright (c) Daniel Sheffield 2023
  3. # All rights reserved
  4. #
  5. # THIS SOFTWARE IS PROVIDED AS IS WITHOUT WARRANTY
  6. import os
  7. from io import BytesIO
  8. from threading import Thread
  9. from typing import Union
  10. from bottle import (
  11. route, request, response,
  12. redirect, abort,
  13. template, static_file,
  14. FormsDict, HTTPError,
  15. )
  16. from psycopg import Cursor, connect
  17. from psycopg.rows import TupleRow
  18. from linkpreview import link_preview
  19. from .qr import get_qr_code
  20. from .validate import get_file_mimetype, get_filename, validate, validate_file, validate_parameter, validate_url
  21. from .hash_util import normalize_base32
  22. from .route_decorators import normalize, poison, cursor
  23. from .query_to_xml import get_categories, get_groups, get_products, get_tags
  24. from .CachedLoadingPage import CachedLoadingPage
  25. from .Cache import Cache
  26. from . import trend as worker
  27. from .save import save, save_upload
  28. host = f"host={os.getenv('HOST')}"
  29. db = f"dbname={os.getenv('DB', 'grocery')}"
  30. user = f"user={os.getenv('USER', 'das')}"
  31. password = f"password={os.getenv('PASSWORD','')}"
  32. if not password.split('=',1)[1]:
  33. password = ''
  34. conn = connect(f"{host} {db} {user} {password}")
  35. @route('/grocery/static/<filename:path>')
  36. def send_static(filename):
  37. return static_file(filename, root='app/rest/static')
  38. @route('/grocery/trend', method=['GET', 'POST'])
  39. @poison(cache=Cache(10))
  40. @normalize
  41. def trend(key: str, forms: FormsDict, cache: Cache):
  42. page = cache[key]
  43. if page:
  44. return page
  45. _, _, path, *_ = request.urlparts
  46. return cache.add(key, CachedLoadingPage(
  47. template("loading", progress=[]),
  48. lambda queue: Thread(target=worker.trend, args=(
  49. queue, conn, path, forms
  50. )).start()
  51. ))
  52. @route('/grocery/groups', method=['GET', 'POST'])
  53. @poison(cache=Cache(10))
  54. @normalize
  55. @cursor(connection=conn)
  56. def groups(cur: Cursor[TupleRow], key: Union[int, str], forms: FormsDict, cache: Cache):
  57. response.content_type = 'application/xhtml+xml; charset=utf-8'
  58. return get_groups(cur, forms)
  59. @route('/grocery/categories', method=['GET', 'POST'])
  60. @poison(cache=Cache(10))
  61. @normalize
  62. @cursor(connection=conn)
  63. def categories(cur: Cursor[TupleRow], key: Union[int, str], forms: FormsDict, cache: Cache):
  64. response.content_type = 'application/xhtml+xml; charset=utf-8'
  65. return get_categories(cur, forms)
  66. @route('/grocery/products', method=['GET', 'POST'])
  67. @poison(cache=Cache(10))
  68. @normalize
  69. @cursor(connection=conn)
  70. def products(cur: Cursor[TupleRow], key: Union[int, str], forms: FormsDict, cache: Cache):
  71. response.content_type = 'application/xhtml+xml; charset=utf-8'
  72. return get_products(cur, forms)
  73. @route('/grocery/tags', method=['GET', 'POST'])
  74. @poison(cache=Cache(10))
  75. @normalize
  76. @cursor(connection=conn)
  77. def tags(cur: Cursor[TupleRow], key: Union[int, str], forms: FormsDict, cache: Cache):
  78. response.content_type = 'application/xhtml+xml; charset=utf-8'
  79. return get_tags(cur, forms)
  80. SCHEME = "https://"
  81. HOST = ""
  82. DOMAIN = "shandan.one"
  83. PORT = ""
  84. LOCATION = SCHEME + (f"{HOST}." if HOST else "") + DOMAIN + (f":{PORT}" if PORT else "")
  85. @route('/clip', method=['GET', 'POST'])
  86. def clip():
  87. if request.method == 'GET':
  88. _hash = request.params.hash
  89. if _hash:
  90. _hash = normalize_base32(_hash)
  91. content = validate(_hash).decode('utf-8')
  92. else:
  93. content = None
  94. link = f'{LOCATION}/clip/{_hash}' if content else f'{LOCATION}/clip'
  95. svg = get_qr_code(content, fallback=link)
  96. response.content_type = 'text/html; charset=utf-8'
  97. form = template(
  98. 'clip-form',
  99. action='/clip',
  100. method='post',
  101. content=content,
  102. disabled=True if content else False
  103. )
  104. return template(
  105. 'paste',
  106. form=form,
  107. svg=svg,
  108. link=link,
  109. disabled=True if content else False,
  110. download=f'/clip/{_hash}' if content else None
  111. )
  112. if request.method == 'POST':
  113. content = validate_parameter(request, 'paste')
  114. if request.params.copy != 'true':
  115. _b32 = save(content)
  116. return redirect(f'/clip?hash={_b32}')
  117. response.content_type = 'text/html; charset=utf-8'
  118. form = template(
  119. 'clip-form',
  120. action='/clip',
  121. method='post',
  122. content=content,
  123. disabled=False
  124. )
  125. link = f'{LOCATION}/clip'
  126. svg = get_qr_code(content, fallback=link)
  127. return template(
  128. 'paste',
  129. form=form,
  130. svg=svg,
  131. link=link,
  132. disabled=False,
  133. download=None
  134. )
  135. @route('/clip/<filename:path>', method='GET')
  136. def get_clip(filename):
  137. filename = filename and normalize_base32(filename)
  138. if not request.params.raw.lower() == 'true':
  139. # TODO: return a form with timeout to a GET instead ?
  140. return redirect(f'/clip?hash={filename}')
  141. ret = validate(filename)
  142. if isinstance(ret, HTTPError):
  143. return ret
  144. return static_file('/'.join([filename,]*2) + '.file', root='app/rest/static')
  145. @route('/upload', method=['GET', 'POST'])
  146. def upload():
  147. if request.method == 'GET':
  148. _hash = request.params.hash
  149. mimetype = None
  150. if _hash:
  151. _hash = normalize_base32(_hash)
  152. name = get_filename(_hash)
  153. mimetype = get_file_mimetype(name)
  154. link = f'{LOCATION}/upload/{_hash}' if _hash else f'{LOCATION}/upload'
  155. response.content_type = 'text/html; charset=utf-8'
  156. form = template('file-form', action='/upload', method='post')
  157. svg = get_qr_code(link)
  158. return template('upload', form=form, svg=svg, link=link, mimetype=mimetype)
  159. if request.method == 'POST':
  160. if 'paste' not in request.files:
  161. return abort(400, "Parameter 'paste' must be specified")
  162. upload = request.files['paste']
  163. if isinstance(upload.file, BytesIO):
  164. if len(upload.file.read()) == 0:
  165. return abort(400, "File is empty")
  166. _b32 = save_upload(upload.raw_filename , upload.file, root='app/rest/static')
  167. return redirect(f'/upload?hash={_b32}')
  168. @route('/upload/<filename:path>', method='GET')
  169. def get_upload(filename):
  170. filename = filename and normalize_base32(filename)
  171. download = True
  172. if request.params.download == "false":
  173. download = False
  174. return validate_file(filename, download=download)
  175. @route('/goto', method=['GET', 'POST'])
  176. def goto():
  177. if request.method == 'GET':
  178. _hash = request.params.hash
  179. if _hash:
  180. _hash = normalize_base32(_hash)
  181. content = validate(_hash).decode('utf-8')
  182. else:
  183. content = None
  184. if content and request.params.go == 'true':
  185. target = validate_url(content)
  186. return redirect(target)
  187. link = f'{LOCATION}/goto/{_hash}' if content else f'{LOCATION}/goto'
  188. svg = get_qr_code(content, fallback=link)
  189. disabled = True if content else False
  190. response.content_type = 'text/html; charset=utf-8'
  191. form = template(
  192. 'goto-form',
  193. action='/goto',
  194. method='post',
  195. content=content,
  196. disabled=disabled
  197. )
  198. preview = dict()
  199. if content:
  200. try:
  201. page = link_preview(link, parser="lxml")
  202. preview['title'] = page.title
  203. preview['img'] = page.absolute_image
  204. preview['domain'] = page.site_name
  205. preview['link'] = content
  206. except:
  207. pass
  208. return template(
  209. 'goto',
  210. form=form,
  211. svg=svg,
  212. link=link,
  213. disabled=disabled,
  214. preview=preview,
  215. )
  216. if request.method == 'POST':
  217. content = validate_parameter(request, 'url')
  218. _b32 = save(content)
  219. # validate but save content unmodified
  220. _ = validate_url(content.decode('utf-8'))
  221. return redirect(f'/goto?hash={_b32}')
  222. @route('/goto/<filename:path>', method='GET')
  223. def redirect_goto(filename):
  224. filename = filename and normalize_base32(filename)
  225. return redirect(f'/goto?hash={filename}&go=true')
  226. @route('/<any>/', method='GET')
  227. def redirect_trailing_slash(any): return redirect(f'/{any}')