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