diff options
| -rw-r--r-- | __main__.py | 17 | ||||
| -rw-r--r-- | auth.py | 82 | ||||
| -rw-r--r-- | database.py | 70 | ||||
| -rw-r--r-- | handlers.py | 71 | ||||
| -rw-r--r-- | helpers.py | 71 | ||||
| -rw-r--r-- | routes/auth.py | 10 | ||||
| -rw-r--r-- | routes/media.py | 37 | ||||
| -rw-r--r-- | routes/uploads.py | 276 |
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") @@ -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): @@ -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) |
