pyapi.py 7.8 KB

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