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) 179repoName = db.Column(db.String(98), db.ForeignKey("repo.route"), nullable=False) 180ownerName = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 181 182parentID = db.Column(db.String(109), db.ForeignKey("post.identifier"), nullable=True) 183 184date = db.Column(db.DateTime, default=datetime.now) 185subject = db.Column(db.Unicode(384)) 186message = db.Column(db.UnicodeText) 187repo = db.relationship("Repo", back_populates="posts") 188owner = db.relationship("User", back_populates="posts") 189parent = db.relationship("Post", back_populates="children", remote_side="Post.identifier") 190children = db.relationship("Post", back_populates="parent", remote_side="Post.parentID") 191 192def __init__(self, owner, repo, parent, subject, message): 193self.identifier = f"/{owner.username}/{repo.name}/{repo.lastPostID}" 194self.repoName = repo.route 195self.repo = repo 196self.ownerName = owner.username 197self.owner = owner 198self.subject = subject 199self.message = message 200self.parent = parent 201repo.lastPostID += 1 202 203 204def getPermissionLevel(loggedIn, username, repository): 205user = User.query.filter_by(username=loggedIn).first() 206repo = Repo.query.filter_by(route=f"/{username}/{repository}").first() 207 208if user and repo: 209permission = RepoAccess.query.filter_by(user=user, repo=repo).first() 210if permission: 211return permission.accessLevel 212 213return None 214 215 216def getVisibility(username, repository): 217repo = Repo.query.filter_by(route=f"/{username}/{repository}").first() 218 219if repo: 220return repo.visibility 221 222return None 223 224 225import gitHTTP 226import jinjaUtils 227 228 229def humanSize(value, decimals=2, scale=1024, 230units=("B", "kiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB", "RiB", "QiB")): 231for unit in units: 232if value < scale: 233break 234value /= scale 235if int(value) == value: 236# do not return decimals, if the value is already round 237return int(value), unit 238return round(value * 10 ** decimals) / 10 ** decimals, unit 239 240 241def guessMIME(path): 242if os.path.isdir(path): 243mimetype = "inode/directory" 244elif magic.from_file(path, mime=True): 245mimetype = magic.from_file(path, mime=True) 246else: 247mimetype = "application/octet-stream" 248return mimetype 249 250 251def convertToHTML(path): 252with open(path, "r") as f: 253contents = f.read() 254return contents 255 256 257@app.context_processor 258def default(): 259username = flask.session.get("username") 260 261return {"loggedInUser": username} 262 263 264@app.route("/") 265def main(): 266return flask.render_template("home.html") 267 268 269@app.route("/about/") 270def about(): 271return flask.render_template("about.html", platform=platform) 272 273 274@app.route("/settings/", methods=["GET", "POST"]) 275def settings(): 276if flask.request.method == "GET": 277if not flask.session.get("username"): 278flask.abort(401) 279user = User.query.filter_by(username=flask.session.get("username")).first() 280 281return flask.render_template("user-settings.html", user=user) 282else: 283user = User.query.filter_by(username=flask.session.get("username")).first() 284 285user.displayName = flask.request.form["displayname"] 286user.URL = flask.request.form["url"] 287user.company = flask.request.form["company"] 288user.companyURL = flask.request.form["companyurl"] 289user.location = flask.request.form["location"] 290user.showMail = flask.request.form.get("showmail", user.showMail) 291 292db.session.commit() 293 294flask.flash(Markup("<iconify-icon icon='mdi:check'></iconify-icon>Settings saved"), category="success") 295return flask.redirect(f"/{flask.session.get('username')}", code=303) 296 297 298@app.route("/accounts/", methods=["GET", "POST"]) 299def login(): 300if flask.request.method == "GET": 301return flask.render_template("login.html") 302else: 303if "login" in flask.request.form: 304username = flask.request.form["username"] 305password = flask.request.form["password"] 306 307user = User.query.filter_by(username=username).first() 308 309if user and bcrypt.check_password_hash(user.passwordHashed, password): 310flask.session["username"] = user.username 311flask.flash( 312Markup(f"<iconify-icon icon='mdi:account'></iconify-icon>Successfully logged in as {username}"), 313category="success") 314return flask.redirect("/", code=303) 315elif not user: 316flask.flash(Markup("<iconify-icon icon='mdi:account-question'></iconify-icon>User not found"), 317category="alert") 318return flask.render_template("login.html") 319else: 320flask.flash(Markup("<iconify-icon icon='mdi:account-question'></iconify-icon>Invalid password"), 321category="error") 322return flask.render_template("login.html") 323if "signup" in flask.request.form: 324username = flask.request.form["username"] 325password = flask.request.form["password"] 326password2 = flask.request.form["password2"] 327email = flask.request.form.get("email") 328email2 = flask.request.form.get("email2") # repeat email is a honeypot 329name = flask.request.form.get("name") 330 331if not onlyChars(username, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_"): 332flask.flash(Markup( 333"<iconify-icon icon='mdi:account-error'></iconify-icon>Usernames may only contain Latin alphabet, numbers, '-' and '_'"), 334category="error") 335return flask.render_template("login.html") 336 337if username in config.RESERVED_NAMES: 338flask.flash( 339Markup(f"<iconify-icon icon='mdi:account-error'></iconify-icon>Sorry, {username} is a system path"), 340category="error") 341return flask.render_template("login.html") 342 343userCheck = User.query.filter_by(username=username).first() 344if userCheck: 345flask.flash( 346Markup(f"<iconify-icon icon='mdi:account-error'></iconify-icon>The username {username} is taken"), 347category="error") 348return flask.render_template("login.html") 349 350if password2 != password: 351flask.flash(Markup("<iconify-icon icon='mdi:key-alert'></iconify-icon>Make sure the passwords match"), 352category="error") 353return flask.render_template("login.html") 354 355user = User(username, password, email, name) 356db.session.add(user) 357db.session.commit() 358flask.session["username"] = user.username 359flask.flash(Markup( 360f"<iconify-icon icon='mdi:account'></iconify-icon>Successfully created and logged in as {username}"), 361category="success") 362return flask.redirect("/", code=303) 363 364 365@app.route("/newrepo/", methods=["GET", "POST"]) 366def newRepo(): 367if flask.request.method == "GET": 368return flask.render_template("new-repo.html") 369else: 370name = flask.request.form["name"] 371visibility = int(flask.request.form["visibility"]) 372 373if not onlyChars(name, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_"): 374flask.flash(Markup( 375"<iconify-icon icon='mdi:error'></iconify-icon>Repository names may only contain Latin alphabet, numbers, '-' and '_'"), 376category="error") 377return flask.render_template("new-repo.html") 378 379user = User.query.filter_by(username=flask.session.get("username")).first() 380 381repo = Repo(user, name, visibility) 382db.session.add(repo) 383db.session.commit() 384 385if not os.path.exists(os.path.join(config.REPOS_PATH, repo.route)): 386subprocess.run(["git", "init", repo.name], 387cwd=os.path.join(config.REPOS_PATH, flask.session.get("username"))) 388 389flask.flash(Markup(f"<iconify-icon icon='mdi:folder'></iconify-icon>Successfully created repository {name}"), 390category="success") 391return flask.redirect(repo.route, code=303) 392 393 394@app.route("/logout") 395def logout(): 396flask.session.clear() 397flask.flash(Markup(f"<iconify-icon icon='mdi:account'></iconify-icon>Successfully logged out"), category="info") 398return flask.redirect("/", code=303) 399 400 401@app.route("/<username>/") 402def userProfile(username): 403user = User.query.filter_by(username=username).first() 404repos = Repo.query.filter_by(ownerName=username, visibility=2) 405return flask.render_template("user-profile.html", user=user, repos=repos) 406 407 408@app.route("/<username>/<repository>/") 409def repositoryIndex(username, repository): 410return flask.redirect("./tree", code=302) 411 412 413@app.route("/<username>/<repository>/raw/<branch>/<path:subpath>") 414def repositoryRaw(username, repository, branch, subpath): 415if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("user"), username, 416repository) is not None): 417flask.abort(403) 418 419serverRepoLocation = os.path.join(config.REPOS_PATH, os.path.join(username, repository)) 420 421app.logger.info(f"Loading {serverRepoLocation}") 422 423if not os.path.exists(serverRepoLocation): 424app.logger.error(f"Cannot load {serverRepoLocation}") 425return flask.render_template("not-found.html"), 404 426 427repo = git.Repo(serverRepoLocation) 428repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 429if not repoData.defaultBranch: 430if repo.heads: 431repoData.defaultBranch = repo.heads[0].name 432else: 433return flask.render_template("empty.html", 434remote=f"http://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200 435if not branch: 436branch = repoData.defaultBranch 437return flask.redirect(f"./{branch}", code=302) 438 439if branch.startswith("tag:"): 440ref = f"tags/{branch[4:]}" 441elif branch.startswith("~"): 442ref = branch[1:] 443else: 444ref = f"heads/{branch}" 445 446ref = ref.replace("~", "/") # encode slashes for URL support 447 448try: 449repo.git.checkout("-f", ref) 450except git.exc.GitCommandError: 451return flask.render_template("not-found.html"), 404 452 453return flask.send_from_directory(config.REPOS_PATH, os.path.join(username, repository, subpath)) 454 455 456@app.route("/info/<username>/avatar") 457def userAvatar(username): 458serverUserdataLocation = os.path.join(config.USERDATA_PATH, username) 459 460if not os.path.exists(serverUserdataLocation): 461return flask.render_template("not-found.html"), 404 462 463return flask.send_from_directory(serverUserdataLocation, "avatar.png") 464 465 466@app.route("/<username>/<repository>/tree/", defaults={"branch": None, "subpath": ""}) 467@app.route("/<username>/<repository>/tree/<branch>/", defaults={"subpath": ""}) 468@app.route("/<username>/<repository>/tree/<branch>/<path:subpath>") 469def repositoryTree(username, repository, branch, subpath): 470if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 471repository) is not None): 472flask.abort(403) 473 474serverRepoLocation = os.path.join(config.REPOS_PATH, os.path.join(username, repository)) 475 476app.logger.info(f"Loading {serverRepoLocation}") 477 478if not os.path.exists(serverRepoLocation): 479app.logger.error(f"Cannot load {serverRepoLocation}") 480return flask.render_template("not-found.html"), 404 481 482repo = git.Repo(serverRepoLocation) 483repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 484if not repoData.defaultBranch: 485if repo.heads: 486repoData.defaultBranch = repo.heads[0].name 487else: 488return flask.render_template("empty.html", 489remote=f"http://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200 490if not branch: 491branch = repoData.defaultBranch 492return flask.redirect(f"./{branch}", code=302) 493 494if branch.startswith("tag:"): 495ref = f"tags/{branch[4:]}" 496elif branch.startswith("~"): 497ref = branch[1:] 498else: 499ref = f"heads/{branch}" 500 501ref = ref.replace("~", "/") # encode slashes for URL support 502 503try: 504repo.git.checkout("-f", ref) 505except git.exc.GitCommandError: 506return flask.render_template("not-found.html"), 404 507 508branches = repo.heads 509 510allRefs = [] 511for ref in repo.heads: 512allRefs.append((ref, "head")) 513for ref in repo.tags: 514allRefs.append((ref, "tag")) 515 516if os.path.isdir(os.path.join(serverRepoLocation, subpath)): 517files = [] 518blobs = [] 519 520for entry in os.listdir(os.path.join(serverRepoLocation, subpath)): 521if not os.path.basename(entry) == ".git": 522files.append(os.path.join(subpath, entry)) 523 524infos = [] 525 526for file in files: 527path = os.path.join(serverRepoLocation, file) 528mimetype = guessMIME(path) 529 530text = gitCommand(serverRepoLocation, None, "log", "--format='%H\n'", file).decode() 531 532sha = text.split("\n")[0] 533identifier = f"/{username}/{repository}/{sha}" 534lastCommit = Commit.query.filter_by(identifier=identifier).first() 535 536info = { 537"name": os.path.basename(file), 538"serverPath": path, 539"relativePath": file, 540"link": os.path.join(f"/{username}/{repository}/tree/{branch}/", file), 541"size": humanSize(os.path.getsize(path)), 542"mimetype": f"{mimetype}{f' ({mimetypes.guess_type(path)[1]})' if mimetypes.guess_type(path)[1] else ''}", 543"commit": lastCommit, 544"shaSize": 7, 545} 546 547specialIcon = config.matchIcon(os.path.basename(file)) 548if specialIcon: 549info["icon"] = specialIcon 550elif os.path.isdir(path): 551info["icon"] = config.folderIcon 552elif mimetypes.guess_type(path)[0] in config.fileIcons: 553info["icon"] = config.fileIcons[mimetypes.guess_type(path)[0]] 554else: 555info["icon"] = config.unknownIcon 556 557if os.path.isdir(path): 558infos.insert(0, info) 559else: 560infos.append(info) 561 562return flask.render_template( 563"repo-tree.html", 564username=username, 565repository=repository, 566files=infos, 567subpath=os.path.join("/", subpath), 568branches=allRefs, 569current=branch 570) 571else: 572path = os.path.join(serverRepoLocation, subpath) 573 574if not os.path.exists(path): 575return flask.render_template("not-found.html"), 404 576 577mimetype = guessMIME(path) 578mode = mimetype.split("/", 1)[0] 579size = humanSize(os.path.getsize(path)) 580 581specialIcon = config.matchIcon(os.path.basename(path)) 582if specialIcon: 583icon = specialIcon 584elif os.path.isdir(path): 585icon = config.folderIcon 586elif mimetypes.guess_type(path)[0] in config.fileIcons: 587icon = config.fileIcons[mimetypes.guess_type(path)[0]] 588else: 589icon = config.unknownIcon 590 591contents = None 592if mode == "text": 593contents = convertToHTML(path) 594 595return flask.render_template( 596"repo-file.html", 597username=username, 598repository=repository, 599file=os.path.join(f"/{username}/{repository}/raw/{branch}/", subpath), 600branches=allRefs, 601current=branch, 602mode=mode, 603mimetype=mimetype, 604detailedtype=magic.from_file(path), 605size=size, 606icon=icon, 607subpath=os.path.join("/", subpath), 608basename=os.path.basename(path), 609contents=contents 610) 611 612 613@app.route("/<username>/<repository>/forum/") 614def repositoryForum(username, repository): 615if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 616repository) is not None): 617flask.abort(403) 618 619serverRepoLocation = os.path.join(config.REPOS_PATH, os.path.join(username, repository)) 620 621app.logger.info(f"Loading {serverRepoLocation}") 622 623if not os.path.exists(serverRepoLocation): 624app.logger.error(f"Cannot load {serverRepoLocation}") 625return flask.render_template("not-found.html"), 404 626 627repo = git.Repo(serverRepoLocation) 628repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 629user = User.query.filter_by(username=username).first() 630relationships = RepoAccess.query.filter_by(repo=repoData) 631userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first() 632 633return flask.render_template("repo-forum.html", username=username, repository=repository, repoData=repoData, relationships=relationships, repo=repo, userRelationship=userRelationship, Post=Post) 634 635 636@app.route("/<username>/<repository>/users/", methods=["GET", "POST"]) 637def repositoryUsers(username, repository): 638if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 639repository) is not None): 640flask.abort(403) 641 642serverRepoLocation = os.path.join(config.REPOS_PATH, os.path.join(username, repository)) 643 644app.logger.info(f"Loading {serverRepoLocation}") 645 646if not os.path.exists(serverRepoLocation): 647app.logger.error(f"Cannot load {serverRepoLocation}") 648return flask.render_template("not-found.html"), 404 649 650repo = git.Repo(serverRepoLocation) 651repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 652user = User.query.filter_by(username=flask.session.get("username")).first() 653relationships = RepoAccess.query.filter_by(repo=repoData) 654userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first() 655 656if flask.request.method == "GET": 657return flask.render_template("repo-users.html", username=username, repository=repository, repoData=repoData, relationships=relationships, repo=repo, userRelationship=userRelationship) 658else: 659if getPermissionLevel(flask.session.get("username"), username, repository) != 2: 660flask.abort(401) 661 662if flask.request.form.get("new-username"): 663# Create new relationship 664newUser = User.query.filter_by(username=flask.request.form.get("new-username")).first() 665relationship = RepoAccess(newUser, repoData, flask.request.form.get("new-level")) 666db.session.add(relationship) 667db.session.commit() 668if flask.request.form.get("update-username"): 669# Create new relationship 670updatedUser = User.query.filter_by(username=flask.request.form.get("update-username")).first() 671relationship = RepoAccess.query.filter_by(repo=repoData, user=updatedUser).first() 672if flask.request.form.get("update-level") == -1: 673relationship.delete() 674else: 675relationship.accessLevel = flask.request.form.get("update-level") 676db.session.commit() 677 678return flask.redirect(app.url_for("repositoryUsers", username=username, repository=repository)) 679 680 681@app.route("/<username>/<repository>/branches/") 682def repositoryBranches(username, repository): 683if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 684repository) is not None): 685flask.abort(403) 686 687serverRepoLocation = os.path.join(config.REPOS_PATH, os.path.join(username, repository)) 688 689app.logger.info(f"Loading {serverRepoLocation}") 690 691if not os.path.exists(serverRepoLocation): 692app.logger.error(f"Cannot load {serverRepoLocation}") 693return flask.render_template("not-found.html"), 404 694 695repo = git.Repo(serverRepoLocation) 696repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 697 698return flask.render_template("repo-branches.html", username=username, repository=repository, repoData=repoData, repo=repo) 699 700 701@app.route("/<username>/<repository>/log/") 702def repositoryLog(username, repository): 703if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 704repository) is not None): 705flask.abort(403) 706 707serverRepoLocation = os.path.join(config.REPOS_PATH, os.path.join(username, repository)) 708 709app.logger.info(f"Loading {serverRepoLocation}") 710 711if not os.path.exists(serverRepoLocation): 712app.logger.error(f"Cannot load {serverRepoLocation}") 713return flask.render_template("not-found.html"), 404 714 715repo = git.Repo(serverRepoLocation) 716repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 717commits = Commit.query.filter_by(repo=repoData) 718 719return flask.render_template("repo-log.html", username=username, repository=repository, repoData=repoData, repo=repo, commits=commits) 720 721 722@app.route("/<username>/<repository>/settings/") 723def repositorySettings(username, repository): 724if getPermissionLevel(flask.session.get("username"), username, repository) != 2: 725flask.abort(401) 726 727return flask.render_template("repo-settings.html", username=username, repository=repository) 728 729 730@app.errorhandler(404) 731def e404(error): 732return flask.render_template("not-found.html"), 404 733 734 735@app.errorhandler(401) 736def e401(error): 737return flask.render_template("unauthorised.html"), 401 738 739 740@app.errorhandler(403) 741def e403(error): 742return flask.render_template("forbidden.html"), 403 743 744 745@app.errorhandler(418) 746def e418(error): 747return flask.render_template("teapot.html"), 418 748 749 750@app.errorhandler(405) 751def e405(error): 752return flask.render_template("method-not-allowed.html"), 405 753 754 755if __name__ == "__main__": 756app.run(debug=True, port=8080, host="0.0.0.0") 757