aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFulgen301 <tokmajigeorge@gmail.com>2018-09-16 16:58:21 +0200
committerFulgen301 <tokmajigeorge@gmail.com>2018-09-16 16:58:21 +0200
commit027139279aa0e83c123ba26139d84505f1d4af90 (patch)
treeff9ecd64cab21f2b034288a476870282fb944fa5
parent0184fbb68c8bf1d8d1a123929dfab0497d5236af (diff)
downloadparry-027139279aa0e83c123ba26139d84505f1d4af90.tar.gz
parry-027139279aa0e83c123ba26139d84505f1d4af90.zip
Use sqlalchemy as backend, implement JWT authentication, add uploading, commenting and voting
-rw-r--r--__main__.py17
-rw-r--r--auth.py82
-rw-r--r--database.py70
-rw-r--r--handlers.py71
-rw-r--r--helpers.py71
-rw-r--r--routes/auth.py10
-rw-r--r--routes/media.py37
-rw-r--r--routes/uploads.py276
8 files changed, 427 insertions, 207 deletions
diff --git a/__main__.py b/__main__.py
index f46e94c..95b78ab 100644
--- a/__main__.py
+++ b/__main__.py
@@ -15,6 +15,8 @@
from .helpers import *
from .handlers import *
from .routes import *
+import time
+sys.tracebacklimit = 1000
@route("/api")
def get_root():
@@ -23,10 +25,17 @@ def get_root():
if __name__ == "__main__":
def loadEntries():
for handler in [CC(), CCAN()]:
- for entry in handler.process():
- database["entries"][entry["id"]] = entry
- saveJSON(database, "database.json")
+ handler.process()
+
+ def checkForUnownedFiles():
+ while True:
+ session = DBSession()
+ for file in session.query(File).filter_by(upload=None):
+ if not file.upload and file.date - datetime.now() > 5 * 60:
+ session.delete(file)
+ _unlink_file(file)
+ time.sleep(600)
threading.Thread(target=loadEntries, daemon=True).start()
+ threading.Thread(target=checkForUnownedFiles, daemon=True).start()
run(host='0.0.0.0', port=9621, debug=True)
- saveJSON(database, "database.json")
diff --git a/auth.py b/auth.py
new file mode 100644
index 0000000..1f45844
--- /dev/null
+++ b/auth.py
@@ -0,0 +1,82 @@
+# Copyright (c) 2018, George Tokmaji
+
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import os, hashlib, base64, jwt, json
+from .database import *
+from bottle import install, HTTPResponse, route, request, default_app, post
+from bottle_jwt import JWTProviderPlugin, jwt_auth_required, JWTProvider
+
+def calculateUserHash(username : str, password : str) -> object:
+ return hashlib.sha512(hashlib.sha512(username.encode("utf-8")).digest() + hashlib.sha512(password.encode("utf-8")).digest())
+
+class AuthBackend(object):
+ def authenticate_user(self, username, password):
+ if username is None or password is None:
+ raise HTTPResponse("Username or password missing", 400)
+ session = DBSession()
+ try:
+ user = session.query(User).filter_by(name=username, hash=calculateUserHash(username, password).hexdigest()).one()
+ except db.orm.exc.NoResultFound:
+ return
+
+ return {"id" : str(user.id), "name" : user.name, "email" : user.email}
+
+ def get_user(self, user_id):
+ session = DBSession()
+ user = session.query(User).get(user_id)
+ if user:
+ return {"id" : str(user.id), "name" : user.name, "email" : user.email}
+
+def custom_create_token(self, user, ttl=None):
+ user_id = json.dumps(user[self.id_field]).encode("utf-8")
+ payload = {
+ "sub" : base64.b64encode(user_id).decode("utf-8"),
+ "name" : user["name"],
+ "email" : user["email"]
+ }
+
+ if self.ttl:
+ payload["exp"] = datetime.utcnow() + datetime.timedelta(seconds=ttl) if ttl else self.expires
+
+ return jwt.encode(payload, self.secret, algorithm=self.algorithm), payload["exp"]
+
+JWTProvider.create_token = custom_create_token
+
+install(JWTProviderPlugin(
+ keyword="jwt",
+ auth_endpoint="/api/auth",
+ backend=AuthBackend(),
+ fields=("username", "password"),
+ secret=os.environ["PARRY_SECRET"],
+ ttl=30 * 60
+ ))
+
+
+def auth_basic(f):
+ def checkAuth(*args, **kwargs):
+ session = DBSession()
+ try:
+ User.query.filter_by(name=request.forms["username"], hash=calculateUserHash(request.forms["username"], request.forms["password"]).hexdigest()).first()
+ except db.orm.exc.NoResultFound:
+ return HTTPResponse(status=401)
+
+ del request.forms["password"]
+ return f(*args, **kwargs)
+ return checkAuth
+
+def get_user(session : DBSession):
+ try:
+ return session.query(User).filter_by(name=request.get_user()["name"]).one()
+ except db.orm.exc.NoResultFound:
+ raise HTTPResponse(status=401)
diff --git a/database.py b/database.py
index f5eea45..89f769f 100644
--- a/database.py
+++ b/database.py
@@ -6,6 +6,8 @@ import sqlalchemy as db
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship, sessionmaker
+BLOCKSIZE = 1024 ** 2
+
class Types(object):
class ObjectId(db.TypeDecorator):
impl = db.UnicodeText
@@ -25,7 +27,7 @@ class Types(object):
return ";".join(i.replace(";", ",") for i in value)
def process_result_value(self, value : str, dialect) -> list:
- return value.split(";")
+ return value.split(";") if value else []
Base = declarative_base()
@@ -35,6 +37,13 @@ class User(Base):
name = db.Column(db.UnicodeText, nullable=False)
email = db.Column(db.UnicodeText)
hash = db.Column(db.String(512), nullable=False)
+
+ def json(self):
+ return {
+ "id" : self.id,
+ "name" : self.name,
+ "email" : self.email
+ }
class Upload(Base):
__tablename__ = "uploads"
@@ -46,10 +55,23 @@ class Upload(Base):
tags = db.Column(Types.List, default=list)
created_at = db.Column(db.DateTime, nullable=False, default=datetime.now)
updated_at = db.Column(db.DateTime, nullable=False, default=datetime.now, onupdate=datetime.now)
+ readonly = db.Column(db.Boolean, nullable=True, default=bool)
_v = db.Column(db.Integer, default=0)
author = relationship("User")
- #dependency = relationship("Dependency")
+
+ def json(self):
+ return {
+ "id" : self.id,
+ "title" : self.title,
+ "author" : self.author.json() if self.author else {"name" : "N/A"},
+ "description" : self.description,
+ "slug" : self.slug,
+ "tags" : self.tags,
+ "createdAt" : self.created_at.isoformat(),
+ "updatedAt" : self.updated_at.isoformat(),
+ "_v" : self._v
+ }
class File(Base):
__tablename__ = "files"
@@ -63,6 +85,22 @@ class File(Base):
download_url = db.Column(db.Text)
upload = relationship("Upload")
+
+ def json(self):
+ return {
+ "id" : self.id,
+ "filename" : self.name,
+ "metadata" : {
+ "hashes" : {
+ "sha1" : self.hash
+ }
+ },
+ "aliases" : None,
+ "content-type" : self.content_type,
+ "length" : self.length,
+ "chunkSize" : BLOCKSIZE,
+ "uploadDate" : self.date.isoformat()
+ }
class Comment(Base):
__tablename__ = "comments"
@@ -75,16 +113,34 @@ class Comment(Base):
author = relationship("User")
upload = relationship("Upload")
+
+ def json(self):
+ return {
+ "id" : self.id,
+ "body" : self.body,
+ "author" : self.author.json(),
+ "upload" : self.upload.id,
+ "createdAt" : self.created_at.isoformat(),
+ "updatedAt" : self.updated_at.isoformat()
+ }
-class Voting(Base):
- __tablename__ = "votings"
+class Vote(Base):
+ __tablename__ = "votes"
id = db.Column(Types.ObjectId, primary_key=True, default=ObjectId)
author_id = db.Column(Types.ObjectId, db.ForeignKey("users.id"))
- target_id = db.Column(Types.ObjectId, db.ForeignKey("uploads.id"))
+ upload_id = db.Column(Types.ObjectId, db.ForeignKey("uploads.id"))
+ impact = db.Column(db.Integer, nullable=False)
author = relationship("User")
- target = relationship("Upload")
- pass
+ upload = relationship("Upload")
+
+ def json(self):
+ return {
+ "id" : self.id,
+ "author" : self.author.json(),
+ "upload" : self.upload.id,
+ "impact" : self.impact
+ }
#class Dependency(Base):
#__tablename__ = "dependencies"
diff --git a/handlers.py b/handlers.py
index bfca09b..433b76a 100644
--- a/handlers.py
+++ b/handlers.py
@@ -24,10 +24,6 @@ class Site(ABC):
def process(self):
return
-class Parry(Site):
- def process(self):
- yield {}
-
class Website(Site):
tag = ""
prefix = ""
@@ -46,49 +42,44 @@ class Website(Site):
id = ObjectId.from_datetime(datetime.strptime(m["updatedAt"], self.date_format))
try:
entry = session.query(Upload).filter_by(id=id).one()
- try:
- entry.author = session.query(User).filter_by(name=m["author"]).one()
- except db.orm.exc.NoResultFound:
- pass
-
+ exists = True
continue
except db.orm.exc.NoResultFound:
- pass
+ entry = Upload(id=id)
+ exists = False
- entry = Upload(
- id=id,
- title=html.unescape(m["title"]),
- tags=[self.tag],
- slug="".join(i for i in html.unescape(m["title"]).lower() if i in string.ascii_letters),
- updated_at=datetime.strptime(m["updatedAt"], self.date_format),
- _v=1
- )
+ entry.title = html.unescape(m["title"])
+ entry.tags = [self.tag]
+ entry.slug = "".join(i for i in html.unescape(m["title"]).lower() if i in string.ascii_letters)
+ entry.updated_at = datetime.strptime(m["updatedAt"], self.date_format)
+ entry._v = 1
try:
entry.author = session.query(User).filter_by(name=m["author"]).one()
except db.orm.exc.NoResultFound:
pass
- downloadURL = self.prefix + m["downloadURL"]
- try:
- r = requests.get(downloadURL, stream=True, allow_redirects=True)
- except requests.exceptions.ConnectionError:
- continue
-
- if not r:
- continue
-
- locale.setlocale(locale.LC_ALL, "C")
- session.add(File(
- hash=calculateHashForResource(r).hexdigest(),
- id=entry.id,
- name=downloadURL.split("/")[-1],
- content_type=r.headers.get("Content-Type", "application/octet-stream"),
- length=int(r.headers["Content-Length"]),
- date=datetime.strptime(r.headers["Date"], "%a, %d %b %Y %H:%M:%S GMT"),
- download_url=downloadURL,
- upload=entry
- ))
+ if not exists:
+ downloadURL = self.prefix + m["downloadURL"]
+ try:
+ r = requests.get(downloadURL, stream=True, allow_redirects=True)
+ except requests.exceptions.ConnectionError:
+ continue
+
+ if not r:
+ continue
+
+ locale.setlocale(locale.LC_ALL, "C")
+ session.add(File(
+ hash=calculateHashForResource(r).hexdigest(),
+ id=entry.id,
+ name=downloadURL.split("/")[-1],
+ content_type=r.headers.get("Content-Type", "application/octet-stream"),
+ length=int(r.headers["Content-Length"]),
+ date=datetime.strptime(r.headers["Date"], "%a, %d %b %Y %H:%M:%S GMT"),
+ download_url=downloadURL,
+ upload=entry
+ ))
locale.setlocale(locale.LC_ALL, "")
@@ -97,7 +88,9 @@ class Website(Site):
if d and "description" in d.groups():
entry.description = html.unescape(d["description"])
- session.add(entry)
+
+ if not exists:
+ session.add(entry)
session.commit()
class CCAN(Website):
diff --git a/helpers.py b/helpers.py
index 66c733b..c748839 100644
--- a/helpers.py
+++ b/helpers.py
@@ -14,33 +14,28 @@
import sys
import os, re, json, math
-import requests, hashlib
-from bottle import route, run, Bottle, request, static_file, response, hook, HTTPResponse, JSONPlugin, install
+import requests
+from bson.objectid import ObjectId
+from bottle import install, run, Bottle, static_file, response, hook, JSONPlugin, get, post, error
import threading
-os.chdir(os.path.dirname(__file__))
-from .database import *
-
-BLOCKSIZE = 1024 ** 2
+class ParryEncoder(json.JSONEncoder):
+ _default = json.JSONEncoder.default
+ def default(self, obj):
+ if isinstance(obj, ObjectId):
+ return str(obj)
+
+ return self._default(obj)
-locks = {
- "io" : threading.Lock()
- }
+install(JSONPlugin(json_dumps=lambda s: json.dumps(s, cls=ParryEncoder)))
-def with_lock(k):
- def wrapper(f):
- def func(*args, **kwargs):
- with locks[k]:
- return f(*args, **kwargs)
-
- return func
- return wrapper
+os.chdir(os.path.dirname(__file__))
+from .auth import *
def calculateHashForResource(resource : requests.Response) -> object:
hashobj = hashlib.sha1()
calculateHashForFile(resource.raw, hashobj)
- assert(resource.raw.tell())
if "content-length" not in resource.headers:
resource.headers["Content-Length"] = resource.raw.tell()
return hashobj
@@ -61,33 +56,21 @@ def calculateHashForFile(file, hashobj : object = None) -> object:
def notAllowed():
raise HTTPResponse(f"Cannot {request.method} {request.path}")
-@hook('after_request')
+@route("<path:path>", method="OPTIONS")
+def options(path):
+ return HTTPResponse(status=204, headers={"Access-Control-Allow-Origin" : "*", "Access-Control-Allow-Methods" : "GET, POST, HEAD, PUT, PATCH, DELETE", "Access-Control-Allow-Headers" : "*"})
+
+@hook("after_request")
def enable_cors():
response.headers["Access-Control-Allow-Origin"] = "*"
+ response.headers["Access-Control-Allow-Methods"] = "POST, GET, PUT, DELETE, PATCH, HEAD"
-# Auth
+@error(500)
+def internal_error(error):
+ if request.json is None:
+ error.exception = None
+ error._status_code = 400
+ error._body = "Invalid JSON specified"
-def calculateUserHash(username : str, password : str) -> object:
- return hashlib.sha512(hashlib.sha512(username.encode("utf-8")).digest() + hashlib.sha512(password.encode("utf-8")).digest())
-
-def auth_basic(f):
- def checkAuth(*args, **kwargs):
- session = DBSession()
- try:
- User.query.filter_by(name=requests.forms["username"], hash=calculateUserHash(request.forms["username"], request.forms["password"]).hexdigest()).first()
- except db.orm.exc.NoResultFound:
- return HTTPResponse(status=401)
-
- del request.forms["password"]
- return f(*args, **kwargs)
- return checkAuth
-
-class ParryEncoder(json.JSONEncoder):
- _default = json.JSONEncoder.default
- def default(self, obj):
- if isinstance(obj, ObjectId):
- return str(obj)
-
- return self._default(obj)
-
-install(JSONPlugin(json_dumps=lambda s: json.dumps(s, cls=ParryEncoder)))
+def request_data():
+ return request.json if request.json else request.forms
diff --git a/routes/auth.py b/routes/auth.py
index a34aaa5..934abc7 100644
--- a/routes/auth.py
+++ b/routes/auth.py
@@ -14,7 +14,7 @@
from ..helpers import *
-@route("/api/auth", method="POST")
+@post("/api/auth/register")
def post_auth_new():
session = DBSession()
try:
@@ -30,4 +30,10 @@ def post_auth_new():
except db.orm.exc.NoResultFound:
session.add(User(name=username, hash=hash))
session.commit()
- return HTTPResponse(status=201)
+ return HTTPResponse(status=303, headers={"Location" : "/api/auth"})
+
+@get("/api/auth")
+@jwt_auth_required
+def get_auth():
+ return request.get_user()
+
diff --git a/routes/media.py b/routes/media.py
index b9828a8..207eeda 100644
--- a/routes/media.py
+++ b/routes/media.py
@@ -13,14 +13,45 @@
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
from ..helpers import *
+import magic
from datetime import datetime
-@route("/api/media")
+def _upload_file(file, entry=None):
+ f = File(id=ObjectId(), name=file.filename, upload=entry)
+ path = os.path.join(os.getcwd(), "media", str(f.id))
+ file.save(path)
+
+ with open(path, "rb") as fobj:
+ f.hash = calculateHashForFile(fobj).hexdigest()
+ f.length = fobj.tell()
+
+ f.content_type = magic.from_file(path, mime=True)
+
+ return f
+
+def _delete_file(file):
+ try:
+ os.unlink(os.path.join(os.getcwd(), "media", str(file.id)))
+ except FileNotFoundError:
+ pass
+ except OSError:
+ print("Failed to unlink", file.id, file=sys.stderr)
+
+@get("/api/media")
def get_media():
notAllowed()
+@post("/api/media")
+@jwt_auth_required
+def post_media():
+ session = DBSession()
+ f = _upload_file(next(request.files.values()))
+ session.add(f)
+ session.commit()
+ print(f.json())
+ return HTTPResponse(f.json(), status=201)
-@route("/api/media/<id>")
+@get("/api/media/<id>")
def get_media_id(id):
session = DBSession()
try:
@@ -37,6 +68,6 @@ def get_media_id(id):
#return requests.request(request.method, file.download_url, allow_redirects=True)
return HTTPResponse(status=302, headers={"Location" : file.download_url})
else:
- return static_file(file.id, os.path.join(os.getcwd(), "media"), file.content_type, file.name)
+ return static_file(str(file.id), os.path.join(os.getcwd(), "media"), file.content_type, download=file.name if request.params.download else False)
raise HTTPResponse(status=404)
diff --git a/routes/uploads.py b/routes/uploads.py
index 5c7d7fc..3777795 100644
--- a/routes/uploads.py
+++ b/routes/uploads.py
@@ -13,68 +13,46 @@
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
from ..helpers import *
-import string, magic
+from .media import *
+from bottle import delete, put
+from itertools import chain
+import string
-def _add_upload(entry : Upload, session : DBSession):
- return {
- "voting" : {
- "sum" : 0,
- "count" : 0,
- "votes" : None
- },
- "id" : entry.id,
- "title" : entry.title,
- "author" : {
- "id" : entry.author.id if entry.author is not None else "0" * 24,
- "username" : entry.author.name if entry.author is not None else "N/A"
- },
- "tags" : entry.tags,
- "files" : [{
- "metadata" : {
- "hashes" : {
- "sha1" : file.hash
- }
- },
- "aliases" : None,
- "deleted" : False,
- "id" : file.id,
- "filename" : file.name,
- "content-type" : file.content_type,
- "length" : file.length,
- "chunkSize" : BLOCKSIZE,
- "uploadData" : file.date.isoformat()
- } for file in session.query(File).filter_by(upload=entry)
- ],
- "dependencies" : [], #TODO
- "deleted" : False,
- "description" : entry.description,
- "pic" : None, #TODO
- "slug" : entry.slug,
- "createdAt" : entry.created_at.isoformat(),
- "updatedAt" : entry.updated_at.isoformat(),
- "__v" : entry._v,
- "comments" : [{
- "voting" : {
- "sum" : 0,
- "count" : 0,
- "votes" : None
- },
- "deleted" : False,
- "id" : comment.id,
- "body" : comment.body,
- "author" : {
- "id" : comment.author.id,
- "username" : comment.author.name
- },
- "upload" : comment.upload.id,
- "createdAt" : comment.created_at.isoformat(),
- "updatedAt" : comment.updated_at.isoformat()
- } for comment in session.query(Comment).filter_by(upload=entry)
- ]
+_vote_dummy = {
+ "voting" : {
+ "sum" : 0,
+ "count" : 0,
+ "votes" : None
}
+ }
+
+def _add_upload(entry : Upload, session : DBSession):
+ e = entry.json()
+
+ #e.update(_vote_dummy)
+
+ e["comments"] = [{**(comment.json()), **_vote_dummy} for comment in session.query(Comment).filter_by(upload=entry)]
+ e["files"] = []
+ e["pictures"] = []
+
+ for file in session.query(File).filter_by(upload=entry):
+ if not file.content_type.startswith("image"):
+ e["files"].append(file.json())
+ else:
+ e["pictures"].append(file.json())
+
+ e["dependencies"] = [] #TODO
+
+ votes = session.query(Vote).filter_by(upload=entry)
+ e["voting"] = {
+ "sum" : sum(i.vote for i in votes),
+ "count" : votes.count()
+ }
+
+ return e
-@route("/api/uploads")
+@get("/api/uploads")
def get_uploads():
ret = {
"pagination" : {
@@ -89,36 +67,32 @@ def get_uploads():
session = DBSession()
for entry in session.query(Upload).order_by(Upload.updated_at.desc()):
ret["uploads"].append(_add_upload(entry, session))
-
ret["pagination"]["total"] = ret["pagination"]["limit"] = len(ret["uploads"])
return ret
-@route("/api/uploads/<id>")
-def get_upload(id):
- session = DBSession()
- entry = session.query(Upload).get(id)
- if entry is not None:
- return _add_upload(entry, session)
- else:
- raise HTTPResponse(status=404)
-
-@route("/api/uploads", method="POST")
-@auth_basic
-def post_upload():
+@post("/api/uploads")
+@put("/api/uploads/<id>")
+@jwt_auth_required
+def post_upload(id=None):
try:
session = DBSession()
- if len(session.query(Upload).filter_by(title=requests.forms.title).all()):
- raise HTTPResponse("An entry with the specified title already exists", 410)
+ if id is not None:
+ entry = session.query(Upload).get(id)
+ if not entry:
+ raise HTTPResponse(status=404)
+ else:
+ if session.query(Upload).filter_by(title=request_data()["title"]).count():
+ raise HTTPResponse("An entry with the specified title already exists", 410)
+
+ entry = Upload()
- entry = Upload(
- title=request.forms.title,
- author=session.query(User).filter_by(username=request.forms.username),
- description=request.forms.description,
- slug="".join(i for i in requests.forms.title.lower() if i in string.ascii_letters),
- tags=request.forms.tags.split(";") if "tags" in request.forms else []
- )
+ entry.title = request_data()["title"]
+ entry.description=request_data()["description"]
+ entry.slug = "".join(i for i in request_data()["title"].lower() if i in string.ascii_letters)
+ entry.tags = request_data().get("tags", [])
+ entry.author = get_user(session)
session.add(entry)
@@ -127,58 +101,144 @@ def post_upload():
except FileExistsError:
pass
- for file in request.files.values():
- f = File(
- name=file.filename,
- upload=entry
- )
-
- path = os.path.join(os.getcwd(), "media", f["id"])
- file.save(path)
-
- with open(path, "rb") as fobj:
- f.hash = calculateHashForFile(fobj).hexdigest()
- f.length = fobj.tell()
-
- f.content_type = magic.from_file(path, mime=True)
- session.add(f)
-
+ file_ids = []
+ if "files" in request_data():
+ for f in request_data()["files"]:
+ try:
+ f = f["id"]
+ except TypeError:
+ pass
+ if ObjectId.is_valid(f):
+ file_ids.append(ObjectId(f))
+ file = session.query(File).get(id)
+ file.upload = entry
+ session.add(file)
+
+ else:
+ for file in request.files.values():
+ f = _upload_file(file, entry)
+ session.add(f)
+ file_ids.append(f.id)
+
+ for file in session.query(File).filter(File.upload_id == entry.id, ~File.id.in_(file_ids)): # we change the upload above, so we can't use Filter.upload
+ session.delete(file)
+
except KeyError as e:
session.rollback()
raise HTTPResponse(f"Missing form value: {e.args[0]}", 400)
session.commit()
- return HTTPResponse(status=201)
+ return HTTPResponse(status=(201 if id is not None else 204))
-@route("/api/uploads/<id>/comments", method="POST")
-@auth_basic
-def post_comments(id):
+
+@get("/api/uploads/<id>")
+def get_upload(id):
session = DBSession()
+ entry = session.query(Upload).get(id)
+ if entry is not None:
+ return _add_upload(entry, session)
+
+ raise HTTPResponse(status=404)
+
+@delete("/api/uploads/<id>")
+@jwt_auth_required
+def delete_upload(id):
+ session = DBSession()
+ author = get_user(session)
try:
- session.query(Upload).filter_by(id=id).one()
+ entry = session.query(Upload).filter_by(id=id, author=author).one()
except db.orm.exc.NoResultFound:
+ raise HTTPResponse(status=404)
+
+ if entry.readonly:
+ raise HTTPResponse("Resource is read-only", 403)
+
+ session.delete(entry)
+ for i in [Comment, Voting]:
+ for e in session.query(i).filter_by(upload=entry):
+ session.delete(e)
+
+ for f in session.query(File).filter_by(upload=entry):
+ session.delete(e)
+ _delete_file(f)
+
+ #TODO: Dependencies
+ session.commit()
+ return HTTPResponse(status=204)
+
+
+@get("/api/uploads/<id>/comments")
+def get_comments(id):
+ session = DBSession()
+ upload = session.query(Upload).get(id)
+ if upload is None:
+ raise HTTPResponse("Invalid upload id", 404)
+
+ return {
+ "comments" : [{**(comment.json()), **_vote_dummy} for comment in session.query(Comment).filter_by(upload=upload)]
+ }
+
+@post("/api/uploads/<id>/comments")
+@jwt_auth_required
+def post_comments(id):
+ session = DBSession()
+ upload = session.query(Upload).get(id)
+ if upload is None:
raise HTTPResponse("Invalid upload id", 404)
try:
session.add(Comment(
- body=request.forms.body,
- author=session.query(User).filter_by(username=request.forms.username).one()
+ body=request_data()["body"],
+ author=get_user(session),
+ upload=upload
))
except KeyError as e:
- raise HTTPResponse(f"Missing form value: {e.args[0]}", 400)
+ raise HTTPResponse(f"Missing json value: {e.args[0]}", 400)
session.commit()
return HTTPResponse(status=201)
-@route("/api/uploads/<id>/comments/<comment_id>", method="DELETE")
-@auth_basic
+@delete("/api/uploads/<id>/comments/<comment_id>")
+@jwt_auth_required
def delete_comments(id, comment_id):
session = DBSession()
try:
- comment = session.query(Comment).filter_by(id=comment_id, author=session.query(User).filter_by(username).one(), upload=session.query(Upload).filter_by(id=id).one()).one()
+ comment = session.query(Comment).filter_by(id=comment_id, author=get_user(session), upload=session.query(Upload).get(id)).one()
except db.orm.exc.NoResultFound:
- raise HTTPResponse("Requested comment not found", 404)
+ raise HTTPResponse(status=404)
session.delete(comment)
session.commit()
return HTTPResponse(status=204)
+
+@get("/api/uploads/<id>/vote")
+@jwt_auth_required
+def get_vote(id):
+ session = DBSession()
+ upload = session.query(Upload).get(id)
+ if upload is None:
+ raise HTTPResponse("Invalid upload id", 404)
+
+ try:
+ return session.query(Vote).filter_by(upload=upload, author=get_user(session)).one().json()
+ except db.orm.exc.NoResultFound:
+ raise HTTPResponse(status=404)
+
+@post("/api/uploads/<id>/vote")
+@jwt_auth_required
+def post_vote(id):
+ session = DBSession()
+ upload = session.query(Upload).get(id)
+ if upload is None:
+ raise HTTPResponse("Invalid upload id", 404)
+
+ author = get_user(session)
+ try:
+ vote = session.query(Vote).filter_by(upload=upload, author=author).one()
+ except db.orm.exc.NoResultFound:
+ vote = Vote(author=author, upload=upload)
+
+ vote.impact = request_data()["impact"]
+ session.add(vote)
+ session.commit()
+ return HTTPResponse(status=204)