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)