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