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