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
|
## Features
|
||||||
|
|
||||||
- Support any database supported by Django (Sqlite3, MySQL, PostgreSQL, Oracle, ...)
|
- 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.
|
- Syntax highlighting for a bunch of languages using Pygments.
|
||||||
|
- Rendering of Markdown as HTML.
|
||||||
- Easy paste from command line or any programming language.
|
- 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
|
## Running Pasteque
|
||||||
|
|
||||||
In a [venv](https://docs.python.org/3/library/venv.html), install the requirements:
|
In a [venv](https://docs.python.org/3/library/venv.html), install the requirements:
|
||||||
|
|
|
@ -39,12 +39,17 @@ class Paste(models.Model):
|
||||||
return
|
return
|
||||||
self.auth = sha256(secret.encode("UTF-8")).hexdigest()
|
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):
|
def compute_size(self):
|
||||||
"""Computes size."""
|
"""Computes size."""
|
||||||
self.size = len(self.content)
|
self.size = len(self.content)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse("short_paste", kwargs={"slug": self.slug})
|
return reverse("paste", kwargs={"path": self.slug})
|
||||||
|
|
||||||
def incr_viewcount(self):
|
def incr_viewcount(self):
|
||||||
"""Increment view counter."""
|
"""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 django.views.static import serve
|
||||||
|
|
||||||
from paste import views
|
from paste import views
|
||||||
from webtools import settings
|
from webtools import settings
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", views.index, name="index"),
|
path("", views.IndexView.as_view(), name="index"),
|
||||||
path("::/static/<path>", serve, {"document_root": settings.STATIC_ROOT}),
|
path("::/static/<path>", serve, {"document_root": settings.STATIC_ROOT}),
|
||||||
path("::/list/", views.list_view),
|
path("::/list/", views.ListView.as_view()),
|
||||||
path("<path:slug>", views.show, name="short_paste"),
|
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.http import Http404, HttpResponse, HttpResponseRedirect
|
||||||
from django.shortcuts import get_object_or_404, render
|
from django.shortcuts import get_object_or_404, render
|
||||||
from django.template import RequestContext, loader
|
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 django.views.decorators.csrf import csrf_exempt
|
||||||
from tabulate import tabulate
|
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"
|
return tabulate(values, headers=headers, tablefmt="github") + "\n"
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
@method_decorator(csrf_exempt, name="dispatch")
|
||||||
def index(request):
|
class PasteView(View):
|
||||||
"""Displays form."""
|
def get(self, request, path=""):
|
||||||
if request.method == "GET":
|
paste = get_object_or_404(Paste, slug=path)
|
||||||
return render(request, "paste/index.html")
|
paste.incr_viewcount()
|
||||||
if request.headers.get("Expect") == "100-continue":
|
|
||||||
return HttpResponse("")
|
if "html" in request.headers["accept"]:
|
||||||
pastes = []
|
return HttpResponse(paste_to_html(paste.filename, paste.content))
|
||||||
files = get_files(request)
|
else:
|
||||||
prefix = Paste.choose_prefix(list(files.keys()))
|
return HttpResponse(paste.content, content_type="text/plain")
|
||||||
for filename, the_file in files.items():
|
|
||||||
filename = filename.replace("\\", "/")
|
def put(self, request, path=""):
|
||||||
paste = Paste(
|
if request.headers.get("Expect") == "100-continue":
|
||||||
slug=f"{prefix}/{filename}".rstrip("/"),
|
return HttpResponse("")
|
||||||
filename=filename.split("/")[-1],
|
try:
|
||||||
content=the_file.read().decode("UTF-8"),
|
paste = Paste.objects.get(slug=path)
|
||||||
)
|
except Paste.DoesNotExist:
|
||||||
paste.set_secret(request.headers.get("Authorization"))
|
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.compute_size()
|
||||||
paste.save()
|
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(
|
return HttpResponse(
|
||||||
loader.render_to_string(
|
pastes_as_table(request, [paste]), content_type="text/plain"
|
||||||
"paste/show-markdown.html", {"highlighted": markdown_to_html(table)}
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
return HttpResponse(table, content_type="text/plain")
|
|
||||||
|
|
||||||
|
|
||||||
def show(request, slug):
|
@method_decorator(csrf_exempt, name="dispatch")
|
||||||
"""Display paste."""
|
class IndexView(PasteView):
|
||||||
if slug.startswith("::"):
|
def post(self, request):
|
||||||
raise Http404()
|
if request.headers.get("Expect") == "100-continue":
|
||||||
paste = get_object_or_404(Paste, slug=slug)
|
return HttpResponse("")
|
||||||
paste.incr_viewcount()
|
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(pastes_as_table(request, pastes), content_type="text/plain")
|
||||||
return HttpResponse(paste_to_html(paste.filename, paste.content))
|
|
||||||
else:
|
|
||||||
return HttpResponse(paste.content, 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)
|
@lru_cache(1024)
|
||||||
|
|
Loading…
Reference in New Issue