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