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