v1_api.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. __package__ = 'archivebox.api'
  2. from io import StringIO
  3. from traceback import format_exception
  4. from contextlib import redirect_stdout, redirect_stderr
  5. from django.http import HttpRequest, HttpResponse
  6. from django.core.exceptions import ObjectDoesNotExist, EmptyResultSet, PermissionDenied
  7. from ninja import NinjaAPI, Swagger
  8. # TODO: explore adding https://eadwincode.github.io/django-ninja-extra/
  9. from archivebox.config import VERSION
  10. from archivebox.config.version import get_COMMIT_HASH
  11. from api.auth import API_AUTH_METHODS
  12. COMMIT_HASH = get_COMMIT_HASH() or 'unknown'
  13. html_description=f'''
  14. <h3>Welcome to your ArchiveBox server's REST API <code>[v1 ALPHA]</code> homepage!</h3>
  15. <br/>
  16. <i><b>WARNING: This API is still in an early development stage and may change!</b></i>
  17. <br/>
  18. <ul>
  19. <li>⬅️ Manage your server: <a href="/admin/api/"><b>Setup API Keys</b></a>, <a href="/admin/">Go to your Server Admin UI</a>, <a href="/">Go to your Snapshots list</a>
  20. <li>💬 Ask questions and get help here: <a href="https://zulip.archivebox.io">ArchiveBox Chat Forum</a></li>
  21. <li>🐞 Report API bugs here: <a href="https://github.com/ArchiveBox/ArchiveBox/issues">Github Issues</a></li>
  22. <li>📚 ArchiveBox Documentation: <a href="https://github.com/ArchiveBox/ArchiveBox/wiki">Github Wiki</a></li>
  23. <li>📜 See the API source code: <a href="https://github.com/ArchiveBox/ArchiveBox/blob/dev/archivebox/api"><code>archivebox/api/</code></a></li>
  24. </ul>
  25. <small>Served by ArchiveBox v{VERSION} (<a href="https://github.com/ArchiveBox/ArchiveBox/commit/{COMMIT_HASH}"><code>{COMMIT_HASH[:8]}</code></a>), API powered by <a href="https://django-ninja.dev/"><code>django-ninja</code></a>.</small>
  26. '''
  27. def register_urls(api: NinjaAPI) -> NinjaAPI:
  28. # api.add_router('/auth/', 'api.v1_auth.router')
  29. api.add_router('/core/', 'api.v1_core.router')
  30. api.add_router('/crawls/', 'api.v1_crawls.router')
  31. api.add_router('/cli/', 'api.v1_cli.router')
  32. api.add_router('/workers/', 'api.v1_workers.router')
  33. return api
  34. class NinjaAPIWithIOCapture(NinjaAPI):
  35. def create_temporal_response(self, request: HttpRequest) -> HttpResponse:
  36. stdout, stderr = StringIO(), StringIO()
  37. with redirect_stderr(stderr):
  38. with redirect_stdout(stdout):
  39. request.stdout = stdout
  40. request.stderr = stderr
  41. response = super().create_temporal_response(request)
  42. # Diable caching of API responses entirely
  43. response['Cache-Control'] = 'no-store'
  44. # Add debug stdout and stderr headers to response
  45. response['X-ArchiveBox-Stdout'] = str(request.stdout)[200:]
  46. response['X-ArchiveBox-Stderr'] = str(request.stderr)[200:]
  47. # response['X-ArchiveBox-View'] = self.get_openapi_operation_id(request) or 'Unknown'
  48. # Add Auth Headers to response
  49. api_token = getattr(request, '_api_token', None)
  50. token_expiry = api_token.expires.isoformat() if api_token and api_token.expires else 'Never'
  51. response['X-ArchiveBox-Auth-Method'] = getattr(request, '_api_auth_method', None) or 'None'
  52. response['X-ArchiveBox-Auth-Expires'] = token_expiry
  53. response['X-ArchiveBox-Auth-Token-Id'] = api_token.abid if api_token else 'None'
  54. response['X-ArchiveBox-Auth-User-Id'] = request.user.pk if request.user.pk else 'None'
  55. response['X-ArchiveBox-Auth-User-Username'] = request.user.username if request.user.pk else 'None'
  56. # import ipdb; ipdb.set_trace()
  57. # print('RESPONDING NOW', response)
  58. return response
  59. api = NinjaAPIWithIOCapture(
  60. title='ArchiveBox API',
  61. description=html_description,
  62. version=VERSION,
  63. csrf=False,
  64. auth=API_AUTH_METHODS,
  65. urls_namespace="api-1",
  66. docs=Swagger(settings={"persistAuthorization": True}),
  67. # docs_decorator=login_required,
  68. # renderer=ORJSONRenderer(),
  69. )
  70. api = register_urls(api)
  71. urls = api.urls
  72. @api.exception_handler(Exception)
  73. def generic_exception_handler(request, err):
  74. status = 503
  75. if isinstance(err, (ObjectDoesNotExist, EmptyResultSet, PermissionDenied)):
  76. status = 404
  77. print(''.join(format_exception(err)))
  78. return api.create_response(
  79. request,
  80. {
  81. "succeeded": False,
  82. "message": f'{err.__class__.__name__}: {err}',
  83. "errors": [
  84. ''.join(format_exception(err)),
  85. # or send simpler parent-only traceback:
  86. # *([str(err.__context__)] if getattr(err, '__context__', None) else []),
  87. ],
  88. },
  89. status=status,
  90. )
  91. # import orjson
  92. # from ninja.renderers import BaseRenderer
  93. # class ORJSONRenderer(BaseRenderer):
  94. # media_type = "application/json"
  95. # def render(self, request, data, *, response_status):
  96. # return {
  97. # "success": True,
  98. # "errors": [],
  99. # "result": data,
  100. # "stdout": ansi_to_html(stdout.getvalue().strip()),
  101. # "stderr": ansi_to_html(stderr.getvalue().strip()),
  102. # }
  103. # return orjson.dumps(data)