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