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