pyapi.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  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 bottle import (
  8. route, request, response,
  9. redirect, abort,
  10. template, static_file,
  11. HTTPResponse, HTTPError,
  12. )
  13. from linkpreview import link_preview
  14. from .validate import CLIP_SIZE_LIMIT, get_file_mimetype, get_file_size, get_filename, validate, validate_file, validate_parameter, validate_url
  15. from .hash_util import normalize_base32
  16. from .save import save, save_upload
  17. SCHEME = "https://"
  18. HOST = ""
  19. DOMAIN = "shandan.one"
  20. PORT = ""
  21. LOCATION = SCHEME + (f"{HOST}." if HOST else "") + DOMAIN + (f":{PORT}" if PORT else "")
  22. @route('/clip', method=['GET', 'POST'])
  23. def clip():
  24. if request.method == 'GET':
  25. _hash = request.params.hash
  26. if _hash:
  27. _hash = normalize_base32(_hash)
  28. content = validate(_hash).decode('utf-8')
  29. else:
  30. content = None
  31. link = f'{LOCATION}/clip/{_hash}' if content else f'{LOCATION}/clip'
  32. response.content_type = 'text/html; charset=utf-8'
  33. form = template(
  34. 'clip-form',
  35. action='/clip',
  36. method='post',
  37. content=content,
  38. disabled=True if content else False
  39. )
  40. return template(
  41. 'paste',
  42. form=form,
  43. qr=f'{LOCATION}/clip/{_hash}.qr' if content else f'{LOCATION}/grocery/static/clip-qr.svg',
  44. link=link,
  45. disabled=True if content else False,
  46. download=f'/clip/{_hash}' if content else None
  47. )
  48. if request.method == 'POST':
  49. content = validate_parameter(request, 'paste')
  50. if request.params.copy != 'true':
  51. _b32 = save(content, LOCATION)
  52. return redirect(f'/clip?hash={_b32}')
  53. response.content_type = 'text/html; charset=utf-8'
  54. form = template(
  55. 'clip-form',
  56. action='/clip',
  57. method='post',
  58. content=content,
  59. disabled=False
  60. )
  61. link = f'{LOCATION}/clip'
  62. return template(
  63. 'paste',
  64. form=form,
  65. qr=f'{LOCATION}/grocery/static/clip-qr.svg',
  66. link=link,
  67. disabled=False,
  68. download=None
  69. )
  70. @route('/clip/<filename:path>', method='GET')
  71. def get_clip(filename):
  72. ext = 'file'
  73. if filename and filename.endswith('.qr'):
  74. filename, ext = filename.split('.', 1)
  75. filename = filename and normalize_base32(filename)
  76. path = f'{filename}/{filename}.{ext}'
  77. if ext == 'qr':
  78. return static_file(path, root='rest/static', mimetype='image/svg+xml')
  79. filename = filename and normalize_base32(filename)
  80. if not request.params.raw.lower() == 'true':
  81. # TODO: return a form with timeout to a GET instead ?
  82. return redirect(f'/clip?hash={filename}')
  83. ret = validate(filename)
  84. if isinstance(ret, HTTPError):
  85. return ret
  86. return static_file(path, root='rest/static')
  87. @route('/upload', method=['GET', 'POST'])
  88. def upload():
  89. if request.method == 'GET':
  90. _hash = request.params.hash
  91. mimetype = None
  92. if _hash:
  93. _hash = normalize_base32(_hash)
  94. size = get_file_size(_hash)
  95. name = get_filename(_hash)
  96. mimetype = get_file_mimetype(name)
  97. if mimetype is not True and mimetype.startswith('text'):
  98. mimetype = None if size > CLIP_SIZE_LIMIT else mimetype
  99. link = f'{LOCATION}/upload/{_hash}' if _hash else f'{LOCATION}/upload'
  100. response.content_type = 'text/html; charset=utf-8'
  101. disabled = True if _hash else False
  102. form = template('file-form', action='/upload', method='post', disabled=disabled)
  103. return template(
  104. 'upload',
  105. form=form,
  106. qr=f'{LOCATION}/upload/{_hash}.qr' if _hash else f'{LOCATION}/grocery/static/upload-qr.svg',
  107. link=link,
  108. mimetype=mimetype,
  109. disabled=disabled
  110. )
  111. if request.method == 'POST':
  112. if 'paste' not in request.files:
  113. return abort(400, "Parameter 'paste' must be specified")
  114. upload = request.files['paste']
  115. if isinstance(upload.file, BytesIO):
  116. if len(upload.file.read()) == 0:
  117. return abort(400, "File is empty")
  118. _b32 = save_upload(upload.raw_filename, LOCATION, upload.file, root='rest/static')
  119. return redirect(f'/upload?hash={_b32}')
  120. @route('/upload/<filename:path>', method='GET')
  121. def get_upload(filename):
  122. ext = 'file'
  123. if filename and filename.endswith('.qr'):
  124. filename, ext = filename.split('.', 1)
  125. filename = filename and normalize_base32(filename)
  126. path = f'{filename}/{filename}.{ext}'
  127. if ext == 'qr':
  128. return static_file(path, root='rest/static', mimetype='image/svg+xml')
  129. download = True
  130. mimetype = True
  131. if request.params.download == "false":
  132. download = False
  133. mimetype = request.params.mimetype or None
  134. return validate_file(filename, download=download, mimetype=mimetype)
  135. @route('/goto', method=['GET', 'POST'])
  136. def goto():
  137. if request.method == 'GET':
  138. _hash = request.params.hash
  139. if _hash:
  140. _hash = normalize_base32(_hash)
  141. content = validate(_hash).decode('utf-8')
  142. else:
  143. content = None
  144. if content and request.params.go == 'true':
  145. target = validate_url(content)
  146. return redirect(target)
  147. link = f'{LOCATION}/goto/{_hash}' if content else f'{LOCATION}/goto'
  148. disabled = True if content else False
  149. response.content_type = 'text/html; charset=utf-8'
  150. form = template(
  151. 'goto-form',
  152. action='/goto',
  153. method='post',
  154. content=content,
  155. disabled=disabled
  156. )
  157. preview = dict()
  158. if content:
  159. try:
  160. page = link_preview(link, parser="lxml")
  161. preview['title'] = page.title
  162. preview['img'] = page.absolute_image
  163. preview['domain'] = page.site_name
  164. preview['link'] = content
  165. except:
  166. pass
  167. return template(
  168. 'goto',
  169. form=form,
  170. qr=f'{LOCATION}/goto/{_hash}.qr' if content else f'{LOCATION}/grocery/static/goto-qr.svg',
  171. link=link,
  172. disabled=disabled,
  173. preview=preview,
  174. )
  175. if request.method == 'POST':
  176. content = validate_parameter(request, 'url')
  177. _b32 = save(content, LOCATION)
  178. # validate but save content unmodified
  179. _ = validate_url(content.decode('utf-8'))
  180. return redirect(f'/goto?hash={_b32}')
  181. @route('/goto/<filename:path>', method='GET')
  182. def redirect_goto(filename):
  183. ext = 'file'
  184. if filename and filename.endswith('.qr'):
  185. filename, ext = filename.split('.', 1)
  186. filename = filename and normalize_base32(filename)
  187. path = f'{filename}/{filename}.{ext}'
  188. if ext == 'qr':
  189. return static_file(path, root='rest/static', mimetype='image/svg+xml')
  190. return redirect(f'/goto?hash={filename}&go=true')
  191. @route('/<any>/', method='GET')
  192. def redirect_trailing_slash(any): return redirect(f'/{any}')