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{'s' if config.suggestHTTPS else ''}://{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, 617remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}" 618) 619else: 620path = os.path.join(serverRepoLocation, subpath) 621 622if not os.path.exists(path): 623return flask.render_template("not-found.html"), 404 624 625mimetype = guessMIME(path) 626mode = mimetype.split("/", 1)[0] 627size = humanSize(os.path.getsize(path)) 628 629specialIcon = config.matchIcon(os.path.basename(path)) 630if specialIcon: 631icon = specialIcon 632elif os.path.isdir(path): 633icon = config.folderIcon 634elif mimetypes.guess_type(path)[0] in config.fileIcons: 635icon = config.fileIcons[mimetypes.guess_type(path)[0]] 636else: 637icon = config.unknownIcon 638 639contents = None 640if mode == "text": 641contents = convertToHTML(path) 642 643return flask.render_template( 644"repo-file.html", 645username=username, 646repository=repository, 647file=os.path.join(f"/{username}/{repository}/raw/{branch}/", subpath), 648branches=allRefs, 649current=branch, 650mode=mode, 651mimetype=mimetype, 652detailedtype=magic.from_file(path), 653size=size, 654icon=icon, 655subpath=os.path.join("/", subpath), 656basename=os.path.basename(path), 657contents=contents, 658remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}" 659) 660 661 662@app.route("/<username>/<repository>/forum/") 663def repositoryForum(username, repository): 664if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 665repository) is not None): 666flask.abort(403) 667 668serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) 669 670app.logger.info(f"Loading {serverRepoLocation}") 671 672if not os.path.exists(serverRepoLocation): 673app.logger.error(f"Cannot load {serverRepoLocation}") 674return flask.render_template("not-found.html"), 404 675 676repo = git.Repo(serverRepoLocation) 677repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 678user = User.query.filter_by(username=flask.session.get("username")).first() 679relationships = RepoAccess.query.filter_by(repo=repoData) 680userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first() 681 682return flask.render_template( 683"repo-forum.html", 684username=username, 685repository=repository, 686repoData=repoData, 687relationships=relationships, 688repo=repo, 689userRelationship=userRelationship, 690Post=Post, 691remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}" 692) 693 694 695@app.route("/<username>/<repository>/forum/new", methods=["POST"]) 696def repositoryForumAdd(username, repository): 697if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 698repository) is not None): 699flask.abort(403) 700 701serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) 702 703app.logger.info(f"Loading {serverRepoLocation}") 704 705if not os.path.exists(serverRepoLocation): 706app.logger.error(f"Cannot load {serverRepoLocation}") 707return flask.render_template("not-found.html"), 404 708 709repo = git.Repo(serverRepoLocation) 710repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 711user = User.query.filter_by(username=flask.session.get("username")).first() 712relationships = RepoAccess.query.filter_by(repo=repoData) 713userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first() 714 715post = Post(user, repoData, None, flask.request.form["subject"], flask.request.form["message"]) 716 717db.session.add(post) 718db.session.commit() 719 720return flask.redirect(flask.url_for("repositoryForumThread", username=username, repository=repository, postID=post.number), code=303) 721 722 723@app.route("/<username>/<repository>/forum/<int:postID>") 724def repositoryForumThread(username, repository, postID): 725if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 726repository) is not None): 727flask.abort(403) 728 729serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) 730 731app.logger.info(f"Loading {serverRepoLocation}") 732 733if not os.path.exists(serverRepoLocation): 734app.logger.error(f"Cannot load {serverRepoLocation}") 735return flask.render_template("not-found.html"), 404 736 737repo = git.Repo(serverRepoLocation) 738repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 739user = User.query.filter_by(username=flask.session.get("username")).first() 740relationships = RepoAccess.query.filter_by(repo=repoData) 741userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first() 742 743return flask.render_template( 744"repo-forum-thread.html", 745username=username, 746repository=repository, 747repoData=repoData, 748relationships=relationships, 749repo=repo, 750userRelationship=userRelationship, 751Post=Post, 752postID=postID, 753maxPostNesting=4, 754remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}" 755) 756 757 758@app.route("/<username>/<repository>/forum/<int:postID>/reply", methods=["POST"]) 759def repositoryForumReply(username, repository, postID): 760if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 761repository) is not None): 762flask.abort(403) 763 764serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) 765 766app.logger.info(f"Loading {serverRepoLocation}") 767 768if not os.path.exists(serverRepoLocation): 769app.logger.error(f"Cannot load {serverRepoLocation}") 770return flask.render_template("not-found.html"), 404 771 772repo = git.Repo(serverRepoLocation) 773repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 774user = User.query.filter_by(username=flask.session.get("username")).first() 775relationships = RepoAccess.query.filter_by(repo=repoData) 776userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first() 777if not user: 778flask.abort(401) 779 780parent = Post.query.filter_by(identifier=f"/{username}/{repository}/{postID}").first() 781post = Post(user, repoData, parent, flask.request.form["subject"], flask.request.form["message"]) 782 783db.session.add(post) 784post.updateDate() 785db.session.commit() 786 787return flask.redirect(flask.url_for("repositoryForumThread", username=username, repository=repository, postID=postID), code=303) 788 789 790@app.route("/<username>/<repository>/forum/<int:postID>/voteup", defaults={"score": 1}) 791@app.route("/<username>/<repository>/forum/<int:postID>/votedown", defaults={"score": -1}) 792@app.route("/<username>/<repository>/forum/<int:postID>/votes", defaults={"score": 0}) 793def repositoryForumVote(username, repository, postID, score): 794if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 795repository) is not None): 796flask.abort(403) 797 798serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) 799 800app.logger.info(f"Loading {serverRepoLocation}") 801 802if not os.path.exists(serverRepoLocation): 803app.logger.error(f"Cannot load {serverRepoLocation}") 804return flask.render_template("not-found.html"), 404 805 806repo = git.Repo(serverRepoLocation) 807repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 808user = User.query.filter_by(username=flask.session.get("username")).first() 809relationships = RepoAccess.query.filter_by(repo=repoData) 810userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first() 811if not user: 812flask.abort(401) 813 814post = Post.query.filter_by(identifier=f"/{username}/{repository}/{postID}").first() 815 816if score: 817oldRelationship = PostVote.query.filter_by(userUsername=user.username, postIdentifier=post.identifier).first() 818if oldRelationship: 819if score == oldRelationship.voteScore: 820db.session.delete(oldRelationship) 821post.voteSum -= oldRelationship.voteScore 822else: 823post.voteSum -= oldRelationship.voteScore 824post.voteSum += score 825oldRelationship.voteScore = score 826else: 827relationship = PostVote(user, post, score) 828post.voteSum += score 829db.session.add(relationship) 830 831db.session.commit() 832 833userVote = PostVote.query.filter_by(userUsername=user.username, postIdentifier=post.identifier).first() 834response = flask.make_response(str(post.voteSum) + " " + str(userVote.voteScore if userVote else 0)) 835response.content_type = "text/plain" 836 837return response 838 839 840@app.route("/<username>/<repository>/users/", methods=["GET", "POST"]) 841def repositoryUsers(username, repository): 842if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 843repository) is not None): 844flask.abort(403) 845 846serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) 847 848app.logger.info(f"Loading {serverRepoLocation}") 849 850if not os.path.exists(serverRepoLocation): 851app.logger.error(f"Cannot load {serverRepoLocation}") 852return flask.render_template("not-found.html"), 404 853 854repo = git.Repo(serverRepoLocation) 855repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 856user = User.query.filter_by(username=flask.session.get("username")).first() 857relationships = RepoAccess.query.filter_by(repo=repoData) 858userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first() 859 860if flask.request.method == "GET": 861return flask.render_template( 862"repo-users.html", 863username=username, 864repository=repository, 865repoData=repoData, 866relationships=relationships, 867repo=repo, 868userRelationship=userRelationship, 869remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}" 870) 871else: 872if getPermissionLevel(flask.session.get("username"), username, repository) != 2: 873flask.abort(401) 874 875if flask.request.form.get("new-username"): 876# Create new relationship 877newUser = User.query.filter_by(username=flask.request.form.get("new-username")).first() 878relationship = RepoAccess(newUser, repoData, flask.request.form.get("new-level")) 879db.session.add(relationship) 880db.session.commit() 881if flask.request.form.get("update-username"): 882# Create new relationship 883updatedUser = User.query.filter_by(username=flask.request.form.get("update-username")).first() 884relationship = RepoAccess.query.filter_by(repo=repoData, user=updatedUser).first() 885if flask.request.form.get("update-level") == -1: 886relationship.delete() 887else: 888relationship.accessLevel = flask.request.form.get("update-level") 889db.session.commit() 890 891return flask.redirect(app.url_for("repositoryUsers", username=username, repository=repository)) 892 893 894@app.route("/<username>/<repository>/branches/") 895def repositoryBranches(username, repository): 896if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 897repository) is not None): 898flask.abort(403) 899 900serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) 901 902app.logger.info(f"Loading {serverRepoLocation}") 903 904if not os.path.exists(serverRepoLocation): 905app.logger.error(f"Cannot load {serverRepoLocation}") 906return flask.render_template("not-found.html"), 404 907 908repo = git.Repo(serverRepoLocation) 909repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 910 911return flask.render_template( 912"repo-branches.html", 913username=username, 914repository=repository, 915repoData=repoData, 916repo=repo, 917remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}" 918) 919 920 921@app.route("/<username>/<repository>/log/") 922def repositoryLog(username, repository): 923if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 924repository) is not None): 925flask.abort(403) 926 927serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) 928 929app.logger.info(f"Loading {serverRepoLocation}") 930 931if not os.path.exists(serverRepoLocation): 932app.logger.error(f"Cannot load {serverRepoLocation}") 933return flask.render_template("not-found.html"), 404 934 935repo = git.Repo(serverRepoLocation) 936repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 937commits = Commit.query.filter_by(repo=repoData) 938 939return flask.render_template( 940"repo-log.html", 941username=username, 942repository=repository, 943repoData=repoData, 944repo=repo, 945commits=commits, 946remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}" 947) 948 949 950@app.route("/<username>/<repository>/settings/") 951def repositorySettings(username, repository): 952if getPermissionLevel(flask.session.get("username"), username, repository) != 2: 953flask.abort(401) 954 955return flask.render_template("repo-settings.html", username=username, repository=repository) 956 957 958@app.errorhandler(404) 959def e404(error): 960return flask.render_template("not-found.html"), 404 961 962 963@app.errorhandler(401) 964def e401(error): 965return flask.render_template("unauthorised.html"), 401 966 967 968@app.errorhandler(403) 969def e403(error): 970return flask.render_template("forbidden.html"), 403 971 972 973@app.errorhandler(418) 974def e418(error): 975return flask.render_template("teapot.html"), 418 976 977 978@app.errorhandler(405) 979def e405(error): 980return flask.render_template("method-not-allowed.html"), 405 981 982 983if __name__ == "__main__": 984app.run(debug=True, port=8080, host="0.0.0.0") 985