pyapi.py 7.2 KB

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