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