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