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