docs_server.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. # -*- coding: utf-8 -*-
  2. """
  3. (c) 2014-2016 - Copyright Red Hat Inc
  4. Authors:
  5. Pierre-Yves Chibon <pingou@pingoured.fr>
  6. """
  7. from __future__ import absolute_import, unicode_literals
  8. import logging
  9. import os
  10. import flask
  11. import pygit2
  12. from binaryornot.helpers import is_binary_string
  13. from whitenoise import WhiteNoise
  14. import pagure.config
  15. import pagure.doc_utils
  16. import pagure.exceptions
  17. import pagure.forms
  18. import pagure.lib.mimetype
  19. import pagure.lib.model_base
  20. import pagure.lib.query
  21. # Create the application.
  22. APP = flask.Flask(__name__)
  23. # Setup WhiteNoise for serving static files
  24. here = os.path.abspath(
  25. os.path.join(os.path.dirname(os.path.abspath(__file__)))
  26. )
  27. APP.wsgi_app = WhiteNoise(
  28. APP.wsgi_app, root=os.path.join(here, "static"), prefix="/static"
  29. )
  30. # set up FAS
  31. APP.config = pagure.config.reload_config()
  32. SESSION = pagure.lib.model_base.create_session(APP.config["DB_URL"])
  33. if not APP.debug:
  34. APP.logger.addHandler(
  35. pagure.mail_logging.get_mail_handler(
  36. smtp_server=APP.config.get("SMTP_SERVER", "127.0.0.1"),
  37. mail_admin=APP.config.get("MAIL_ADMIN", APP.config["EMAIL_ERROR"]),
  38. from_email=APP.config.get(
  39. "FROM_EMAIL", "pagure@fedoraproject.org"
  40. ),
  41. )
  42. )
  43. # Send classic logs into syslog
  44. SHANDLER = logging.StreamHandler()
  45. SHANDLER.setLevel(APP.config.get("log_level", "INFO"))
  46. APP.logger.addHandler(SHANDLER)
  47. _log = logging.getLogger(__name__)
  48. TMPL_HTML = """
  49. <!DOCTYPE html>
  50. <html lang='en'>
  51. <head>
  52. <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
  53. <style type="text/css">
  54. ul {{
  55. margin: 0;
  56. padding: 0;
  57. }}
  58. </style>
  59. </head>
  60. <body>
  61. {content}
  62. </body>
  63. </html>
  64. """
  65. def __get_tree(repo_obj, tree, filepath, index=0, extended=False):
  66. """Retrieve the entry corresponding to the provided filename in a
  67. given tree.
  68. """
  69. filename = filepath[index]
  70. if isinstance(tree, pygit2.Blob): # pragma: no cover
  71. # If we were given a blob, then let's just return it
  72. return (tree, None, None)
  73. for element in tree:
  74. if element.name == filename or (
  75. not filename and element.name.startswith("index")
  76. ):
  77. # If we have a folder we must go one level deeper
  78. if element.filemode == 16384:
  79. if (index + 1) == len(filepath):
  80. filepath.append("")
  81. return __get_tree(
  82. repo_obj,
  83. repo_obj[element.oid],
  84. filepath,
  85. index=index + 1,
  86. extended=True,
  87. )
  88. else:
  89. return (element, tree, False)
  90. if filename == "":
  91. return (None, tree, extended)
  92. else:
  93. raise pagure.exceptions.FileNotFoundException(
  94. "File %s not found" % ("/".join(filepath),)
  95. )
  96. def __get_tree_and_content(repo_obj, commit, path):
  97. """Return the tree and the content of the specified file."""
  98. (blob_or_tree, tree_obj, extended) = __get_tree(
  99. repo_obj, commit.tree, path
  100. )
  101. if blob_or_tree is None:
  102. return (tree_obj, None, None)
  103. if not repo_obj[blob_or_tree.oid]:
  104. # Not tested and no idea how to test it, but better safe than sorry
  105. flask.abort(404, description="File not found")
  106. is_file = False
  107. try:
  108. is_file = isinstance(blob_or_tree, pygit2.TreeEntry)
  109. except AttributeError:
  110. is_file = isinstance(blob_or_tree, pygit2.Blob)
  111. if is_file:
  112. filename = blob_or_tree.name
  113. name, ext = os.path.splitext(filename)
  114. blob_obj = repo_obj[blob_or_tree.oid]
  115. if not is_binary_string(blob_obj.data):
  116. try:
  117. content, safe = pagure.doc_utils.convert_readme(
  118. blob_obj.data, ext
  119. )
  120. if safe:
  121. filename = name + ".html"
  122. except pagure.exceptions.PagureEncodingException:
  123. content = blob_obj.data
  124. else:
  125. content = blob_obj.data
  126. tree = sorted(tree_obj, key=lambda x: x.filemode)
  127. return (tree, content, filename)
  128. @APP.route("/<repo>/")
  129. @APP.route("/<namespace>.<repo>/")
  130. @APP.route("/<repo>/<path:filename>")
  131. @APP.route("/<namespace>.<repo>/<path:filename>")
  132. @APP.route("/fork/<username>/<repo>/")
  133. @APP.route("/fork/<namespace>.<username>/<repo>/")
  134. @APP.route("/fork/<username>/<repo>/<path:filename>")
  135. @APP.route("/fork/<namespace>.<username>/<repo>/<path:filename>")
  136. def view_docs(repo, username=None, namespace=None, filename=None):
  137. """Display the documentation"""
  138. if "." in repo:
  139. namespace, repo = repo.split(".", 1)
  140. repo = pagure.lib.query.get_authorized_project(
  141. SESSION, repo, user=username, namespace=namespace
  142. )
  143. if not repo:
  144. flask.abort(404, description="Project not found")
  145. if not repo.settings.get("project_documentation", True):
  146. flask.abort(404, description="This project has documentation disabled")
  147. reponame = repo.repopath("docs")
  148. if not os.path.exists(reponame):
  149. flask.abort(404, description="Documentation not found")
  150. repo_obj = pygit2.Repository(reponame)
  151. if not repo_obj.is_empty:
  152. commit = repo_obj[repo_obj.head.target]
  153. else:
  154. flask.abort(
  155. 404,
  156. flask.Markup(
  157. "No content found in the repository, you may want to read "
  158. 'the <a href="'
  159. 'https://docs.pagure.org/pagure/usage/using_doc.html">'
  160. "Using the doc repository of your project</a> documentation."
  161. ),
  162. )
  163. content = None
  164. tree = None
  165. if not filename:
  166. path = [""]
  167. else:
  168. path = [it for it in filename.split("/") if it]
  169. if commit:
  170. try:
  171. (tree, content, filename) = __get_tree_and_content(
  172. repo_obj, commit, path
  173. )
  174. except pagure.exceptions.FileNotFoundException as err:
  175. flask.flash("%s" % err, "error")
  176. except Exception as err:
  177. _log.exception(err)
  178. flask.abort(
  179. 500, description="Unkown error encountered and reported"
  180. )
  181. if not content:
  182. if not tree or not len(tree):
  183. flask.abort(404, description="No content found in the repository")
  184. html = "<li>"
  185. for el in tree:
  186. name = el.name
  187. # Append a trailing '/' to the folders
  188. if el.filemode == 16384:
  189. name += "/"
  190. html += '<ul><a href="{0}">{1}</a></ul>'.format(name, name)
  191. html += "</li>"
  192. content = TMPL_HTML.format(content=html)
  193. mimetype = "text/html"
  194. else:
  195. mimetype, _ = pagure.lib.mimetype.guess_type(filename, content)
  196. return flask.Response(content, mimetype=mimetype)