Allow to update own pastes.
This commit is contained in:
parent
2b13d6cbb0
commit
133fd480b8
29
README.md
29
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:
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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">"Content-Type: text/plain"</span>
|
||||
<span class="k">else</span>
|
||||
curl https://p.afpy.org/ <span class="s2">"</span><span class="si">${</span><span class="p">@/*/-F&=@&</span><span class="si">}</span><span class="s2">"</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 %}
|
|
@ -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"),
|
||||
]
|
||||
|
|
126
paste/views.py
126
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)
|
||||
|
|
Loading…
Reference in New Issue