diff --git a/README.md b/README.md index f782e06..09059b1 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,38 @@ instance is installed by ansible, the role is available here: ## Features - Support any database supported by Django (Sqlite3, MySQL, PostgreSQL, Oracle, ...) -- Available in english, french .. and easily translatable into another languages. - Syntax highlighting for a bunch of languages using Pygments. +- Rendering of Markdown as HTML. - Easy paste from command line or any programming language. +## Using + +### Where's the homepage? + +This pastebin has no homepage: its homepage is a paste like any +other. So to create one for your instance, just paste something to it, like: + + $ curl localhost:8000 -XPUT -H "Authorization: Secret supersecret" --data-binary @using.fr.md + +The `Authorization` allows you to update the paste by uploading it again. + + +### Where's the admin? + +The admin is hosted as `/::/admin/`. As almost any URL can be used by pastes, we "burry" the admin here. + +And no paste can start with `::`. + + +### What's this Authorization header? + +By providing a secret in the `Authorization` header, one can edit its +pasts by `PUT`ting to it, and list all its pastes by querying: + + curl localhost:8000/::/list/ -H "Authorization: Secret supersecret" + + ## Running Pasteque In a [venv](https://docs.python.org/3/library/venv.html), install the requirements: diff --git a/paste/models.py b/paste/models.py index 70616e3..14472e2 100644 --- a/paste/models.py +++ b/paste/models.py @@ -39,12 +39,17 @@ class Paste(models.Model): return self.auth = sha256(secret.encode("UTF-8")).hexdigest() + def check_secret(self, secret=None): + if not secret: + return False + return self.auth == sha256(secret.encode("UTF-8")).hexdigest() + def compute_size(self): """Computes size.""" self.size = len(self.content) def get_absolute_url(self): - return reverse("short_paste", kwargs={"slug": self.slug}) + return reverse("paste", kwargs={"path": self.slug}) def incr_viewcount(self): """Increment view counter.""" diff --git a/paste/templates/paste/index.html b/paste/templates/paste/index.html deleted file mode 100644 index f360ae7..0000000 --- a/paste/templates/paste/index.html +++ /dev/null @@ -1,147 +0,0 @@ -{% extends "base.html" %} -{% load filters %} -{% load i18n %} -{% load compress %} -{% block content %} -

Paf'Py

-

Et PAF !

- -

Envoyer un ou plusieurs fichiers

- -En utilisant des requêtes multipart/form-data il est possible -d'envoyer un fichier : - -
curl {{ request.build_absolute_uri }} -Fmanage.py=@manage.py
-| URL                     |   size | filename  |
-|-------------------------|--------|-----------|
-| https://p.afpy.org/g3LE |    251 | manage.py |
-
- -ou plusieurs fichiers en même temps : - -
curl {{ request.build_absolute_uri }} -Fmanage.py=@manage.py -Frequirements.txt=@requirements.txt
-| URL                     |   size | filename         |
-|-------------------------|--------|------------------|
-| https://p.afpy.org/g3LE |    251 | manage.py        |
-| https://p.afpy.org/k4oT |    547 | requirements.txt |
-
- -C'est l'extension dans le nom du fichier qui permet de choisir la -coloration syntaxique pour chaque fichier. - - -

Envoyer dans le corps d'une requête

- -Il est possible de coller un unique fichier via le corps d'une requête POST : - -
-$ cal | curl -XPOST --data-binary @- {{ request.build_absolute_uri }}
-| URL                     |   size | filename |
-|-------------------------|--------|----------|
-| https://p.afpy.org/mo8X |    184 | request  |
-
-
- -Dans ce cas, il est possible de choisir la coloration syntaxique via l'entête Content-Type : - -
-$ cal | curl -XPOST -H "Content-Type: text/plain" --data-binary @- {{ request.build_absolute_uri }}
-| URL                     |   size | filename    |
-|-------------------------|--------|-------------|
-| https://p.afpy.org/dNuo |    184 | request.txt |
-
-
- - -

Fonction bash

- -

Pour ceux qui ne souhaitent pas rédiger des requêtes curl -toute la journée, voici une petite fonction bash :

- -
paf()
-{
-    if [[ $# == 0 ]]
-    then
-        curl https://p.afpy.org/ --data-binary @- -H "Content-Type: text/plain"
-    else
-        curl https://p.afpy.org/ "${@/*/-F&=@&}"
-    fi
-}
-
- -Cette fonction est capable d'envoyer un fichier : - -
$ paf manage.py
-| URL                     |   size | filename  |
-|-------------------------|--------|-----------|
-| https://p.afpy.org/g3LE |    251 | manage.py |
-
- -plusieurs fichiers : - -
$ paf *.py
-| URL                     |   size | filename              |
-|-------------------------|--------|-----------------------|
-| https://p.afpy.org/bvRV |    188 | admin.py              |
-| https://p.afpy.org/5uei |    296 | context_processors.py |
-| https://p.afpy.org/Xg5a |   1419 | models.py             |
-| https://p.afpy.org/GkGS |    309 | urls.py               |
-| https://p.afpy.org/LVXL |   2730 | views.py              |
-
- -et même de lire sur stdin : - -
$ cal | paf
-| URL                     |   size | filename    |
-|-------------------------|--------|-------------|
-| https://p.afpy.org/dNuo |    184 | request.txt |
-
- - -Dernière démo, puisque le résultat d'un envoi est un tableau de toutes -les URL, il est tentant de le partager lui aussi : - -
$ paf *.py | paf
-| URL                     |   size | filename    |
-|-------------------------|--------|-------------|
-| https://p.afpy.org/L5pc |    488 | request.txt |
-
- -Ce qui donne : - -
-$ curl https://p.afpy.org/L5pc
-| URL                     |   size | filename              |
-|-------------------------|--------|-----------------------|
-| https://p.afpy.org/7rFj |    188 | admin.py              |
-| https://p.afpy.org/DLfp |    296 | context_processors.py |
-| https://p.afpy.org/9o33 |      0 | __init__.py           |
-| https://p.afpy.org/YdvG |   1419 | models.py             |
-| https://p.afpy.org/97fG |    309 | urls.py               |
-| https://p.afpy.org/oPRr |   2974 | views.py              |
-
- -pratique pour partager tout un dossier. - - -

Accès aux collages

- -Chaque collage peut-être consulté dans un navigateur (où il est présenté avec de la coloration syntaxique : -https://p.afpy.org/g3LE), ou -être consulté en ligne de commande (où il est délivré brut) : - -
$ curl https://p.afpy.org/g3LE
-#!/usr/bin/env python
-import os
-import sys
-
-if __name__ == "__main__":
-    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "webtools.settings")
-
-    from django.core.management import execute_from_command_line
-
-    execute_from_command_line(sys.argv)
-
-
- -{% endblock %} diff --git a/paste/urls.py b/paste/urls.py index 353f10c..54cdece 100644 --- a/paste/urls.py +++ b/paste/urls.py @@ -1,12 +1,12 @@ -from django.urls import path +from django.urls import path, re_path from django.views.static import serve from paste import views from webtools import settings urlpatterns = [ - path("", views.index, name="index"), + path("", views.IndexView.as_view(), name="index"), path("::/static/", serve, {"document_root": settings.STATIC_ROOT}), - path("::/list/", views.list_view), - path("", views.show, name="short_paste"), + path("::/list/", views.ListView.as_view()), + re_path(r"^(?!::)(?P.*)$", views.PasteView.as_view(), name="paste"), ] diff --git a/paste/views.py b/paste/views.py index d5147c5..09cab68 100644 --- a/paste/views.py +++ b/paste/views.py @@ -5,6 +5,8 @@ from mimetypes import common_types, types_map from django.http import Http404, HttpResponse, HttpResponseRedirect from django.shortcuts import get_object_or_404, render from django.template import RequestContext, loader +from django.utils.decorators import method_decorator +from django.views import View from django.views.decorators.csrf import csrf_exempt from tabulate import tabulate @@ -45,62 +47,88 @@ def pastes_as_table(request, pastes, headers=("URL", "size", "filename")): return tabulate(values, headers=headers, tablefmt="github") + "\n" -@csrf_exempt -def index(request): - """Displays form.""" - if request.method == "GET": - return render(request, "paste/index.html") - if request.headers.get("Expect") == "100-continue": - return HttpResponse("") - pastes = [] - files = get_files(request) - prefix = Paste.choose_prefix(list(files.keys())) - for filename, the_file in files.items(): - filename = filename.replace("\\", "/") - paste = Paste( - slug=f"{prefix}/{filename}".rstrip("/"), - filename=filename.split("/")[-1], - content=the_file.read().decode("UTF-8"), - ) - paste.set_secret(request.headers.get("Authorization")) +@method_decorator(csrf_exempt, name="dispatch") +class PasteView(View): + def get(self, request, path=""): + paste = get_object_or_404(Paste, slug=path) + paste.incr_viewcount() + + if "html" in request.headers["accept"]: + return HttpResponse(paste_to_html(paste.filename, paste.content)) + else: + return HttpResponse(paste.content, content_type="text/plain") + + def put(self, request, path=""): + if request.headers.get("Expect") == "100-continue": + return HttpResponse("") + try: + paste = Paste.objects.get(slug=path) + except Paste.DoesNotExist: + paste = Paste( + slug=path, + filename=path.rstrip("/").split("/")[-1], + ) + paste.set_secret(request.headers.get("Authorization")) + else: + if not paste.check_secret(request.headers.get("Authorization")): + return HttpResponse("Paste already exists.\n", status=401) + paste.content = request.read().decode("UTF-8") paste.compute_size() paste.save() - pastes.append(paste) - - return HttpResponse(pastes_as_table(request, pastes), content_type="text/plain") - - -def list_view(request): - secret = request.headers.get("Authorization") - pastes = [] - if secret: - pastes = Paste.objects.by_secret(secret).order_by("paste_time") - table = pastes_as_table( - request, - pastes, - headers=("filename", "size", "URL", "paste_time", "access_time"), - ) - if "html" in request.headers["accept"]: return HttpResponse( - loader.render_to_string( - "paste/show-markdown.html", {"highlighted": markdown_to_html(table)} - ) + pastes_as_table(request, [paste]), content_type="text/plain" ) - else: - return HttpResponse(table, content_type="text/plain") -def show(request, slug): - """Display paste.""" - if slug.startswith("::"): - raise Http404() - paste = get_object_or_404(Paste, slug=slug) - paste.incr_viewcount() +@method_decorator(csrf_exempt, name="dispatch") +class IndexView(PasteView): + def post(self, request): + if request.headers.get("Expect") == "100-continue": + return HttpResponse("") + pastes = [] + files = get_files(request) + prefix = Paste.choose_prefix(list(files.keys())) + for filename, the_file in files.items(): + filename = filename.replace("\\", "/") + paste = Paste( + slug=f"{prefix}/{filename}".rstrip("/"), + filename=filename.split("/")[-1], + content=the_file.read().decode("UTF-8"), + ) + paste.set_secret(request.headers.get("Authorization")) + paste.compute_size() + paste.save() + pastes.append(paste) - if "html" in request.headers["accept"]: - return HttpResponse(paste_to_html(paste.filename, paste.content)) - else: - return HttpResponse(paste.content, content_type="text/plain") + return HttpResponse(pastes_as_table(request, pastes), content_type="text/plain") + + +class ListView(View): + def get(self, request): + secret = request.headers.get("Authorization") + pastes = [] + if secret: + pastes = Paste.objects.by_secret(secret).order_by("paste_time") + table = pastes_as_table( + request, + pastes, + headers=( + "filename", + "size", + "URL", + "paste_time", + "access_time", + "viewcount", + ), + ) + if "html" in request.headers["accept"]: + return HttpResponse( + loader.render_to_string( + "paste/show-markdown.html", {"highlighted": markdown_to_html(table)} + ) + ) + else: + return HttpResponse(table, content_type="text/plain") @lru_cache(1024)