pastesrht: Use GraphQL for paste CRUD operations

This commit is contained in:
Adnan Maolood 2022-06-14 21:27:06 -04:00 committed by Drew DeVault
parent f9571a8868
commit 907980513a
4 changed files with 150 additions and 86 deletions

View File

@ -1,4 +1,5 @@
import humanize
import json
import stat
from pastesrht import filters
from pastesrht.oauth import PasteOAuthService
@ -30,6 +31,7 @@ class PasteApp(SrhtFlask):
def inject():
return {
"humanize": humanize,
"json": json.dumps,
"stat": stat,
}

View File

@ -1,9 +1,10 @@
from flask import Blueprint, abort, request
from hashlib import sha1
from pastesrht.access import get_user_or_abort, has_access, paste_add_file, paste_drop, UserAccess
from pastesrht.access import get_user_or_abort, has_access, UserAccess
from pastesrht.types import Paste, Blob, PasteFile, PasteVisibility
from srht.api import paginated_response
from srht.database import db
from srht.graphql import exec_gql, GraphQLOperation, GraphQLUpload
from srht.oauth import oauth, current_token
from srht.validation import Validation
@ -30,15 +31,9 @@ def pastes_POST():
files = valid.require("files", cls=list)
if not valid.ok:
return valid.response
paste = Paste()
paste.user = current_token.user
paste.user_id = current_token.user_id
visibility = valid.optional("visibility",
cls=PasteVisibility,
default=PasteVisibility.unlisted)
paste.visibility = visibility
db.session.flush()
paste_sha = sha1()
filenames = set()
for i, f in enumerate(files):
valid.expect(isinstance(f, dict),
@ -59,17 +54,42 @@ def pastes_POST():
filenames.update({filename})
if not valid.ok:
return valid.response
for f in files:
contents = f.get("contents").replace('\r\n', '\n').replace('\r', '\n')
paste_file, blob = paste_add_file(paste, contents, f.get("filename"))
paste_sha.update((paste_file.filename
if paste_file.filename else "\0").encode())
paste_sha.update(blob.sha.encode())
paste_sha.update(str(current_token.user_id).encode())
paste_sha.update(str(paste.id).encode())
paste.sha = paste_sha.hexdigest()
db.session.commit()
return paste.to_dict(), 201
op = GraphQLOperation("""
mutation CreatePaste($files: [Upload!]!, $visibility: Visibility!) {
create(files: $files, visibility: $visibility) {
created
visibility
sha: id
user {
canonical_name: canonicalName
... on User {
name: username
}
}
files {
filename
blob_id: hash
}
}
}
""")
files = [
GraphQLUpload(
file.get("filename"),
file.get("contents").replace('\r\n', '\n').replace('\r', '\n'),
"text/plain",
) for file in files
]
op.var("files", files)
op.var("visibility", visibility.value.upper())
resp = op.execute("paste.sr.ht", user=current_token.user, valid=valid)
if not valid.ok:
return valid.response
resp = resp["create"]
resp["visibility"] = resp["visibility"].lower()
return resp, 201
@pastes.route("/api/pastes/<sha>")
@oauth("pastes:read")
@ -93,9 +113,31 @@ def paste_by_id_PUT(sha):
visibility = valid.optional("visibility",
cls=PasteVisibility,
default=paste.visibility)
paste.visibility = visibility
db.session.commit()
return paste.to_dict()
resp = exec_gql("paste.sr.ht", """
mutation UpdatePaste($id: String!, $visibility: Visibility!) {
update(id: $id, visibility: $visibility) {
created
visibility
sha: id
user {
canonical_name: canonicalName
... on User {
name: username
}
}
files {
filename
blob_id: hash
}
}
}
""", user=current_token.user, valid=valid, id=sha, visibility=visibility.value.upper())
if not valid.ok:
return valid.response
resp = resp["update"]
resp["visibility"] = resp["visibility"].lower()
return resp
@pastes.route("/api/pastes/<sha>", methods=['DELETE'])
@oauth("pastes:write")
@ -105,7 +147,11 @@ def paste_by_id_DELETE(sha):
abort(404)
if not has_access(current_token.user, paste, UserAccess.write):
abort(401)
paste_drop(paste)
exec_gql("paste.sr.ht", """
mutation DeletePaste($id: String!) {
delete(id: $id) { id }
}
""", user=current_token.user, id=sha)
return {}, 204
@pastes.route("/api/blobs/<sha>")

View File

@ -1,16 +1,20 @@
import json
import pygments
from flask import Blueprint, render_template, request, redirect, Response
from datetime import timedelta
from flask import Blueprint, current_app, render_template, request, redirect, Response
from flask import url_for, abort
from hashlib import sha1
from markupsafe import Markup
from pastesrht.access import get_paste_or_abort, get_user_or_abort, has_access, paste_add_file, paste_drop, DbLock, UserAccess
from pastesrht.access import get_paste_or_abort, get_user_or_abort, has_access, DbLock, UserAccess
from pastesrht.types import Paste, PasteFile, PasteVisibility, Blob
from pastesrht.search import apply_search
from pygments import highlight
from pygments.formatters import HtmlFormatter
from pygments.lexers import guess_lexer, guess_lexer_for_filename, TextLexer
from srht.cache import get_cache, set_cache
from srht.database import db
from srht.flask import paginate_query
from srht.graphql import exec_gql, GraphQLOperation, GraphQLUpload
from srht.oauth import current_user, loginrequired
from srht.validation import Validation
@ -22,6 +26,27 @@ def index():
return render_template("new-paste.html")
return render_template("index.html")
def create_paste(valid, files, visibility):
op = GraphQLOperation("""
mutation CreatePaste($files: [Upload!]!, $visibility: Visibility!) {
create(files: $files, visibility: $visibility) { id }
}
""")
uploads = []
for file in files:
filename = file.get("filename")
cache_key = "paste.sr.ht:blobs:{0}".format(file.get("sha"))
contents = get_cache(cache_key)
if contents is None:
abort(404)
uploads.append(GraphQLUpload(filename, contents.decode(), "text/plain"))
op.var("files", uploads)
op.var("visibility", visibility.value.upper())
resp = op.execute("paste.sr.ht", valid=valid)
if not valid.ok:
return None
return resp["create"]["id"]
@public.route("/new-paste", methods=["POST"])
@loginrequired
def new_paste_POST():
@ -31,65 +56,48 @@ def new_paste_POST():
contents = contents.replace("\r\n", "\n").replace("\r", "\n")
filename = valid.optional("filename")
commit = valid.require("commit")
visibility = valid.optional("visibility", cls=PasteVisibility)
visibility = valid.require("visibility", cls=PasteVisibility)
filename = filename.strip() if filename else filename
paste = None
paste_id = valid.optional("paste-id")
if paste_id is not None:
paste = Paste.query.filter(Paste.id == int(paste_id)).one_or_none()
if not paste:
abort(404)
if not visibility and paste.visibility:
visibility = paste.visibility
elif visibility:
paste.visibility = visibility
if commit == "force":
sha = sha1()
for f in paste.files:
sha.update((f.filename if f.filename else "\0").encode())
sha.update(f.blob.sha.encode())
sha.update(str(current_user.id).encode())
paste.sha = sha.hexdigest()
db.session.commit()
return redirect(url_for(".paste_GET",
user=current_user.username, sha=paste.sha))
if not paste:
paste = Paste()
paste.user_id = current_user.id
if visibility:
paste.visibility = visibility
db.session.add(paste)
files = valid.optional("files")
files = json.loads(files) if files else []
valid.kwargs.pop("files", None)
for f in paste.files:
if f.filename == filename:
if commit == "force":
valid.errors = [] # Clear validation errors since contents is not required
paste_id = create_paste(valid, files, visibility)
if not valid.ok:
return render_template("new-paste.html", **valid.kwargs)
return redirect(url_for(".paste_GET", user=current_user.username,
sha=paste_id))
for f in files:
if f.get("filename") == filename:
# TODO: Edit this file?
valid.error("A file with this name already exists in this paste.",
field="filename")
if not valid.ok:
return render_template("new-paste.html", paste=paste, **valid.kwargs)
db.session.flush()
paste_file, blob = paste_add_file(paste, contents, filename)
if commit == "no":
return render_template("new-paste.html", paste=paste)
return render_template("new-paste.html", files=files, **valid.kwargs)
sha = sha1()
for f in paste.files:
sha.update((f.filename if f.filename else "\0").encode())
sha.update(f.blob.sha.encode())
sha.update(str(current_user.id).encode())
sha.update(str(paste.id).encode())
paste.sha = sha.hexdigest()
if visibility:
paste.visibility = visibility
db.session.add(paste)
db.session.commit()
return redirect(url_for(".paste_GET",
user=current_user.username, sha=paste.sha))
sha.update(contents.encode())
sha = sha.hexdigest()
files.append({
"sha": sha,
"filename": filename,
"size": len(contents),
})
set_cache("paste.sr.ht:blobs:{0}".format(sha), timedelta(hours=1), contents.encode())
if commit == "no":
return render_template("new-paste.html", files=files, visibility=visibility)
paste_id = create_paste(valid, files, visibility)
if not valid.ok:
return render_template("new-paste.html", files=files, **valid.kwargs)
return redirect(url_for(".paste_GET", user=current_user.username,
sha=paste_id))
def _get_shebang(data):
if not data.startswith('#!'):
@ -166,8 +174,12 @@ def paste_manage_POST(user, sha):
visibility = valid.optional("visibility",
cls=PasteVisibility,
default=paste.visibility)
paste.visibility = visibility
db.session.commit()
exec_gql(current_app.site, """
mutation UpdatePaste($id: String!, $visibility: Visibility!) {
update(id: $id, visibility: $visibility) { id }
}
""", id=sha, visibility=visibility.value.upper())
return redirect(url_for('.paste_manage', user=user.username, sha=sha))
@ -185,7 +197,11 @@ def paste_delete_POST(user, sha):
if not has_access(current_user, paste, UserAccess.write):
abort(401)
paste_drop(paste)
exec_gql(current_app.site, """
mutation DeletePaste($id: String!) {
delete(id: $id) { id }
}
""", id=sha)
return redirect(url_for('.index'))
@public.route("/blob/<sha>")

View File

@ -15,8 +15,8 @@
<div class="col-lg-8">
<form action="/new-paste" method="POST">
{{csrf_token()}}
{% if paste and paste.id %}
<input type="hidden" name="paste-id" value="{{ paste.id }}" />
{% if files %}
<input type="hidden" name="files" value="{{ json(files) }}" />
{% endif %}
<div class="form-group">
<input
@ -51,7 +51,7 @@
type="radio"
name="visibility"
value="public"
{{ "checked" if paste and paste.visibility.value == "public" else "" }}
{{ "checked" if visibility and visibility.value == "public" else "" }}
> Public
</label>
</div>
@ -65,7 +65,7 @@
type="radio"
name="visibility"
value="unlisted"
{{ "checked" if not paste or paste.visibility.value == "unlisted" else "" }}
{{ "checked" if not visibility or visibility.value == "unlisted" else "" }}
> Unlisted
</label>
</div>
@ -79,7 +79,7 @@
type="radio"
name="visibility"
value="private"
{{ "checked" if paste and paste.visibility.value == "private" else "" }}
{{ "checked" if visibility and visibility.value == "private" else "" }}
> Private
</label>
</div>
@ -99,10 +99,10 @@
>Create paste {{icon('caret-right')}}</button>
</div>
<div class="clearfix"></div>
{% if paste %}
{% if files %}
<div class="event-list" style="margin-top: 1rem">
<div class="file-list event">
{% for file in paste.files %}
{% for file in files %}
<div style="grid-column-start: 1">{{file.filename}}</div>
<div class="text-muted" style="margin-left: 1rem">
<span title="{{"{0:0o}".format(33188)}}">
@ -110,8 +110,8 @@
</span>
</div>
<div class="text-muted" style="margin-left: 1rem">
<span title="{{ len(file.blob.contents) }} bytes">
{{humanize.naturalsize(len(file.blob.contents),
<span title="{{ file.size }} bytes">
{{humanize.naturalsize(file.size,
binary=True).replace("Byte", "byte")}}
</span>
</div>