app.py
Python script, ASCII text executable
1import os 2import random 3import subprocess 4from functools import wraps 5 6import cairosvg 7import flask 8from flask_sqlalchemy import SQLAlchemy 9import git 10import mimetypes 11import magic 12from flask_bcrypt import Bcrypt 13from markupsafe import escape, Markup 14from flask_migrate import Migrate 15from datetime import datetime 16from enum import Enum 17import shutil 18from PIL import Image 19from cairosvg import svg2png 20import platform 21 22import config 23 24app = flask.Flask(__name__) 25 26from flask_httpauth import HTTPBasicAuth 27 28auth = HTTPBasicAuth() 29 30app.config["SQLALCHEMY_DATABASE_URI"] = config.DB_URI 31app.config["SECRET_KEY"] = config.DB_PASSWORD 32app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False 33db = SQLAlchemy(app) 34bcrypt = Bcrypt(app) 35migrate = Migrate(app, db) 36from models import * 37 38 39def gitCommand(repo, data, *args): 40if not os.path.isdir(repo): 41raise FileNotFoundError("Repo not found") 42env = os.environ.copy() 43 44command = ["git", *args] 45 46proc = subprocess.Popen(" ".join(command), cwd=repo, env=env, shell=True, stdout=subprocess.PIPE, 47stdin=subprocess.PIPE) 48print(command) 49 50if data: 51proc.stdin.write(data) 52 53out, err = proc.communicate() 54return out 55 56 57def onlyChars(string, chars): 58for i in string: 59if i not in chars: 60return False 61return True 62 63 64def getPermissionLevel(loggedIn, username, repository): 65user = User.query.filter_by(username=loggedIn).first() 66repo = Repo.query.filter_by(route=f"/{username}/{repository}").first() 67 68if user and repo: 69permission = RepoAccess.query.filter_by(user=user, repo=repo).first() 70if permission: 71return permission.accessLevel 72 73return None 74 75 76def getVisibility(username, repository): 77repo = Repo.query.filter_by(route=f"/{username}/{repository}").first() 78 79if repo: 80return repo.visibility 81 82return None 83 84 85def getFavourite(loggedIn, username, repository): 86print(loggedIn, username, repository) 87relationship = RepoFavourite.query.filter_by(userUsername=loggedIn, repoRoute=f"/{username}/{repository}").first() 88return relationship 89 90 91import gitHTTP 92import jinjaUtils 93 94 95def humanSize(value, decimals=2, scale=1024, 96units=("B", "kiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB", "RiB", "QiB")): 97for unit in units: 98if value < scale: 99break 100value /= scale 101if int(value) == value: 102# do not return decimals, if the value is already round 103return int(value), unit 104return round(value * 10 ** decimals) / 10 ** decimals, unit 105 106 107def guessMIME(path): 108if os.path.isdir(path): 109mimetype = "inode/directory" 110elif magic.from_file(path, mime=True): 111mimetype = magic.from_file(path, mime=True) 112else: 113mimetype = "application/octet-stream" 114return mimetype 115 116 117def convertToHTML(path): 118with open(path, "r") as f: 119contents = f.read() 120return contents 121 122 123repositories = flask.Blueprint("repository", __name__, template_folder="templates/repository/") 124 125 126@app.context_processor 127def default(): 128username = flask.session.get("username") 129 130userObject = User.query.filter_by(username=username).first() 131 132return { 133"loggedInUser": username, 134"userObject": userObject, 135"Notification": Notification, 136"unread": UserNotification.query.filter_by(userUsername=username).filter(UserNotification.attentionLevel > 0).count() 137} 138 139 140@app.route("/") 141def main(): 142return flask.render_template("home.html") 143 144 145@app.route("/about/") 146def about(): 147return flask.render_template("about.html", platform=platform) 148 149 150@app.route("/settings/", methods=["GET", "POST"]) 151def settings(): 152if not flask.session.get("username"): 153flask.abort(401) 154if flask.request.method == "GET": 155user = User.query.filter_by(username=flask.session.get("username")).first() 156 157return flask.render_template("user-settings.html", user=user) 158else: 159user = User.query.filter_by(username=flask.session.get("username")).first() 160 161user.displayName = flask.request.form["displayname"] 162user.URL = flask.request.form["url"] 163user.company = flask.request.form["company"] 164user.companyURL = flask.request.form["companyurl"] 165user.location = flask.request.form["location"] 166user.showMail = flask.request.form.get("showmail", user.showMail) 167 168db.session.commit() 169 170flask.flash(Markup("<iconify-icon icon='mdi:check'></iconify-icon>Settings saved"), category="success") 171return flask.redirect(f"/{flask.session.get('username')}", code=303) 172 173 174@app.route("/favourites/", methods=["GET", "POST"]) 175def favourites(): 176if not flask.session.get("username"): 177flask.abort(401) 178if flask.request.method == "GET": 179relationships = RepoFavourite.query.filter_by(userUsername=flask.session.get("username")) 180 181return flask.render_template("favourites.html", favourites=relationships) 182 183 184@app.route("/notifications/", methods=["GET", "POST"]) 185def notifications(): 186if not flask.session.get("username"): 187flask.abort(401) 188if flask.request.method == "GET": 189return flask.render_template("notifications.html", notifications=UserNotification.query.filter_by(userUsername=flask.session.get("username"))) 190 191 192@app.route("/accounts/", methods=["GET", "POST"]) 193def login(): 194if flask.request.method == "GET": 195return flask.render_template("login.html") 196else: 197if "login" in flask.request.form: 198username = flask.request.form["username"] 199password = flask.request.form["password"] 200 201user = User.query.filter_by(username=username).first() 202 203if user and bcrypt.check_password_hash(user.passwordHashed, password): 204flask.session["username"] = user.username 205flask.flash( 206Markup(f"<iconify-icon icon='mdi:account'></iconify-icon>Successfully logged in as {username}"), 207category="success") 208return flask.redirect("/", code=303) 209elif not user: 210flask.flash(Markup("<iconify-icon icon='mdi:account-question'></iconify-icon>User not found"), 211category="alert") 212return flask.render_template("login.html") 213else: 214flask.flash(Markup("<iconify-icon icon='mdi:account-question'></iconify-icon>Invalid password"), 215category="error") 216return flask.render_template("login.html") 217if "signup" in flask.request.form: 218username = flask.request.form["username"] 219password = flask.request.form["password"] 220password2 = flask.request.form["password2"] 221email = flask.request.form.get("email") 222email2 = flask.request.form.get("email2") # repeat email is a honeypot 223name = flask.request.form.get("name") 224 225if not onlyChars(username, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_"): 226flask.flash(Markup( 227"<iconify-icon icon='mdi:account-error'></iconify-icon>Usernames may only contain Latin alphabet, numbers, '-' and '_'"), 228category="error") 229return flask.render_template("login.html") 230 231if username in config.RESERVED_NAMES: 232flask.flash( 233Markup(f"<iconify-icon icon='mdi:account-error'></iconify-icon>Sorry, {username} is a system path"), 234category="error") 235return flask.render_template("login.html") 236 237userCheck = User.query.filter_by(username=username).first() 238if userCheck: 239flask.flash( 240Markup(f"<iconify-icon icon='mdi:account-error'></iconify-icon>The username {username} is taken"), 241category="error") 242return flask.render_template("login.html") 243 244if password2 != password: 245flask.flash(Markup("<iconify-icon icon='mdi:key-alert'></iconify-icon>Make sure the passwords match"), 246category="error") 247return flask.render_template("login.html") 248 249user = User(username, password, email, name) 250db.session.add(user) 251db.session.commit() 252flask.session["username"] = user.username 253flask.flash(Markup( 254f"<iconify-icon icon='mdi:account'></iconify-icon>Successfully created and logged in as {username}"), 255category="success") 256return flask.redirect("/", code=303) 257 258 259@app.route("/newrepo/", methods=["GET", "POST"]) 260def newRepo(): 261if not flask.session.get("username"): 262flask.abort(401) 263if flask.request.method == "GET": 264return flask.render_template("new-repo.html") 265else: 266name = flask.request.form["name"] 267visibility = int(flask.request.form["visibility"]) 268 269if not onlyChars(name, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_"): 270flask.flash(Markup( 271"<iconify-icon icon='mdi:error'></iconify-icon>Repository names may only contain Latin alphabet, numbers, '-' and '_'"), 272category="error") 273return flask.render_template("new-repo.html") 274 275user = User.query.filter_by(username=flask.session.get("username")).first() 276 277repo = Repo(user, name, visibility) 278db.session.add(repo) 279db.session.commit() 280 281if not os.path.exists(os.path.join(config.REPOS_PATH, repo.route)): 282subprocess.run(["git", "init", repo.name], 283cwd=os.path.join(config.REPOS_PATH, flask.session.get("username"))) 284 285flask.flash(Markup(f"<iconify-icon icon='mdi:folder'></iconify-icon>Successfully created repository {name}"), 286category="success") 287return flask.redirect(repo.route, code=303) 288 289 290@app.route("/logout") 291def logout(): 292flask.session.clear() 293flask.flash(Markup(f"<iconify-icon icon='mdi:account'></iconify-icon>Successfully logged out"), category="info") 294return flask.redirect("/", code=303) 295 296 297@app.route("/<username>/", methods=["GET", "POST"]) 298def userProfile(username): 299oldRelationship = UserFollow.query.filter_by(followerUsername=flask.session.get("username"), followedUsername=username).first() 300if flask.request.method == "GET": 301user = User.query.filter_by(username=username).first() 302match flask.request.args.get("action"): 303case "repositories": 304repos = Repo.query.filter_by(ownerName=username, visibility=2) 305return flask.render_template("user-profile-repositories.html", user=user, repos=repos, relationship=oldRelationship) 306case "followers": 307return flask.render_template("user-profile-followers.html", user=user, relationship=oldRelationship) 308case "follows": 309return flask.render_template("user-profile-follows.html", user=user, relationship=oldRelationship) 310case _: 311return flask.render_template("user-profile-overview.html", user=user, relationship=oldRelationship) 312 313elif flask.request.method == "POST": 314match flask.request.args.get("action"): 315case "follow": 316if oldRelationship: 317db.session.delete(oldRelationship) 318else: 319relationship = UserFollow( 320flask.session.get("username"), 321username 322) 323db.session.add(relationship) 324 325db.session.commit() 326return flask.redirect("?", code=303) 327 328 329@app.route("/<username>/<repository>/") 330def repositoryIndex(username, repository): 331return flask.redirect("./tree", code=302) 332 333 334@app.route("/info/<username>/avatar") 335def userAvatar(username): 336serverUserdataLocation = os.path.join(config.USERDATA_PATH, username) 337 338if not os.path.exists(serverUserdataLocation): 339return flask.render_template("not-found.html"), 404 340 341return flask.send_from_directory(serverUserdataLocation, "avatar.png") 342 343 344@app.route("/<username>/<repository>/raw/<branch>/<path:subpath>") 345def repositoryRaw(username, repository, branch, subpath): 346if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 347repository) is not None): 348flask.abort(403) 349 350serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) 351 352app.logger.info(f"Loading {serverRepoLocation}") 353 354if not os.path.exists(serverRepoLocation): 355app.logger.error(f"Cannot load {serverRepoLocation}") 356return flask.render_template("not-found.html"), 404 357 358repo = git.Repo(serverRepoLocation) 359repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 360if not repoData.defaultBranch: 361if repo.heads: 362repoData.defaultBranch = repo.heads[0].name 363else: 364return flask.render_template("empty.html", 365remote=f"http://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200 366if not branch: 367branch = repoData.defaultBranch 368return flask.redirect(f"./{branch}", code=302) 369 370if branch.startswith("tag:"): 371ref = f"tags/{branch[4:]}" 372elif branch.startswith("~"): 373ref = branch[1:] 374else: 375ref = f"heads/{branch}" 376 377ref = ref.replace("~", "/") # encode slashes for URL support 378 379try: 380repo.git.checkout("-f", ref) 381except git.exc.GitCommandError: 382return flask.render_template("not-found.html"), 404 383 384return flask.send_from_directory(config.REPOS_PATH, os.path.join(username, repository, subpath)) 385 386 387@repositories.route("/<username>/<repository>/tree/", defaults={"branch": None, "subpath": ""}) 388@repositories.route("/<username>/<repository>/tree/<branch>/", defaults={"subpath": ""}) 389@repositories.route("/<username>/<repository>/tree/<branch>/<path:subpath>") 390def repositoryTree(username, repository, branch, subpath): 391if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 392repository) is not None): 393flask.abort(403) 394 395serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) 396 397app.logger.info(f"Loading {serverRepoLocation}") 398 399if not os.path.exists(serverRepoLocation): 400app.logger.error(f"Cannot load {serverRepoLocation}") 401return flask.render_template("not-found.html"), 404 402 403repo = git.Repo(serverRepoLocation) 404repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 405if not repoData.defaultBranch: 406if repo.heads: 407repoData.defaultBranch = repo.heads[0].name 408else: 409return flask.render_template("empty.html", 410remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200 411if not branch: 412branch = repoData.defaultBranch 413return flask.redirect(f"./{branch}", code=302) 414 415if branch.startswith("tag:"): 416ref = f"tags/{branch[4:]}" 417elif branch.startswith("~"): 418ref = branch[1:] 419else: 420ref = f"heads/{branch}" 421 422ref = ref.replace("~", "/") # encode slashes for URL support 423 424try: 425repo.git.checkout("-f", ref) 426except git.exc.GitCommandError: 427return flask.render_template("not-found.html"), 404 428 429branches = repo.heads 430 431allRefs = [] 432for ref in repo.heads: 433allRefs.append((ref, "head")) 434for ref in repo.tags: 435allRefs.append((ref, "tag")) 436 437if os.path.isdir(os.path.join(serverRepoLocation, subpath)): 438files = [] 439blobs = [] 440 441for entry in os.listdir(os.path.join(serverRepoLocation, subpath)): 442if not os.path.basename(entry) == ".git": 443files.append(os.path.join(subpath, entry)) 444 445infos = [] 446 447for file in files: 448path = os.path.join(serverRepoLocation, file) 449mimetype = guessMIME(path) 450 451text = gitCommand(serverRepoLocation, None, "log", "--format='%H\n'", file).decode() 452 453sha = text.split("\n")[0] 454identifier = f"/{username}/{repository}/{sha}" 455lastCommit = Commit.query.filter_by(identifier=identifier).first() 456 457info = { 458"name": os.path.basename(file), 459"serverPath": path, 460"relativePath": file, 461"link": os.path.join(f"/{username}/{repository}/tree/{branch}/", file), 462"size": humanSize(os.path.getsize(path)), 463"mimetype": f"{mimetype}{f' ({mimetypes.guess_type(path)[1]})' if mimetypes.guess_type(path)[1] else ''}", 464"commit": lastCommit, 465"shaSize": 7, 466} 467 468specialIcon = config.matchIcon(os.path.basename(file)) 469if specialIcon: 470info["icon"] = specialIcon 471elif os.path.isdir(path): 472info["icon"] = config.folderIcon 473elif mimetypes.guess_type(path)[0] in config.fileIcons: 474info["icon"] = config.fileIcons[mimetypes.guess_type(path)[0]] 475else: 476info["icon"] = config.unknownIcon 477 478if os.path.isdir(path): 479infos.insert(0, info) 480else: 481infos.append(info) 482 483return flask.render_template( 484"repo-tree.html", 485username=username, 486repository=repository, 487files=infos, 488subpath=os.path.join("/", subpath), 489branches=allRefs, 490current=branch, 491remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 492isFavourite=getFavourite(flask.session.get("username"), username, repository) 493) 494else: 495path = os.path.join(serverRepoLocation, subpath) 496 497if not os.path.exists(path): 498return flask.render_template("not-found.html"), 404 499 500mimetype = guessMIME(path) 501mode = mimetype.split("/", 1)[0] 502size = humanSize(os.path.getsize(path)) 503 504specialIcon = config.matchIcon(os.path.basename(path)) 505if specialIcon: 506icon = specialIcon 507elif os.path.isdir(path): 508icon = config.folderIcon 509elif mimetypes.guess_type(path)[0] in config.fileIcons: 510icon = config.fileIcons[mimetypes.guess_type(path)[0]] 511else: 512icon = config.unknownIcon 513 514contents = None 515if mode == "text": 516contents = convertToHTML(path) 517 518return flask.render_template( 519"repo-file.html", 520username=username, 521repository=repository, 522file=os.path.join(f"/{username}/{repository}/raw/{branch}/", subpath), 523branches=allRefs, 524current=branch, 525mode=mode, 526mimetype=mimetype, 527detailedtype=magic.from_file(path), 528size=size, 529icon=icon, 530subpath=os.path.join("/", subpath), 531basename=os.path.basename(path), 532contents=contents, 533remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 534isFavourite=getFavourite(flask.session.get("username"), username, repository) 535) 536 537 538@repositories.route("/<username>/<repository>/forum/") 539def repositoryForum(username, repository): 540if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 541repository) is not None): 542flask.abort(403) 543 544serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) 545 546app.logger.info(f"Loading {serverRepoLocation}") 547 548if not os.path.exists(serverRepoLocation): 549app.logger.error(f"Cannot load {serverRepoLocation}") 550return flask.render_template("not-found.html"), 404 551 552repo = git.Repo(serverRepoLocation) 553repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 554user = User.query.filter_by(username=flask.session.get("username")).first() 555relationships = RepoAccess.query.filter_by(repo=repoData) 556userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first() 557 558return flask.render_template( 559"repo-forum.html", 560username=username, 561repository=repository, 562repoData=repoData, 563relationships=relationships, 564repo=repo, 565userRelationship=userRelationship, 566Post=Post, 567remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 568isFavourite=getFavourite(flask.session.get("username"), username, repository) 569) 570 571 572@repositories.route("/<username>/<repository>/forum/new", methods=["POST"]) 573def repositoryForumAdd(username, repository): 574if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 575repository) is not None): 576flask.abort(403) 577 578serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) 579 580app.logger.info(f"Loading {serverRepoLocation}") 581 582if not os.path.exists(serverRepoLocation): 583app.logger.error(f"Cannot load {serverRepoLocation}") 584return flask.render_template("not-found.html"), 404 585 586repo = git.Repo(serverRepoLocation) 587repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 588user = User.query.filter_by(username=flask.session.get("username")).first() 589relationships = RepoAccess.query.filter_by(repo=repoData) 590userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first() 591 592post = Post(user, repoData, None, flask.request.form["subject"], flask.request.form["message"]) 593 594db.session.add(post) 595db.session.commit() 596 597return flask.redirect(flask.url_for(".repositoryForumThread", username=username, repository=repository, postID=post.number), code=303) 598 599 600@repositories.route("/<username>/<repository>/forum/<int:postID>") 601def repositoryForumThread(username, repository, postID): 602if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 603repository) is not None): 604flask.abort(403) 605 606serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) 607 608app.logger.info(f"Loading {serverRepoLocation}") 609 610if not os.path.exists(serverRepoLocation): 611app.logger.error(f"Cannot load {serverRepoLocation}") 612return flask.render_template("not-found.html"), 404 613 614repo = git.Repo(serverRepoLocation) 615repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 616user = User.query.filter_by(username=flask.session.get("username")).first() 617relationships = RepoAccess.query.filter_by(repo=repoData) 618userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first() 619 620return flask.render_template( 621"repo-forum-thread.html", 622username=username, 623repository=repository, 624repoData=repoData, 625relationships=relationships, 626repo=repo, 627userRelationship=userRelationship, 628Post=Post, 629postID=postID, 630maxPostNesting=4, 631remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 632isFavourite=getFavourite(flask.session.get("username"), username, repository) 633) 634 635 636@repositories.route("/<username>/<repository>/forum/<int:postID>/reply", methods=["POST"]) 637def repositoryForumReply(username, repository, postID): 638if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 639repository) is not None): 640flask.abort(403) 641 642serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) 643 644app.logger.info(f"Loading {serverRepoLocation}") 645 646if not os.path.exists(serverRepoLocation): 647app.logger.error(f"Cannot load {serverRepoLocation}") 648return flask.render_template("not-found.html"), 404 649 650repo = git.Repo(serverRepoLocation) 651repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 652user = User.query.filter_by(username=flask.session.get("username")).first() 653relationships = RepoAccess.query.filter_by(repo=repoData) 654userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first() 655if not user: 656flask.abort(401) 657 658parent = Post.query.filter_by(identifier=f"/{username}/{repository}/{postID}").first() 659post = Post(user, repoData, parent, flask.request.form["subject"], flask.request.form["message"]) 660 661db.session.add(post) 662post.updateDate() 663db.session.commit() 664 665return flask.redirect(flask.url_for(".repositoryForumThread", username=username, repository=repository, postID=postID), code=303) 666 667 668@app.route("/<username>/<repository>/forum/<int:postID>/voteup", defaults={"score": 1}) 669@app.route("/<username>/<repository>/forum/<int:postID>/votedown", defaults={"score": -1}) 670@app.route("/<username>/<repository>/forum/<int:postID>/votes", defaults={"score": 0}) 671def repositoryForumVote(username, repository, postID, score): 672if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 673repository) is not None): 674flask.abort(403) 675 676serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) 677 678app.logger.info(f"Loading {serverRepoLocation}") 679 680if not os.path.exists(serverRepoLocation): 681app.logger.error(f"Cannot load {serverRepoLocation}") 682return flask.render_template("not-found.html"), 404 683 684repo = git.Repo(serverRepoLocation) 685repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 686user = User.query.filter_by(username=flask.session.get("username")).first() 687relationships = RepoAccess.query.filter_by(repo=repoData) 688userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first() 689if not user: 690flask.abort(401) 691 692post = Post.query.filter_by(identifier=f"/{username}/{repository}/{postID}").first() 693 694if score: 695oldRelationship = PostVote.query.filter_by(userUsername=user.username, postIdentifier=post.identifier).first() 696if oldRelationship: 697if score == oldRelationship.voteScore: 698db.session.delete(oldRelationship) 699post.voteSum -= oldRelationship.voteScore 700else: 701post.voteSum -= oldRelationship.voteScore 702post.voteSum += score 703oldRelationship.voteScore = score 704else: 705relationship = PostVote(user, post, score) 706post.voteSum += score 707db.session.add(relationship) 708 709db.session.commit() 710 711userVote = PostVote.query.filter_by(userUsername=user.username, postIdentifier=post.identifier).first() 712response = flask.make_response(str(post.voteSum) + " " + str(userVote.voteScore if userVote else 0)) 713response.content_type = "text/plain" 714 715return response 716 717 718@app.route("/<username>/<repository>/favourite") 719def repositoryFavourite(username, repository): 720if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 721repository) is not None): 722flask.abort(403) 723 724serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) 725 726app.logger.info(f"Loading {serverRepoLocation}") 727 728if not os.path.exists(serverRepoLocation): 729app.logger.error(f"Cannot load {serverRepoLocation}") 730return flask.render_template("not-found.html"), 404 731 732repo = git.Repo(serverRepoLocation) 733repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 734user = User.query.filter_by(username=flask.session.get("username")).first() 735relationships = RepoAccess.query.filter_by(repo=repoData) 736userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first() 737if not user: 738flask.abort(401) 739 740oldRelationship = RepoFavourite.query.filter_by(userUsername=user.username, repoRoute=repoData.route).first() 741if oldRelationship: 742db.session.delete(oldRelationship) 743else: 744relationship = RepoFavourite(user, repoData) 745db.session.add(relationship) 746 747db.session.commit() 748 749return flask.redirect(flask.url_for("favourites"), code=303) 750 751 752@repositories.route("/<username>/<repository>/users/", methods=["GET", "POST"]) 753def repositoryUsers(username, repository): 754if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 755repository) is not None): 756flask.abort(403) 757 758serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) 759 760app.logger.info(f"Loading {serverRepoLocation}") 761 762if not os.path.exists(serverRepoLocation): 763app.logger.error(f"Cannot load {serverRepoLocation}") 764return flask.render_template("not-found.html"), 404 765 766repo = git.Repo(serverRepoLocation) 767repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 768user = User.query.filter_by(username=flask.session.get("username")).first() 769relationships = RepoAccess.query.filter_by(repo=repoData) 770userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first() 771 772if flask.request.method == "GET": 773return flask.render_template( 774"repo-users.html", 775username=username, 776repository=repository, 777repoData=repoData, 778relationships=relationships, 779repo=repo, 780userRelationship=userRelationship, 781remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 782isFavourite=getFavourite(flask.session.get("username"), username, repository) 783) 784else: 785if getPermissionLevel(flask.session.get("username"), username, repository) != 2: 786flask.abort(401) 787 788if flask.request.form.get("new-username"): 789# Create new relationship 790newUser = User.query.filter_by(username=flask.request.form.get("new-username")).first() 791relationship = RepoAccess(newUser, repoData, flask.request.form.get("new-level")) 792db.session.add(relationship) 793db.session.commit() 794if flask.request.form.get("update-username"): 795# Create new relationship 796updatedUser = User.query.filter_by(username=flask.request.form.get("update-username")).first() 797relationship = RepoAccess.query.filter_by(repo=repoData, user=updatedUser).first() 798if flask.request.form.get("update-level") == -1: 799relationship.delete() 800else: 801relationship.accessLevel = flask.request.form.get("update-level") 802db.session.commit() 803 804return flask.redirect(app.url_for(".repositoryUsers", username=username, repository=repository)) 805 806 807@repositories.route("/<username>/<repository>/branches/") 808def repositoryBranches(username, repository): 809if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 810repository) is not None): 811flask.abort(403) 812 813serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) 814 815app.logger.info(f"Loading {serverRepoLocation}") 816 817if not os.path.exists(serverRepoLocation): 818app.logger.error(f"Cannot load {serverRepoLocation}") 819return flask.render_template("not-found.html"), 404 820 821repo = git.Repo(serverRepoLocation) 822repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 823 824return flask.render_template( 825"repo-branches.html", 826username=username, 827repository=repository, 828repoData=repoData, 829repo=repo, 830remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 831isFavourite=getFavourite(flask.session.get("username"), username, repository) 832) 833 834 835@repositories.route("/<username>/<repository>/log/", defaults={"branch": None}) 836@repositories.route("/<username>/<repository>/log/<branch>/") 837def repositoryLog(username, repository, branch): 838if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 839repository) is not None): 840flask.abort(403) 841 842serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) 843 844app.logger.info(f"Loading {serverRepoLocation}") 845 846if not os.path.exists(serverRepoLocation): 847app.logger.error(f"Cannot load {serverRepoLocation}") 848return flask.render_template("not-found.html"), 404 849 850repo = git.Repo(serverRepoLocation) 851repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 852if not repoData.defaultBranch: 853if repo.heads: 854repoData.defaultBranch = repo.heads[0].name 855else: 856return flask.render_template("empty.html", 857remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200 858if not branch: 859branch = repoData.defaultBranch 860return flask.redirect(f"./{branch}", code=302) 861 862if branch.startswith("tag:"): 863ref = f"tags/{branch[4:]}" 864elif branch.startswith("~"): 865ref = branch[1:] 866else: 867ref = f"heads/{branch}" 868 869ref = ref.replace("~", "/") # encode slashes for URL support 870 871try: 872repo.git.checkout("-f", ref) 873except git.exc.GitCommandError: 874return flask.render_template("not-found.html"), 404 875 876branches = repo.heads 877 878allRefs = [] 879for ref in repo.heads: 880allRefs.append((ref, "head")) 881for ref in repo.tags: 882allRefs.append((ref, "tag")) 883 884commitList = [f"/{username}/{repository}/{sha}" for sha in gitCommand(serverRepoLocation, None, "log", "--format='%H'").decode().split("\n")] 885 886commits = Commit.query.filter(Commit.identifier.in_(commitList)) 887 888return flask.render_template( 889"repo-log.html", 890username=username, 891repository=repository, 892branches=allRefs, 893current=branch, 894repoData=repoData, 895repo=repo, 896commits=commits, 897remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 898isFavourite=getFavourite(flask.session.get("username"), username, repository) 899) 900 901 902@repositories.route("/<username>/<repository>/settings/") 903def repositorySettings(username, repository): 904if getPermissionLevel(flask.session.get("username"), username, repository) != 2: 905flask.abort(401) 906 907return flask.render_template("repo-settings.html", username=username, repository=repository) 908 909 910@app.errorhandler(404) 911def e404(error): 912return flask.render_template("not-found.html"), 404 913 914 915@app.errorhandler(401) 916def e401(error): 917return flask.render_template("unauthorised.html"), 401 918 919 920@app.errorhandler(403) 921def e403(error): 922return flask.render_template("forbidden.html"), 403 923 924 925@app.errorhandler(418) 926def e418(error): 927return flask.render_template("teapot.html"), 418 928 929 930@app.errorhandler(405) 931def e405(error): 932return flask.render_template("method-not-allowed.html"), 405 933 934 935if __name__ == "__main__": 936app.run(debug=True, port=8080, host="0.0.0.0") 937 938 939app.register_blueprint(repositories) 940