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 = True if flask.request.form.get("showmail") else False 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 username == flask.session.get("username"): 326flask.abort(403) 327if oldRelationship: 328db.session.delete(oldRelationship) 329else: 330relationship = UserFollow( 331flask.session.get("username"), 332username 333) 334db.session.add(relationship) 335db.session.commit() 336 337user = db.session.get(User, username) 338author = db.session.get(User, flask.session.get("username")) 339notification = Notification({"type": "update", "version": "0.0.0"}) 340db.session.add(notification) 341db.session.commit() 342 343result = celeryTasks.sendNotification.delay(notification.id, [username], 1) 344flask.flash(f"Sending notification in task {result.id}", "success") 345 346db.session.commit() 347return flask.redirect("?", code=303) 348 349 350@app.route("/<username>/<repository>/") 351def repositoryIndex(username, repository): 352return flask.redirect("./tree", code=302) 353 354 355@app.route("/info/<username>/avatar") 356def userAvatar(username): 357serverUserdataLocation = os.path.join(config.USERDATA_PATH, username) 358 359if not os.path.exists(serverUserdataLocation): 360return flask.render_template("not-found.html"), 404 361 362return flask.send_from_directory(serverUserdataLocation, "avatar.png") 363 364 365@app.route("/<username>/<repository>/raw/<branch>/<path:subpath>") 366def repositoryRaw(username, repository, branch, subpath): 367if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 368repository) is not None): 369flask.abort(403) 370 371serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) 372 373app.logger.info(f"Loading {serverRepoLocation}") 374 375if not os.path.exists(serverRepoLocation): 376app.logger.error(f"Cannot load {serverRepoLocation}") 377return flask.render_template("not-found.html"), 404 378 379repo = git.Repo(serverRepoLocation) 380repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 381if not repoData.defaultBranch: 382if repo.heads: 383repoData.defaultBranch = repo.heads[0].name 384else: 385return flask.render_template("empty.html", 386remote=f"http://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200 387if not branch: 388branch = repoData.defaultBranch 389return flask.redirect(f"./{branch}", code=302) 390 391if branch.startswith("tag:"): 392ref = f"tags/{branch[4:]}" 393elif branch.startswith("~"): 394ref = branch[1:] 395else: 396ref = f"heads/{branch}" 397 398ref = ref.replace("~", "/") # encode slashes for URL support 399 400try: 401repo.git.checkout("-f", ref) 402except git.exc.GitCommandError: 403return flask.render_template("not-found.html"), 404 404 405return flask.send_from_directory(config.REPOS_PATH, os.path.join(username, repository, subpath)) 406 407 408@repositories.route("/<username>/<repository>/tree/", defaults={"branch": None, "subpath": ""}) 409@repositories.route("/<username>/<repository>/tree/<branch>/", defaults={"subpath": ""}) 410@repositories.route("/<username>/<repository>/tree/<branch>/<path:subpath>") 411def repositoryTree(username, repository, branch, subpath): 412if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 413repository) is not None): 414flask.abort(403) 415 416serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) 417 418app.logger.info(f"Loading {serverRepoLocation}") 419 420if not os.path.exists(serverRepoLocation): 421app.logger.error(f"Cannot load {serverRepoLocation}") 422return flask.render_template("not-found.html"), 404 423 424repo = git.Repo(serverRepoLocation) 425repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 426if not repoData.defaultBranch: 427if repo.heads: 428repoData.defaultBranch = repo.heads[0].name 429else: 430return flask.render_template("empty.html", 431remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200 432if not branch: 433branch = repoData.defaultBranch 434return flask.redirect(f"./{branch}", code=302) 435 436if branch.startswith("tag:"): 437ref = f"tags/{branch[4:]}" 438elif branch.startswith("~"): 439ref = branch[1:] 440else: 441ref = f"heads/{branch}" 442 443ref = ref.replace("~", "/") # encode slashes for URL support 444 445try: 446repo.git.checkout("-f", ref) 447except git.exc.GitCommandError: 448return flask.render_template("not-found.html"), 404 449 450branches = repo.heads 451 452allRefs = [] 453for ref in repo.heads: 454allRefs.append((ref, "head")) 455for ref in repo.tags: 456allRefs.append((ref, "tag")) 457 458if os.path.isdir(os.path.join(serverRepoLocation, subpath)): 459files = [] 460blobs = [] 461 462for entry in os.listdir(os.path.join(serverRepoLocation, subpath)): 463if not os.path.basename(entry) == ".git": 464files.append(os.path.join(subpath, entry)) 465 466infos = [] 467 468for file in files: 469path = os.path.join(serverRepoLocation, file) 470mimetype = guessMIME(path) 471 472text = gitCommand(serverRepoLocation, None, "log", "--format='%H\n'", file).decode() 473 474sha = text.split("\n")[0] 475identifier = f"/{username}/{repository}/{sha}" 476lastCommit = Commit.query.filter_by(identifier=identifier).first() 477 478info = { 479"name": os.path.basename(file), 480"serverPath": path, 481"relativePath": file, 482"link": os.path.join(f"/{username}/{repository}/tree/{branch}/", file), 483"size": humanSize(os.path.getsize(path)), 484"mimetype": f"{mimetype}{f' ({mimetypes.guess_type(path)[1]})' if mimetypes.guess_type(path)[1] else ''}", 485"commit": lastCommit, 486"shaSize": 7, 487} 488 489specialIcon = config.matchIcon(os.path.basename(file)) 490if specialIcon: 491info["icon"] = specialIcon 492elif os.path.isdir(path): 493info["icon"] = config.folderIcon 494elif mimetypes.guess_type(path)[0] in config.fileIcons: 495info["icon"] = config.fileIcons[mimetypes.guess_type(path)[0]] 496else: 497info["icon"] = config.unknownIcon 498 499if os.path.isdir(path): 500infos.insert(0, info) 501else: 502infos.append(info) 503 504return flask.render_template( 505"repo-tree.html", 506username=username, 507repository=repository, 508files=infos, 509subpath=os.path.join("/", subpath), 510branches=allRefs, 511current=branch, 512remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 513isFavourite=getFavourite(flask.session.get("username"), username, repository) 514) 515else: 516path = os.path.join(serverRepoLocation, subpath) 517 518if not os.path.exists(path): 519return flask.render_template("not-found.html"), 404 520 521mimetype = guessMIME(path) 522mode = mimetype.split("/", 1)[0] 523size = humanSize(os.path.getsize(path)) 524 525specialIcon = config.matchIcon(os.path.basename(path)) 526if specialIcon: 527icon = specialIcon 528elif os.path.isdir(path): 529icon = config.folderIcon 530elif mimetypes.guess_type(path)[0] in config.fileIcons: 531icon = config.fileIcons[mimetypes.guess_type(path)[0]] 532else: 533icon = config.unknownIcon 534 535contents = None 536if mode == "text": 537contents = convertToHTML(path) 538 539return flask.render_template( 540"repo-file.html", 541username=username, 542repository=repository, 543file=os.path.join(f"/{username}/{repository}/raw/{branch}/", subpath), 544branches=allRefs, 545current=branch, 546mode=mode, 547mimetype=mimetype, 548detailedtype=magic.from_file(path), 549size=size, 550icon=icon, 551subpath=os.path.join("/", subpath), 552basename=os.path.basename(path), 553contents=contents, 554remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 555isFavourite=getFavourite(flask.session.get("username"), username, repository) 556) 557 558 559@repositories.route("/<username>/<repository>/forum/") 560def repositoryForum(username, repository): 561if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 562repository) is not None): 563flask.abort(403) 564 565serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) 566 567app.logger.info(f"Loading {serverRepoLocation}") 568 569if not os.path.exists(serverRepoLocation): 570app.logger.error(f"Cannot load {serverRepoLocation}") 571return flask.render_template("not-found.html"), 404 572 573repo = git.Repo(serverRepoLocation) 574repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 575user = User.query.filter_by(username=flask.session.get("username")).first() 576relationships = RepoAccess.query.filter_by(repo=repoData) 577userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first() 578 579return flask.render_template( 580"repo-forum.html", 581username=username, 582repository=repository, 583repoData=repoData, 584relationships=relationships, 585repo=repo, 586userRelationship=userRelationship, 587Post=Post, 588remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 589isFavourite=getFavourite(flask.session.get("username"), username, repository) 590) 591 592 593@repositories.route("/<username>/<repository>/forum/new", methods=["POST"]) 594def repositoryForumAdd(username, repository): 595if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 596repository) is not None): 597flask.abort(403) 598 599serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) 600 601app.logger.info(f"Loading {serverRepoLocation}") 602 603if not os.path.exists(serverRepoLocation): 604app.logger.error(f"Cannot load {serverRepoLocation}") 605return flask.render_template("not-found.html"), 404 606 607repo = git.Repo(serverRepoLocation) 608repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 609user = User.query.filter_by(username=flask.session.get("username")).first() 610relationships = RepoAccess.query.filter_by(repo=repoData) 611userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first() 612 613post = Post(user, repoData, None, flask.request.form["subject"], flask.request.form["message"]) 614 615db.session.add(post) 616db.session.commit() 617 618return flask.redirect(flask.url_for(".repositoryForumThread", username=username, repository=repository, postID=post.number), code=303) 619 620 621@repositories.route("/<username>/<repository>/forum/<int:postID>") 622def repositoryForumThread(username, repository, postID): 623if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 624repository) is not None): 625flask.abort(403) 626 627serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) 628 629app.logger.info(f"Loading {serverRepoLocation}") 630 631if not os.path.exists(serverRepoLocation): 632app.logger.error(f"Cannot load {serverRepoLocation}") 633return flask.render_template("not-found.html"), 404 634 635repo = git.Repo(serverRepoLocation) 636repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 637user = User.query.filter_by(username=flask.session.get("username")).first() 638relationships = RepoAccess.query.filter_by(repo=repoData) 639userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first() 640 641return flask.render_template( 642"repo-forum-thread.html", 643username=username, 644repository=repository, 645repoData=repoData, 646relationships=relationships, 647repo=repo, 648userRelationship=userRelationship, 649Post=Post, 650postID=postID, 651maxPostNesting=4, 652remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 653isFavourite=getFavourite(flask.session.get("username"), username, repository) 654) 655 656 657@repositories.route("/<username>/<repository>/forum/<int:postID>/reply", methods=["POST"]) 658def repositoryForumReply(username, repository, postID): 659if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 660repository) is not None): 661flask.abort(403) 662 663serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) 664 665app.logger.info(f"Loading {serverRepoLocation}") 666 667if not os.path.exists(serverRepoLocation): 668app.logger.error(f"Cannot load {serverRepoLocation}") 669return flask.render_template("not-found.html"), 404 670 671repo = git.Repo(serverRepoLocation) 672repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 673user = User.query.filter_by(username=flask.session.get("username")).first() 674relationships = RepoAccess.query.filter_by(repo=repoData) 675userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first() 676if not user: 677flask.abort(401) 678 679parent = Post.query.filter_by(identifier=f"/{username}/{repository}/{postID}").first() 680post = Post(user, repoData, parent, flask.request.form["subject"], flask.request.form["message"]) 681 682db.session.add(post) 683post.updateDate() 684db.session.commit() 685 686return flask.redirect(flask.url_for(".repositoryForumThread", username=username, repository=repository, postID=postID), code=303) 687 688 689@app.route("/<username>/<repository>/forum/<int:postID>/voteup", defaults={"score": 1}) 690@app.route("/<username>/<repository>/forum/<int:postID>/votedown", defaults={"score": -1}) 691@app.route("/<username>/<repository>/forum/<int:postID>/votes", defaults={"score": 0}) 692def repositoryForumVote(username, repository, postID, score): 693if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 694repository) is not None): 695flask.abort(403) 696 697serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) 698 699app.logger.info(f"Loading {serverRepoLocation}") 700 701if not os.path.exists(serverRepoLocation): 702app.logger.error(f"Cannot load {serverRepoLocation}") 703return flask.render_template("not-found.html"), 404 704 705repo = git.Repo(serverRepoLocation) 706repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 707user = User.query.filter_by(username=flask.session.get("username")).first() 708relationships = RepoAccess.query.filter_by(repo=repoData) 709userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first() 710if not user: 711flask.abort(401) 712 713post = Post.query.filter_by(identifier=f"/{username}/{repository}/{postID}").first() 714 715if score: 716oldRelationship = PostVote.query.filter_by(userUsername=user.username, postIdentifier=post.identifier).first() 717if oldRelationship: 718if score == oldRelationship.voteScore: 719db.session.delete(oldRelationship) 720post.voteSum -= oldRelationship.voteScore 721else: 722post.voteSum -= oldRelationship.voteScore 723post.voteSum += score 724oldRelationship.voteScore = score 725else: 726relationship = PostVote(user, post, score) 727post.voteSum += score 728db.session.add(relationship) 729 730db.session.commit() 731 732userVote = PostVote.query.filter_by(userUsername=user.username, postIdentifier=post.identifier).first() 733response = flask.make_response(str(post.voteSum) + " " + str(userVote.voteScore if userVote else 0)) 734response.content_type = "text/plain" 735 736return response 737 738 739@app.route("/<username>/<repository>/favourite") 740def repositoryFavourite(username, repository): 741if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 742repository) is not None): 743flask.abort(403) 744 745serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) 746 747app.logger.info(f"Loading {serverRepoLocation}") 748 749if not os.path.exists(serverRepoLocation): 750app.logger.error(f"Cannot load {serverRepoLocation}") 751return flask.render_template("not-found.html"), 404 752 753repo = git.Repo(serverRepoLocation) 754repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 755user = User.query.filter_by(username=flask.session.get("username")).first() 756relationships = RepoAccess.query.filter_by(repo=repoData) 757userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first() 758if not user: 759flask.abort(401) 760 761oldRelationship = RepoFavourite.query.filter_by(userUsername=user.username, repoRoute=repoData.route).first() 762if oldRelationship: 763db.session.delete(oldRelationship) 764else: 765relationship = RepoFavourite(user, repoData) 766db.session.add(relationship) 767 768db.session.commit() 769 770return flask.redirect(flask.url_for("favourites"), code=303) 771 772 773@repositories.route("/<username>/<repository>/users/", methods=["GET", "POST"]) 774def repositoryUsers(username, repository): 775if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 776repository) is not None): 777flask.abort(403) 778 779serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) 780 781app.logger.info(f"Loading {serverRepoLocation}") 782 783if not os.path.exists(serverRepoLocation): 784app.logger.error(f"Cannot load {serverRepoLocation}") 785return flask.render_template("not-found.html"), 404 786 787repo = git.Repo(serverRepoLocation) 788repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 789user = User.query.filter_by(username=flask.session.get("username")).first() 790relationships = RepoAccess.query.filter_by(repo=repoData) 791userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first() 792 793if flask.request.method == "GET": 794return flask.render_template( 795"repo-users.html", 796username=username, 797repository=repository, 798repoData=repoData, 799relationships=relationships, 800repo=repo, 801userRelationship=userRelationship, 802remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 803isFavourite=getFavourite(flask.session.get("username"), username, repository) 804) 805else: 806if getPermissionLevel(flask.session.get("username"), username, repository) != 2: 807flask.abort(401) 808 809if flask.request.form.get("new-username"): 810# Create new relationship 811newUser = User.query.filter_by(username=flask.request.form.get("new-username")).first() 812relationship = RepoAccess(newUser, repoData, flask.request.form.get("new-level")) 813db.session.add(relationship) 814db.session.commit() 815if flask.request.form.get("update-username"): 816# Create new relationship 817updatedUser = User.query.filter_by(username=flask.request.form.get("update-username")).first() 818relationship = RepoAccess.query.filter_by(repo=repoData, user=updatedUser).first() 819if flask.request.form.get("update-level") == -1: 820relationship.delete() 821else: 822relationship.accessLevel = flask.request.form.get("update-level") 823db.session.commit() 824 825return flask.redirect(app.url_for(".repositoryUsers", username=username, repository=repository)) 826 827 828@repositories.route("/<username>/<repository>/branches/") 829def repositoryBranches(username, repository): 830if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 831repository) is not None): 832flask.abort(403) 833 834serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) 835 836app.logger.info(f"Loading {serverRepoLocation}") 837 838if not os.path.exists(serverRepoLocation): 839app.logger.error(f"Cannot load {serverRepoLocation}") 840return flask.render_template("not-found.html"), 404 841 842repo = git.Repo(serverRepoLocation) 843repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 844 845return flask.render_template( 846"repo-branches.html", 847username=username, 848repository=repository, 849repoData=repoData, 850repo=repo, 851remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 852isFavourite=getFavourite(flask.session.get("username"), username, repository) 853) 854 855 856@repositories.route("/<username>/<repository>/log/", defaults={"branch": None}) 857@repositories.route("/<username>/<repository>/log/<branch>/") 858def repositoryLog(username, repository, branch): 859if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 860repository) is not None): 861flask.abort(403) 862 863serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) 864 865app.logger.info(f"Loading {serverRepoLocation}") 866 867if not os.path.exists(serverRepoLocation): 868app.logger.error(f"Cannot load {serverRepoLocation}") 869return flask.render_template("not-found.html"), 404 870 871repo = git.Repo(serverRepoLocation) 872repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 873if not repoData.defaultBranch: 874if repo.heads: 875repoData.defaultBranch = repo.heads[0].name 876else: 877return flask.render_template("empty.html", 878remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200 879if not branch: 880branch = repoData.defaultBranch 881return flask.redirect(f"./{branch}", code=302) 882 883if branch.startswith("tag:"): 884ref = f"tags/{branch[4:]}" 885elif branch.startswith("~"): 886ref = branch[1:] 887else: 888ref = f"heads/{branch}" 889 890ref = ref.replace("~", "/") # encode slashes for URL support 891 892try: 893repo.git.checkout("-f", ref) 894except git.exc.GitCommandError: 895return flask.render_template("not-found.html"), 404 896 897branches = repo.heads 898 899allRefs = [] 900for ref in repo.heads: 901allRefs.append((ref, "head")) 902for ref in repo.tags: 903allRefs.append((ref, "tag")) 904 905commitList = [f"/{username}/{repository}/{sha}" for sha in gitCommand(serverRepoLocation, None, "log", "--format='%H'").decode().split("\n")] 906 907commits = Commit.query.filter(Commit.identifier.in_(commitList)) 908 909return flask.render_template( 910"repo-log.html", 911username=username, 912repository=repository, 913branches=allRefs, 914current=branch, 915repoData=repoData, 916repo=repo, 917commits=commits, 918remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 919isFavourite=getFavourite(flask.session.get("username"), username, repository) 920) 921 922 923@repositories.route("/<username>/<repository>/settings/") 924def repositorySettings(username, repository): 925if getPermissionLevel(flask.session.get("username"), username, repository) != 2: 926flask.abort(401) 927 928return flask.render_template("repo-settings.html", username=username, repository=repository) 929 930 931@app.errorhandler(404) 932def e404(error): 933return flask.render_template("not-found.html"), 404 934 935 936@app.errorhandler(401) 937def e401(error): 938return flask.render_template("unauthorised.html"), 401 939 940 941@app.errorhandler(403) 942def e403(error): 943return flask.render_template("forbidden.html"), 403 944 945 946@app.errorhandler(418) 947def e418(error): 948return flask.render_template("teapot.html"), 418 949 950 951@app.errorhandler(405) 952def e405(error): 953return flask.render_template("method-not-allowed.html"), 405 954 955 956if __name__ == "__main__": 957app.run(debug=True, port=8080, host="0.0.0.0") 958 959 960app.register_blueprint(repositories) 961