Allow to update own pastes.

This commit is contained in:
Julien Palard 2023-04-25 18:01:34 +02:00
parent 2b13d6cbb0
commit 133fd480b8
Signed by: mdk
GPG Key ID: 0EFC1AC1006886F8
5 changed files with 115 additions and 202 deletions

View File

@ -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:

View File

@ -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."""

View File

@ -1,147 +0,0 @@
{% extends "base.html" %}
{% load filters %}
{% load i18n %}
{% load compress %}
{% block content %}
<h1>Paf'Py</h1>
<p>Et PAF !</p>
<h2>Envoyer un ou plusieurs fichiers</h2>
En utilisant des requêtes <tt>multipart/form-data</tt> il est possible
d'envoyer un fichier :
<div class="highlight"><pre><span></span>curl {{ request.build_absolute_uri }} <span class="o">-F</span>manage.py<span class="o">=</span>@manage.py
| URL | size | filename |
|-------------------------|--------|-----------|
| https://p.afpy.org/g3LE | 251 | manage.py |
</pre></div>
ou plusieurs fichiers en même temps :
<div class="highlight"><pre><span></span>curl {{ request.build_absolute_uri }} <span class="o">-F</span>manage.py<span class="o">=</span>@manage.py <span class="o">-F</span>requirements.txt<span class="o">=</span>@requirements.txt
| URL | size | filename |
|-------------------------|--------|------------------|
| https://p.afpy.org/g3LE | 251 | manage.py |
| https://p.afpy.org/k4oT | 547 | requirements.txt |
</pre></div>
C'est l'extension dans le nom du fichier qui permet de choisir la
coloration syntaxique pour chaque fichier.
<h2>Envoyer dans le corps d'une requête</h2>
Il est possible de coller un unique fichier via le corps d'une requête <tt>POST</tt> :
<div class="highlight"><pre>
$ cal | curl -XPOST --data-binary @- {{ request.build_absolute_uri }}
| URL | size | filename |
|-------------------------|--------|----------|
| https://p.afpy.org/mo8X | 184 | request |
</pre></div>
Dans ce cas, il est possible de choisir la coloration syntaxique via l'entête <tt>Content-Type</tt> :
<div class="highlight"><pre>
$ cal | curl -XPOST -H <span class="s2">"Content-Type: text/plain"</span> --data-binary @- {{ request.build_absolute_uri }}
| URL | size | filename |
|-------------------------|--------|-------------|
| https://p.afpy.org/dNuo | 184 | request.txt |
</pre></div>
<h2>Fonction <tt>bash</tt></h2>
<p>Pour ceux qui ne souhaitent pas rédiger des requêtes <tt>curl</tt>
toute la journée, voici une petite fonction <tt>bash</tt> :</p>
<div class="highlight"><pre><span></span>paf<span class="o">()</span>
<span class="o">{</span>
<span class="k">if</span> <span class="o">[[</span> <span class="nv">$#</span> <span class="o">==</span> <span class="m">0</span> <span class="o">]]</span>
<span class="k">then</span>
curl https://p.afpy.org/ --data-binary @- -H <span class="s2">&quot;Content-Type: text/plain&quot;</span>
<span class="k">else</span>
curl https://p.afpy.org/ <span class="s2">&quot;</span><span class="si">${</span><span class="p">@/*/-F&amp;=@&amp;</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="k">fi</span>
<span class="o">}</span>
</pre></div>
Cette fonction est capable d'envoyer un fichier :
<div class="highlight"><pre><span></span>$ paf manage.py
| URL | size | filename |
|-------------------------|--------|-----------|
| https://p.afpy.org/g3LE | 251 | manage.py |
</pre></div>
plusieurs fichiers :
<div class="highlight"><pre><span></span>$ 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 |
</pre></div>
et même de lire sur <tt>stdin</tt> :
<div class="highlight"><pre><span></span>$ cal | paf
| URL | size | filename |
|-------------------------|--------|-------------|
| https://p.afpy.org/dNuo | 184 | request.txt |
</pre></div>
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 :
<div class="highlight"><pre><span></span>$ paf *.py | paf
| URL | size | filename |
|-------------------------|--------|-------------|
| https://p.afpy.org/L5pc | 488 | request.txt |
<pre></div>
Ce qui donne :
<div class="highlight"><pre>
$ 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 |
</pre></div>
pratique pour partager tout un dossier.
<h2>Accès aux collages</h2>
Chaque collage peut-être consulté dans un navigateur (où il est présenté avec de la coloration syntaxique :
<a href="https://p.afpy.org/g3LE">https://p.afpy.org/g3LE</a>), ou
être consulté en ligne de commande (où il est délivré brut) :
<div class="highlight"><pre><span></span>$ 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)
</pre></div>
{% endblock %}

View File

@ -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/<path>", serve, {"document_root": settings.STATIC_ROOT}),
path("::/list/", views.list_view),
path("<path:slug>", views.show, name="short_paste"),
path("::/list/", views.ListView.as_view()),
re_path(r"^(?!::)(?P<path>.*)$", views.PasteView.as_view(), name="paste"),
]

View File

@ -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)