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) 36 37 38def gitCommand(repo, data, *args): 39if not os.path.isdir(repo): 40raise FileNotFoundError("Repo not found") 41env = os.environ.copy() 42 43command = ["git", *args] 44 45proc = subprocess.Popen(" ".join(command), cwd=repo, env=env, shell=True, stdout=subprocess.PIPE, 46stdin=subprocess.PIPE) 47print(command) 48 49if data: 50proc.stdin.write(data) 51 52out, err = proc.communicate() 53return out 54 55 56def onlyChars(string, chars): 57for i in string: 58if i not in chars: 59return False 60return True 61 62 63with app.app_context(): 64class RepoAccess(db.Model): 65id = db.Column(db.Integer, primary_key=True) 66userUsername = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 67repoRoute = db.Column(db.String(98), db.ForeignKey("repo.route"), nullable=False) 68accessLevel = db.Column(db.SmallInteger(), nullable=False) # 0 read-only, 1 read-write, 2 admin 69 70user = db.relationship("User", back_populates="repoAccess") 71repo = db.relationship("Repo", back_populates="repoAccess") 72 73__table_args__ = (db.UniqueConstraint("userUsername", "repoRoute", name="_user_repo_uc"),) 74 75def __init__(self, user, repo, level): 76self.userUsername = user.username 77self.repoRoute = repo.route 78self.accessLevel = level 79 80 81class User(db.Model): 82username = db.Column(db.String(32), unique=True, nullable=False, primary_key=True) 83displayName = db.Column(db.Unicode(128), unique=False, nullable=True) 84bio = db.Column(db.Unicode(512), unique=False, nullable=True) 85passwordHashed = db.Column(db.String(60), nullable=False) 86email = db.Column(db.String(254), nullable=True) 87company = db.Column(db.Unicode(64), nullable=True) 88companyURL = db.Column(db.String(256), nullable=True) 89URL = db.Column(db.String(256), nullable=True) 90showMail = db.Column(db.Boolean, default=False, nullable=False) 91location = db.Column(db.Unicode(64), nullable=True) 92creationDate = db.Column(db.DateTime, default=datetime.utcnow) 93 94repositories = db.relationship("Repo", back_populates="owner") 95repoAccess = db.relationship("RepoAccess", back_populates="user") 96 97commits = db.relationship("Commit", back_populates="owner") 98posts = db.relationship("Post", back_populates="owner") 99 100def __init__(self, username, password, email=None, displayName=None): 101self.username = username 102self.passwordHashed = bcrypt.generate_password_hash(password, config.HASHING_ROUNDS).decode("utf-8") 103self.email = email 104self.displayName = displayName 105 106# Create the user's directory 107if not os.path.exists(os.path.join(config.REPOS_PATH, username)): 108os.makedirs(os.path.join(config.REPOS_PATH, username)) 109if not os.path.exists(os.path.join(config.USERDATA_PATH, username)): 110os.makedirs(os.path.join(config.USERDATA_PATH, username)) 111 112avatarName = random.choice(os.listdir(config.DEFAULT_AVATARS_PATH)) 113if os.path.join(config.DEFAULT_AVATARS_PATH, avatarName).endswith(".svg"): 114cairosvg.svg2png(url=os.path.join(config.DEFAULT_AVATARS_PATH, avatarName), 115write_to="/tmp/roundabout-avatar.png") 116avatar = Image.open("/tmp/roundabout-avatar.png") 117else: 118avatar = Image.open(os.path.join(config.DEFAULT_AVATARS_PATH, avatarName)) 119avatar.thumbnail(config.AVATAR_SIZE) 120avatar.save(os.path.join(config.USERDATA_PATH, username, "avatar.png")) 121 122 123class Repo(db.Model): 124route = db.Column(db.String(98), unique=True, nullable=False, primary_key=True) 125ownerName = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 126name = db.Column(db.String(64), nullable=False) 127owner = db.relationship("User", back_populates="repositories") 128visibility = db.Column(db.SmallInteger(), nullable=False) 129info = db.Column(db.Unicode(512), nullable=True) 130URL = db.Column(db.String(256), nullable=True) 131creationDate = db.Column(db.DateTime, default=datetime.utcnow) 132 133defaultBranch = db.Column(db.String(64), nullable=True, default="") 134 135commits = db.relationship("Commit", back_populates="repo") 136posts = db.relationship("Post", back_populates="repo") 137repoAccess = db.relationship("RepoAccess", back_populates="repo") 138 139lastPostID = db.Column(db.Integer, nullable=False, default=0) 140 141def __init__(self, owner, name, visibility): 142self.route = f"/{owner.username}/{name}" 143self.name = name 144self.ownerName = owner.username 145self.owner = owner 146self.visibility = visibility 147 148# Add the owner as an admin 149repoAccess = RepoAccess(owner, self, 2) 150db.session.add(repoAccess) 151 152 153class Commit(db.Model): 154identifier = db.Column(db.String(227), unique=True, nullable=False, primary_key=True) 155sha = db.Column(db.String(128), nullable=False) 156repoName = db.Column(db.String(98), db.ForeignKey("repo.route"), nullable=False) 157ownerName = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 158ownerIdentity = db.Column(db.String(321)) 159receiveDate = db.Column(db.DateTime, default=datetime.now) 160authorDate = db.Column(db.DateTime) 161message = db.Column(db.UnicodeText) 162repo = db.relationship("Repo", back_populates="commits") 163owner = db.relationship("User", back_populates="commits") 164 165def __init__(self, sha, owner, repo, date, message, ownerIdentity): 166self.identifier = f"/{owner.username}/{repo.name}/{sha}" 167self.sha = sha 168self.repoName = repo.route 169self.repo = repo 170self.ownerName = owner.username 171self.owner = owner 172self.authorDate = datetime.fromtimestamp(int(date)) 173self.message = message 174self.ownerIdentity = ownerIdentity 175 176 177class Post(db.Model): 178identifier = db.Column(db.String(109), unique=True, nullable=False, primary_key=True) 179number = db.Column(db.String(109), nullable=False) 180repoName = db.Column(db.String(98), db.ForeignKey("repo.route"), nullable=False) 181ownerName = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 182 183parentID = db.Column(db.String(109), db.ForeignKey("post.identifier"), nullable=True) 184state = db.Column(db.SmallInteger, nullable=True, default=1) 185 186date = db.Column(db.DateTime, default=datetime.now) 187subject = db.Column(db.Unicode(384)) 188message = db.Column(db.UnicodeText) 189repo = db.relationship("Repo", back_populates="posts") 190owner = db.relationship("User", back_populates="posts") 191parent = db.relationship("Post", back_populates="children", remote_side="Post.identifier") 192children = db.relationship("Post", back_populates="parent", remote_side="Post.parentID") 193 194def __init__(self, owner, repo, parent, subject, message): 195self.identifier = f"/{owner.username}/{repo.name}/{repo.lastPostID}" 196self.number = repo.lastPostID 197self.repoName = repo.route 198self.repo = repo 199self.ownerName = owner.username 200self.owner = owner 201self.subject = subject 202self.message = message 203self.parent = parent 204repo.lastPostID += 1 205 206 207def getPermissionLevel(loggedIn, username, repository): 208user = User.query.filter_by(username=loggedIn).first() 209repo = Repo.query.filter_by(route=f"/{username}/{repository}").first() 210 211if user and repo: 212permission = RepoAccess.query.filter_by(user=user, repo=repo).first() 213if permission: 214return permission.accessLevel 215 216return None 217 218 219def getVisibility(username, repository): 220repo = Repo.query.filter_by(route=f"/{username}/{repository}").first() 221 222if repo: 223return repo.visibility 224 225return None 226 227 228import gitHTTP 229import jinjaUtils 230 231 232def humanSize(value, decimals=2, scale=1024, 233units=("B", "kiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB", "RiB", "QiB")): 234for unit in units: 235if value < scale: 236break 237value /= scale 238if int(value) == value: 239# do not return decimals, if the value is already round 240return int(value), unit 241return round(value * 10 ** decimals) / 10 ** decimals, unit 242 243 244def guessMIME(path): 245if os.path.isdir(path): 246mimetype = "inode/directory" 247elif magic.from_file(path, mime=True): 248mimetype = magic.from_file(path, mime=True) 249else: 250mimetype = "application/octet-stream" 251return mimetype 252 253 254def convertToHTML(path): 255with open(path, "r") as f: 256contents = f.read() 257return contents 258 259 260@app.context_processor 261def default(): 262username = flask.session.get("username") 263 264return {"loggedInUser": username} 265 266 267@app.route("/") 268def main(): 269return flask.render_template("home.html") 270 271 272@app.route("/about/") 273def about(): 274return flask.render_template("about.html", platform=platform) 275 276 277@app.route("/settings/", methods=["GET", "POST"]) 278def settings(): 279if flask.request.method == "GET": 280if not flask.session.get("username"): 281flask.abort(401) 282user = User.query.filter_by(username=flask.session.get("username")).first() 283 284return flask.render_template("user-settings.html", user=user) 285else: 286user = User.query.filter_by(username=flask.session.get("username")).first() 287 288user.displayName = flask.request.form["displayname"] 289user.URL = flask.request.form["url"] 290user.company = flask.request.form["company"] 291user.companyURL = flask.request.form["companyurl"] 292user.location = flask.request.form["location"] 293user.showMail = flask.request.form.get("showmail", user.showMail) 294 295db.session.commit() 296 297flask.flash(Markup("<iconify-icon icon='mdi:check'></iconify-icon>Settings saved"), category="success") 298return flask.redirect(f"/{flask.session.get('username')}", code=303) 299 300 301@app.route("/accounts/", methods=["GET", "POST"]) 302def login(): 303if flask.request.method == "GET": 304return flask.render_template("login.html") 305else: 306if "login" in flask.request.form: 307username = flask.request.form["username"] 308password = flask.request.form["password"] 309 310user = User.query.filter_by(username=username).first() 311 312if user and bcrypt.check_password_hash(user.passwordHashed, password): 313flask.session["username"] = user.username 314flask.flash( 315Markup(f"<iconify-icon icon='mdi:account'></iconify-icon>Successfully logged in as {username}"), 316category="success") 317return flask.redirect("/", code=303) 318elif not user: 319flask.flash(Markup("<iconify-icon icon='mdi:account-question'></iconify-icon>User not found"), 320category="alert") 321return flask.render_template("login.html") 322else: 323flask.flash(Markup("<iconify-icon icon='mdi:account-question'></iconify-icon>Invalid password"), 324category="error") 325return flask.render_template("login.html") 326if "signup" in flask.request.form: 327username = flask.request.form["username"] 328password = flask.request.form["password"] 329password2 = flask.request.form["password2"] 330email = flask.request.form.get("email") 331email2 = flask.request.form.get("email2") # repeat email is a honeypot 332name = flask.request.form.get("name") 333 334if not onlyChars(username, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_"): 335flask.flash(Markup( 336"<iconify-icon icon='mdi:account-error'></iconify-icon>Usernames may only contain Latin alphabet, numbers, '-' and '_'"), 337category="error") 338return flask.render_template("login.html") 339 340if username in config.RESERVED_NAMES: 341flask.flash( 342Markup(f"<iconify-icon icon='mdi:account-error'></iconify-icon>Sorry, {username} is a system path"), 343category="error") 344return flask.render_template("login.html") 345 346userCheck = User.query.filter_by(username=username).first() 347if userCheck: 348flask.flash( 349Markup(f"<iconify-icon icon='mdi:account-error'></iconify-icon>The username {username} is taken"), 350category="error") 351return flask.render_template("login.html") 352 353if password2 != password: 354flask.flash(Markup("<iconify-icon icon='mdi:key-alert'></iconify-icon>Make sure the passwords match"), 355category="error") 356return flask.render_template("login.html") 357 358user = User(username, password, email, name) 359db.session.add(user) 360db.session.commit() 361flask.session["username"] = user.username 362flask.flash(Markup( 363f"<iconify-icon icon='mdi:account'></iconify-icon>Successfully created and logged in as {username}"), 364category="success") 365return flask.redirect("/", code=303) 366 367 368@app.route("/newrepo/", methods=["GET", "POST"]) 369def newRepo(): 370if flask.request.method == "GET": 371return flask.render_template("new-repo.html") 372else: 373name = flask.request.form["name"] 374visibility = int(flask.request.form["visibility"]) 375 376if not onlyChars(name, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_"): 377flask.flash(Markup( 378"<iconify-icon icon='mdi:error'></iconify-icon>Repository names may only contain Latin alphabet, numbers, '-' and '_'"), 379category="error") 380return flask.render_template("new-repo.html") 381 382user = User.query.filter_by(username=flask.session.get("username")).first() 383 384repo = Repo(user, name, visibility) 385db.session.add(repo) 386db.session.commit() 387 388if not os.path.exists(os.path.join(config.REPOS_PATH, repo.route)): 389subprocess.run(["git", "init", repo.name], 390cwd=os.path.join(config.REPOS_PATH, flask.session.get("username"))) 391 392flask.flash(Markup(f"<iconify-icon icon='mdi:folder'></iconify-icon>Successfully created repository {name}"), 393category="success") 394return flask.redirect(repo.route, code=303) 395 396 397@app.route("/logout") 398def logout(): 399flask.session.clear() 400flask.flash(Markup(f"<iconify-icon icon='mdi:account'></iconify-icon>Successfully logged out"), category="info") 401return flask.redirect("/", code=303) 402 403 404@app.route("/<username>/") 405def userProfile(username): 406user = User.query.filter_by(username=username).first() 407repos = Repo.query.filter_by(ownerName=username, visibility=2) 408return flask.render_template("user-profile.html", user=user, repos=repos) 409 410 411@app.route("/<username>/<repository>/") 412def repositoryIndex(username, repository): 413return flask.redirect("./tree", code=302) 414 415 416@app.route("/<username>/<repository>/raw/<branch>/<path:subpath>") 417def repositoryRaw(username, repository, branch, subpath): 418if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("user"), username, 419repository) is not None): 420flask.abort(403) 421 422serverRepoLocation = os.path.join(config.REPOS_PATH, os.path.join(username, repository)) 423 424app.logger.info(f"Loading {serverRepoLocation}") 425 426if not os.path.exists(serverRepoLocation): 427app.logger.error(f"Cannot load {serverRepoLocation}") 428return flask.render_template("not-found.html"), 404 429 430repo = git.Repo(serverRepoLocation) 431repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 432if not repoData.defaultBranch: 433if repo.heads: 434repoData.defaultBranch = repo.heads[0].name 435else: 436return flask.render_template("empty.html", 437remote=f"http://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200 438if not branch: 439branch = repoData.defaultBranch 440return flask.redirect(f"./{branch}", code=302) 441 442if branch.startswith("tag:"): 443ref = f"tags/{branch[4:]}" 444elif branch.startswith("~"): 445ref = branch[1:] 446else: 447ref = f"heads/{branch}" 448 449ref = ref.replace("~", "/") # encode slashes for URL support 450 451try: 452repo.git.checkout("-f", ref) 453except git.exc.GitCommandError: 454return flask.render_template("not-found.html"), 404 455 456return flask.send_from_directory(config.REPOS_PATH, os.path.join(username, repository, subpath)) 457 458 459@app.route("/info/<username>/avatar") 460def userAvatar(username): 461serverUserdataLocation = os.path.join(config.USERDATA_PATH, username) 462 463if not os.path.exists(serverUserdataLocation): 464return flask.render_template("not-found.html"), 404 465 466return flask.send_from_directory(serverUserdataLocation, "avatar.png") 467 468 469@app.route("/<username>/<repository>/tree/", defaults={"branch": None, "subpath": ""}) 470@app.route("/<username>/<repository>/tree/<branch>/", defaults={"subpath": ""}) 471@app.route("/<username>/<repository>/tree/<branch>/<path:subpath>") 472def repositoryTree(username, repository, branch, subpath): 473if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 474repository) is not None): 475flask.abort(403) 476 477serverRepoLocation = os.path.join(config.REPOS_PATH, os.path.join(username, repository)) 478 479app.logger.info(f"Loading {serverRepoLocation}") 480 481if not os.path.exists(serverRepoLocation): 482app.logger.error(f"Cannot load {serverRepoLocation}") 483return flask.render_template("not-found.html"), 404 484 485repo = git.Repo(serverRepoLocation) 486repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 487if not repoData.defaultBranch: 488if repo.heads: 489repoData.defaultBranch = repo.heads[0].name 490else: 491return flask.render_template("empty.html", 492remote=f"http://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200 493if not branch: 494branch = repoData.defaultBranch 495return flask.redirect(f"./{branch}", code=302) 496 497if branch.startswith("tag:"): 498ref = f"tags/{branch[4:]}" 499elif branch.startswith("~"): 500ref = branch[1:] 501else: 502ref = f"heads/{branch}" 503 504ref = ref.replace("~", "/") # encode slashes for URL support 505 506try: 507repo.git.checkout("-f", ref) 508except git.exc.GitCommandError: 509return flask.render_template("not-found.html"), 404 510 511branches = repo.heads 512 513allRefs = [] 514for ref in repo.heads: 515allRefs.append((ref, "head")) 516for ref in repo.tags: 517allRefs.append((ref, "tag")) 518 519if os.path.isdir(os.path.join(serverRepoLocation, subpath)): 520files = [] 521blobs = [] 522 523for entry in os.listdir(os.path.join(serverRepoLocation, subpath)): 524if not os.path.basename(entry) == ".git": 525files.append(os.path.join(subpath, entry)) 526 527infos = [] 528 529for file in files: 530path = os.path.join(serverRepoLocation, file) 531mimetype = guessMIME(path) 532 533text = gitCommand(serverRepoLocation, None, "log", "--format='%H\n'", file).decode() 534 535sha = text.split("\n")[0] 536identifier = f"/{username}/{repository}/{sha}" 537lastCommit = Commit.query.filter_by(identifier=identifier).first() 538 539info = { 540"name": os.path.basename(file), 541"serverPath": path, 542"relativePath": file, 543"link": os.path.join(f"/{username}/{repository}/tree/{branch}/", file), 544"size": humanSize(os.path.getsize(path)), 545"mimetype": f"{mimetype}{f' ({mimetypes.guess_type(path)[1]})' if mimetypes.guess_type(path)[1] else ''}", 546"commit": lastCommit, 547"shaSize": 7, 548} 549 550specialIcon = config.matchIcon(os.path.basename(file)) 551if specialIcon: 552info["icon"] = specialIcon 553elif os.path.isdir(path): 554info["icon"] = config.folderIcon 555elif mimetypes.guess_type(path)[0] in config.fileIcons: 556info["icon"] = config.fileIcons[mimetypes.guess_type(path)[0]] 557else: 558info["icon"] = config.unknownIcon 559 560if os.path.isdir(path): 561infos.insert(0, info) 562else: 563infos.append(info) 564 565return flask.render_template( 566"repo-tree.html", 567username=username, 568repository=repository, 569files=infos, 570subpath=os.path.join("/", subpath), 571branches=allRefs, 572current=branch 573) 574else: 575path = os.path.join(serverRepoLocation, subpath) 576 577if not os.path.exists(path): 578return flask.render_template("not-found.html"), 404 579 580mimetype = guessMIME(path) 581mode = mimetype.split("/", 1)[0] 582size = humanSize(os.path.getsize(path)) 583 584specialIcon = config.matchIcon(os.path.basename(path)) 585if specialIcon: 586icon = specialIcon 587elif os.path.isdir(path): 588icon = config.folderIcon 589elif mimetypes.guess_type(path)[0] in config.fileIcons: 590icon = config.fileIcons[mimetypes.guess_type(path)[0]] 591else: 592icon = config.unknownIcon 593 594contents = None 595if mode == "text": 596contents = convertToHTML(path) 597 598return flask.render_template( 599"repo-file.html", 600username=username, 601repository=repository, 602file=os.path.join(f"/{username}/{repository}/raw/{branch}/", subpath), 603branches=allRefs, 604current=branch, 605mode=mode, 606mimetype=mimetype, 607detailedtype=magic.from_file(path), 608size=size, 609icon=icon, 610subpath=os.path.join("/", subpath), 611basename=os.path.basename(path), 612contents=contents 613) 614 615 616@app.route("/<username>/<repository>/forum/") 617def repositoryForum(username, repository): 618if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 619repository) is not None): 620flask.abort(403) 621 622serverRepoLocation = os.path.join(config.REPOS_PATH, os.path.join(username, repository)) 623 624app.logger.info(f"Loading {serverRepoLocation}") 625 626if not os.path.exists(serverRepoLocation): 627app.logger.error(f"Cannot load {serverRepoLocation}") 628return flask.render_template("not-found.html"), 404 629 630repo = git.Repo(serverRepoLocation) 631repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 632user = User.query.filter_by(username=username).first() 633relationships = RepoAccess.query.filter_by(repo=repoData) 634userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first() 635 636return flask.render_template("repo-forum.html", username=username, repository=repository, repoData=repoData, relationships=relationships, repo=repo, userRelationship=userRelationship, Post=Post) 637 638 639@app.route("/<username>/<repository>/forum/<postID>") 640def repositoryForumThread(username, repository, postID): 641if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 642repository) is not None): 643flask.abort(403) 644 645serverRepoLocation = os.path.join(config.REPOS_PATH, os.path.join(username, repository)) 646 647app.logger.info(f"Loading {serverRepoLocation}") 648 649if not os.path.exists(serverRepoLocation): 650app.logger.error(f"Cannot load {serverRepoLocation}") 651return flask.render_template("not-found.html"), 404 652 653repo = git.Repo(serverRepoLocation) 654repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 655user = User.query.filter_by(username=username).first() 656relationships = RepoAccess.query.filter_by(repo=repoData) 657userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first() 658 659return flask.render_template("repo-forum-thread.html", username=username, repository=repository, repoData=repoData, relationships=relationships, repo=repo, userRelationship=userRelationship, Post=Post, postID=postID) 660 661 662@app.route("/<username>/<repository>/users/", methods=["GET", "POST"]) 663def repositoryUsers(username, repository): 664if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 665repository) is not None): 666flask.abort(403) 667 668serverRepoLocation = os.path.join(config.REPOS_PATH, os.path.join(username, repository)) 669 670app.logger.info(f"Loading {serverRepoLocation}") 671 672if not os.path.exists(serverRepoLocation): 673app.logger.error(f"Cannot load {serverRepoLocation}") 674return flask.render_template("not-found.html"), 404 675 676repo = git.Repo(serverRepoLocation) 677repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 678user = User.query.filter_by(username=flask.session.get("username")).first() 679relationships = RepoAccess.query.filter_by(repo=repoData) 680userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first() 681 682if flask.request.method == "GET": 683return flask.render_template("repo-users.html", username=username, repository=repository, repoData=repoData, relationships=relationships, repo=repo, userRelationship=userRelationship) 684else: 685if getPermissionLevel(flask.session.get("username"), username, repository) != 2: 686flask.abort(401) 687 688if flask.request.form.get("new-username"): 689# Create new relationship 690newUser = User.query.filter_by(username=flask.request.form.get("new-username")).first() 691relationship = RepoAccess(newUser, repoData, flask.request.form.get("new-level")) 692db.session.add(relationship) 693db.session.commit() 694if flask.request.form.get("update-username"): 695# Create new relationship 696updatedUser = User.query.filter_by(username=flask.request.form.get("update-username")).first() 697relationship = RepoAccess.query.filter_by(repo=repoData, user=updatedUser).first() 698if flask.request.form.get("update-level") == -1: 699relationship.delete() 700else: 701relationship.accessLevel = flask.request.form.get("update-level") 702db.session.commit() 703 704return flask.redirect(app.url_for("repositoryUsers", username=username, repository=repository)) 705 706 707@app.route("/<username>/<repository>/branches/") 708def repositoryBranches(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, os.path.join(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() 723 724return flask.render_template("repo-branches.html", username=username, repository=repository, repoData=repoData, repo=repo) 725 726 727@app.route("/<username>/<repository>/log/") 728def repositoryLog(username, repository): 729if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 730repository) is not None): 731flask.abort(403) 732 733serverRepoLocation = os.path.join(config.REPOS_PATH, os.path.join(username, repository)) 734 735app.logger.info(f"Loading {serverRepoLocation}") 736 737if not os.path.exists(serverRepoLocation): 738app.logger.error(f"Cannot load {serverRepoLocation}") 739return flask.render_template("not-found.html"), 404 740 741repo = git.Repo(serverRepoLocation) 742repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 743commits = Commit.query.filter_by(repo=repoData) 744 745return flask.render_template("repo-log.html", username=username, repository=repository, repoData=repoData, repo=repo, commits=commits) 746 747 748@app.route("/<username>/<repository>/settings/") 749def repositorySettings(username, repository): 750if getPermissionLevel(flask.session.get("username"), username, repository) != 2: 751flask.abort(401) 752 753return flask.render_template("repo-settings.html", username=username, repository=repository) 754 755 756@app.errorhandler(404) 757def e404(error): 758return flask.render_template("not-found.html"), 404 759 760 761@app.errorhandler(401) 762def e401(error): 763return flask.render_template("unauthorised.html"), 401 764 765 766@app.errorhandler(403) 767def e403(error): 768return flask.render_template("forbidden.html"), 403 769 770 771@app.errorhandler(418) 772def e418(error): 773return flask.render_template("teapot.html"), 418 774 775 776@app.errorhandler(405) 777def e405(error): 778return flask.render_template("method-not-allowed.html"), 405 779 780 781if __name__ == "__main__": 782app.run(debug=True, port=8080, host="0.0.0.0") 783