pastesrht: Use GraphQL for paste CRUD operations
This commit is contained in:
parent
f9571a8868
commit
907980513a
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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>")
|
||||
|
|
|
@ -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>")
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue