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_uc1"),) 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 272def getFavourite(loggedIn, username, repository): 273print(loggedIn, username, repository) 274relationship = RepoFavourite.query.filter_by(userUsername=loggedIn, repoRoute=f"/{username}/{repository}").first() 275return relationship 276 277 278import gitHTTP 279import jinjaUtils 280 281 282def humanSize(value, decimals=2, scale=1024, 283units=("B", "kiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB", "RiB", "QiB")): 284for unit in units: 285if value < scale: 286break 287value /= scale 288if int(value) == value: 289# do not return decimals, if the value is already round 290return int(value), unit 291return round(value * 10 ** decimals) / 10 ** decimals, unit 292 293 294def guessMIME(path): 295if os.path.isdir(path): 296mimetype = "inode/directory" 297elif magic.from_file(path, mime=True): 298mimetype = magic.from_file(path, mime=True) 299else: 300mimetype = "application/octet-stream" 301return mimetype 302 303 304def convertToHTML(path): 305with open(path, "r") as f: 306contents = f.read() 307return contents 308 309 310repositories = flask.Blueprint("repository", __name__, template_folder="templates/repository/") 311 312 313@app.context_processor 314def default(): 315username = flask.session.get("username") 316 317return {"loggedInUser": username} 318 319 320@app.route("/") 321def main(): 322return flask.render_template("home.html") 323 324 325@app.route("/about/") 326def about(): 327return flask.render_template("about.html", platform=platform) 328 329 330@app.route("/settings/", methods=["GET", "POST"]) 331def settings(): 332if not flask.session.get("username"): 333flask.abort(401) 334if flask.request.method == "GET": 335user = User.query.filter_by(username=flask.session.get("username")).first() 336 337return flask.render_template("user-settings.html", user=user) 338else: 339user = User.query.filter_by(username=flask.session.get("username")).first() 340 341user.displayName = flask.request.form["displayname"] 342user.URL = flask.request.form["url"] 343user.company = flask.request.form["company"] 344user.companyURL = flask.request.form["companyurl"] 345user.location = flask.request.form["location"] 346user.showMail = flask.request.form.get("showmail", user.showMail) 347 348db.session.commit() 349 350flask.flash(Markup("<iconify-icon icon='mdi:check'></iconify-icon>Settings saved"), category="success") 351return flask.redirect(f"/{flask.session.get('username')}", code=303) 352 353 354@app.route("/favourites/", methods=["GET", "POST"]) 355def favourites(): 356if not flask.session.get("username"): 357flask.abort(401) 358if flask.request.method == "GET": 359relationships = RepoFavourite.query.filter_by(userUsername=flask.session.get("username")) 360 361return flask.render_template("favourites.html", favourites=relationships) 362 363 364@app.route("/accounts/", methods=["GET", "POST"]) 365def login(): 366if flask.request.method == "GET": 367return flask.render_template("login.html") 368else: 369if "login" in flask.request.form: 370username = flask.request.form["username"] 371password = flask.request.form["password"] 372 373user = User.query.filter_by(username=username).first() 374 375if user and bcrypt.check_password_hash(user.passwordHashed, password): 376flask.session["username"] = user.username 377flask.flash( 378Markup(f"<iconify-icon icon='mdi:account'></iconify-icon>Successfully logged in as {username}"), 379category="success") 380return flask.redirect("/", code=303) 381elif not user: 382flask.flash(Markup("<iconify-icon icon='mdi:account-question'></iconify-icon>User not found"), 383category="alert") 384return flask.render_template("login.html") 385else: 386flask.flash(Markup("<iconify-icon icon='mdi:account-question'></iconify-icon>Invalid password"), 387category="error") 388return flask.render_template("login.html") 389if "signup" in flask.request.form: 390username = flask.request.form["username"] 391password = flask.request.form["password"] 392password2 = flask.request.form["password2"] 393email = flask.request.form.get("email") 394email2 = flask.request.form.get("email2") # repeat email is a honeypot 395name = flask.request.form.get("name") 396 397if not onlyChars(username, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_"): 398flask.flash(Markup( 399"<iconify-icon icon='mdi:account-error'></iconify-icon>Usernames may only contain Latin alphabet, numbers, '-' and '_'"), 400category="error") 401return flask.render_template("login.html") 402 403if username in config.RESERVED_NAMES: 404flask.flash( 405Markup(f"<iconify-icon icon='mdi:account-error'></iconify-icon>Sorry, {username} is a system path"), 406category="error") 407return flask.render_template("login.html") 408 409userCheck = User.query.filter_by(username=username).first() 410if userCheck: 411flask.flash( 412Markup(f"<iconify-icon icon='mdi:account-error'></iconify-icon>The username {username} is taken"), 413category="error") 414return flask.render_template("login.html") 415 416if password2 != password: 417flask.flash(Markup("<iconify-icon icon='mdi:key-alert'></iconify-icon>Make sure the passwords match"), 418category="error") 419return flask.render_template("login.html") 420 421user = User(username, password, email, name) 422db.session.add(user) 423db.session.commit() 424flask.session["username"] = user.username 425flask.flash(Markup( 426f"<iconify-icon icon='mdi:account'></iconify-icon>Successfully created and logged in as {username}"), 427category="success") 428return flask.redirect("/", code=303) 429 430 431@app.route("/newrepo/", methods=["GET", "POST"]) 432def newRepo(): 433if not flask.session.get("username"): 434flask.abort(401) 435if flask.request.method == "GET": 436return flask.render_template("new-repo.html") 437else: 438name = flask.request.form["name"] 439visibility = int(flask.request.form["visibility"]) 440 441if not onlyChars(name, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_"): 442flask.flash(Markup( 443"<iconify-icon icon='mdi:error'></iconify-icon>Repository names may only contain Latin alphabet, numbers, '-' and '_'"), 444category="error") 445return flask.render_template("new-repo.html") 446 447user = User.query.filter_by(username=flask.session.get("username")).first() 448 449repo = Repo(user, name, visibility) 450db.session.add(repo) 451db.session.commit() 452 453if not os.path.exists(os.path.join(config.REPOS_PATH, repo.route)): 454subprocess.run(["git", "init", repo.name], 455cwd=os.path.join(config.REPOS_PATH, flask.session.get("username"))) 456 457flask.flash(Markup(f"<iconify-icon icon='mdi:folder'></iconify-icon>Successfully created repository {name}"), 458category="success") 459return flask.redirect(repo.route, code=303) 460 461 462@app.route("/logout") 463def logout(): 464flask.session.clear() 465flask.flash(Markup(f"<iconify-icon icon='mdi:account'></iconify-icon>Successfully logged out"), category="info") 466return flask.redirect("/", code=303) 467 468 469@app.route("/<username>/") 470def userProfile(username): 471user = User.query.filter_by(username=username).first() 472repos = Repo.query.filter_by(ownerName=username, visibility=2) 473return flask.render_template("user-profile.html", user=user, repos=repos) 474 475 476@app.route("/<username>/<repository>/") 477def repositoryIndex(username, repository): 478return flask.redirect("./tree", code=302) 479 480 481@app.route("/info/<username>/avatar") 482def userAvatar(username): 483serverUserdataLocation = os.path.join(config.USERDATA_PATH, username) 484 485if not os.path.exists(serverUserdataLocation): 486return flask.render_template("not-found.html"), 404 487 488return flask.send_from_directory(serverUserdataLocation, "avatar.png") 489 490 491@app.route("/<username>/<repository>/raw/<branch>/<path:subpath>") 492def repositoryRaw(username, repository, branch, subpath): 493if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 494repository) is not None): 495flask.abort(403) 496 497serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) 498 499app.logger.info(f"Loading {serverRepoLocation}") 500 501if not os.path.exists(serverRepoLocation): 502app.logger.error(f"Cannot load {serverRepoLocation}") 503return flask.render_template("not-found.html"), 404 504 505repo = git.Repo(serverRepoLocation) 506repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 507if not repoData.defaultBranch: 508if repo.heads: 509repoData.defaultBranch = repo.heads[0].name 510else: 511return flask.render_template("empty.html", 512remote=f"http://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200 513if not branch: 514branch = repoData.defaultBranch 515return flask.redirect(f"./{branch}", code=302) 516 517if branch.startswith("tag:"): 518ref = f"tags/{branch[4:]}" 519elif branch.startswith("~"): 520ref = branch[1:] 521else: 522ref = f"heads/{branch}" 523 524ref = ref.replace("~", "/") # encode slashes for URL support 525 526try: 527repo.git.checkout("-f", ref) 528except git.exc.GitCommandError: 529return flask.render_template("not-found.html"), 404 530 531return flask.send_from_directory(config.REPOS_PATH, os.path.join(username, repository, subpath)) 532 533 534@repositories.route("/<username>/<repository>/tree/", defaults={"branch": None, "subpath": ""}) 535@repositories.route("/<username>/<repository>/tree/<branch>/", defaults={"subpath": ""}) 536@repositories.route("/<username>/<repository>/tree/<branch>/<path:subpath>") 537def repositoryTree(username, repository, branch, subpath): 538if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 539repository) is not None): 540flask.abort(403) 541 542serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) 543 544app.logger.info(f"Loading {serverRepoLocation}") 545 546if not os.path.exists(serverRepoLocation): 547app.logger.error(f"Cannot load {serverRepoLocation}") 548return flask.render_template("not-found.html"), 404 549 550repo = git.Repo(serverRepoLocation) 551repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 552if not repoData.defaultBranch: 553if repo.heads: 554repoData.defaultBranch = repo.heads[0].name 555else: 556return flask.render_template("empty.html", 557remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200 558if not branch: 559branch = repoData.defaultBranch 560return flask.redirect(f"./{branch}", code=302) 561 562if branch.startswith("tag:"): 563ref = f"tags/{branch[4:]}" 564elif branch.startswith("~"): 565ref = branch[1:] 566else: 567ref = f"heads/{branch}" 568 569ref = ref.replace("~", "/") # encode slashes for URL support 570 571try: 572repo.git.checkout("-f", ref) 573except git.exc.GitCommandError: 574return flask.render_template("not-found.html"), 404 575 576branches = repo.heads 577 578allRefs = [] 579for ref in repo.heads: 580allRefs.append((ref, "head")) 581for ref in repo.tags: 582allRefs.append((ref, "tag")) 583 584if os.path.isdir(os.path.join(serverRepoLocation, subpath)): 585files = [] 586blobs = [] 587 588for entry in os.listdir(os.path.join(serverRepoLocation, subpath)): 589if not os.path.basename(entry) == ".git": 590files.append(os.path.join(subpath, entry)) 591 592infos = [] 593 594for file in files: 595path = os.path.join(serverRepoLocation, file) 596mimetype = guessMIME(path) 597 598text = gitCommand(serverRepoLocation, None, "log", "--format='%H\n'", file).decode() 599 600sha = text.split("\n")[0] 601identifier = f"/{username}/{repository}/{sha}" 602lastCommit = Commit.query.filter_by(identifier=identifier).first() 603 604info = { 605"name": os.path.basename(file), 606"serverPath": path, 607"relativePath": file, 608"link": os.path.join(f"/{username}/{repository}/tree/{branch}/", file), 609"size": humanSize(os.path.getsize(path)), 610"mimetype": f"{mimetype}{f' ({mimetypes.guess_type(path)[1]})' if mimetypes.guess_type(path)[1] else ''}", 611"commit": lastCommit, 612"shaSize": 7, 613} 614 615specialIcon = config.matchIcon(os.path.basename(file)) 616if specialIcon: 617info["icon"] = specialIcon 618elif os.path.isdir(path): 619info["icon"] = config.folderIcon 620elif mimetypes.guess_type(path)[0] in config.fileIcons: 621info["icon"] = config.fileIcons[mimetypes.guess_type(path)[0]] 622else: 623info["icon"] = config.unknownIcon 624 625if os.path.isdir(path): 626infos.insert(0, info) 627else: 628infos.append(info) 629 630return flask.render_template( 631"repo-tree.html", 632username=username, 633repository=repository, 634files=infos, 635subpath=os.path.join("/", subpath), 636branches=allRefs, 637current=branch, 638remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 639isFavourite=getFavourite(flask.session.get("username"), username, repository) 640) 641else: 642path = os.path.join(serverRepoLocation, subpath) 643 644if not os.path.exists(path): 645return flask.render_template("not-found.html"), 404 646 647mimetype = guessMIME(path) 648mode = mimetype.split("/", 1)[0] 649size = humanSize(os.path.getsize(path)) 650 651specialIcon = config.matchIcon(os.path.basename(path)) 652if specialIcon: 653icon = specialIcon 654elif os.path.isdir(path): 655icon = config.folderIcon 656elif mimetypes.guess_type(path)[0] in config.fileIcons: 657icon = config.fileIcons[mimetypes.guess_type(path)[0]] 658else: 659icon = config.unknownIcon 660 661contents = None 662if mode == "text": 663contents = convertToHTML(path) 664 665return flask.render_template( 666"repo-file.html", 667username=username, 668repository=repository, 669file=os.path.join(f"/{username}/{repository}/raw/{branch}/", subpath), 670branches=allRefs, 671current=branch, 672mode=mode, 673mimetype=mimetype, 674detailedtype=magic.from_file(path), 675size=size, 676icon=icon, 677subpath=os.path.join("/", subpath), 678basename=os.path.basename(path), 679contents=contents, 680remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 681isFavourite=getFavourite(flask.session.get("username"), username, repository) 682) 683 684 685@repositories.route("/<username>/<repository>/forum/") 686def repositoryForum(username, repository): 687if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 688repository) is not None): 689flask.abort(403) 690 691serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) 692 693app.logger.info(f"Loading {serverRepoLocation}") 694 695if not os.path.exists(serverRepoLocation): 696app.logger.error(f"Cannot load {serverRepoLocation}") 697return flask.render_template("not-found.html"), 404 698 699repo = git.Repo(serverRepoLocation) 700repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 701user = User.query.filter_by(username=flask.session.get("username")).first() 702relationships = RepoAccess.query.filter_by(repo=repoData) 703userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first() 704 705return flask.render_template( 706"repo-forum.html", 707username=username, 708repository=repository, 709repoData=repoData, 710relationships=relationships, 711repo=repo, 712userRelationship=userRelationship, 713Post=Post, 714remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 715isFavourite=getFavourite(flask.session.get("username"), username, repository) 716) 717 718 719@repositories.route("/<username>/<repository>/forum/new", methods=["POST"]) 720def repositoryForumAdd(username, repository): 721if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 722repository) is not None): 723flask.abort(403) 724 725serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) 726 727app.logger.info(f"Loading {serverRepoLocation}") 728 729if not os.path.exists(serverRepoLocation): 730app.logger.error(f"Cannot load {serverRepoLocation}") 731return flask.render_template("not-found.html"), 404 732 733repo = git.Repo(serverRepoLocation) 734repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 735user = User.query.filter_by(username=flask.session.get("username")).first() 736relationships = RepoAccess.query.filter_by(repo=repoData) 737userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first() 738 739post = Post(user, repoData, None, flask.request.form["subject"], flask.request.form["message"]) 740 741db.session.add(post) 742db.session.commit() 743 744return flask.redirect(flask.url_for(".repositoryForumThread", username=username, repository=repository, postID=post.number), code=303) 745 746 747@repositories.route("/<username>/<repository>/forum/<int:postID>") 748def repositoryForumThread(username, repository, postID): 749if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 750repository) is not None): 751flask.abort(403) 752 753serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) 754 755app.logger.info(f"Loading {serverRepoLocation}") 756 757if not os.path.exists(serverRepoLocation): 758app.logger.error(f"Cannot load {serverRepoLocation}") 759return flask.render_template("not-found.html"), 404 760 761repo = git.Repo(serverRepoLocation) 762repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 763user = User.query.filter_by(username=flask.session.get("username")).first() 764relationships = RepoAccess.query.filter_by(repo=repoData) 765userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first() 766 767return flask.render_template( 768"repo-forum-thread.html", 769username=username, 770repository=repository, 771repoData=repoData, 772relationships=relationships, 773repo=repo, 774userRelationship=userRelationship, 775Post=Post, 776postID=postID, 777maxPostNesting=4, 778remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 779isFavourite=getFavourite(flask.session.get("username"), username, repository) 780) 781 782 783@repositories.route("/<username>/<repository>/forum/<int:postID>/reply", methods=["POST"]) 784def repositoryForumReply(username, repository, postID): 785if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 786repository) is not None): 787flask.abort(403) 788 789serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) 790 791app.logger.info(f"Loading {serverRepoLocation}") 792 793if not os.path.exists(serverRepoLocation): 794app.logger.error(f"Cannot load {serverRepoLocation}") 795return flask.render_template("not-found.html"), 404 796 797repo = git.Repo(serverRepoLocation) 798repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 799user = User.query.filter_by(username=flask.session.get("username")).first() 800relationships = RepoAccess.query.filter_by(repo=repoData) 801userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first() 802if not user: 803flask.abort(401) 804 805parent = Post.query.filter_by(identifier=f"/{username}/{repository}/{postID}").first() 806post = Post(user, repoData, parent, flask.request.form["subject"], flask.request.form["message"]) 807 808db.session.add(post) 809post.updateDate() 810db.session.commit() 811 812return flask.redirect(flask.url_for(".repositoryForumThread", username=username, repository=repository, postID=postID), code=303) 813 814 815@app.route("/<username>/<repository>/forum/<int:postID>/voteup", defaults={"score": 1}) 816@app.route("/<username>/<repository>/forum/<int:postID>/votedown", defaults={"score": -1}) 817@app.route("/<username>/<repository>/forum/<int:postID>/votes", defaults={"score": 0}) 818def repositoryForumVote(username, repository, postID, score): 819if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 820repository) is not None): 821flask.abort(403) 822 823serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) 824 825app.logger.info(f"Loading {serverRepoLocation}") 826 827if not os.path.exists(serverRepoLocation): 828app.logger.error(f"Cannot load {serverRepoLocation}") 829return flask.render_template("not-found.html"), 404 830 831repo = git.Repo(serverRepoLocation) 832repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 833user = User.query.filter_by(username=flask.session.get("username")).first() 834relationships = RepoAccess.query.filter_by(repo=repoData) 835userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first() 836if not user: 837flask.abort(401) 838 839post = Post.query.filter_by(identifier=f"/{username}/{repository}/{postID}").first() 840 841if score: 842oldRelationship = PostVote.query.filter_by(userUsername=user.username, postIdentifier=post.identifier).first() 843if oldRelationship: 844if score == oldRelationship.voteScore: 845db.session.delete(oldRelationship) 846post.voteSum -= oldRelationship.voteScore 847else: 848post.voteSum -= oldRelationship.voteScore 849post.voteSum += score 850oldRelationship.voteScore = score 851else: 852relationship = PostVote(user, post, score) 853post.voteSum += score 854db.session.add(relationship) 855 856db.session.commit() 857 858userVote = PostVote.query.filter_by(userUsername=user.username, postIdentifier=post.identifier).first() 859response = flask.make_response(str(post.voteSum) + " " + str(userVote.voteScore if userVote else 0)) 860response.content_type = "text/plain" 861 862return response 863 864 865@app.route("/<username>/<repository>/favourite") 866def repositoryFavourite(username, repository): 867if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 868repository) is not None): 869flask.abort(403) 870 871serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) 872 873app.logger.info(f"Loading {serverRepoLocation}") 874 875if not os.path.exists(serverRepoLocation): 876app.logger.error(f"Cannot load {serverRepoLocation}") 877return flask.render_template("not-found.html"), 404 878 879repo = git.Repo(serverRepoLocation) 880repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 881user = User.query.filter_by(username=flask.session.get("username")).first() 882relationships = RepoAccess.query.filter_by(repo=repoData) 883userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first() 884if not user: 885flask.abort(401) 886 887oldRelationship = RepoFavourite.query.filter_by(userUsername=user.username, repoRoute=repoData.route).first() 888if oldRelationship: 889db.session.delete(oldRelationship) 890else: 891relationship = RepoFavourite(user, repoData) 892db.session.add(relationship) 893 894db.session.commit() 895 896return flask.redirect(flask.url_for("favourites"), code=303) 897 898 899@repositories.route("/<username>/<repository>/users/", methods=["GET", "POST"]) 900def repositoryUsers(username, repository): 901if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 902repository) is not None): 903flask.abort(403) 904 905serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) 906 907app.logger.info(f"Loading {serverRepoLocation}") 908 909if not os.path.exists(serverRepoLocation): 910app.logger.error(f"Cannot load {serverRepoLocation}") 911return flask.render_template("not-found.html"), 404 912 913repo = git.Repo(serverRepoLocation) 914repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 915user = User.query.filter_by(username=flask.session.get("username")).first() 916relationships = RepoAccess.query.filter_by(repo=repoData) 917userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first() 918 919if flask.request.method == "GET": 920return flask.render_template( 921"repo-users.html", 922username=username, 923repository=repository, 924repoData=repoData, 925relationships=relationships, 926repo=repo, 927userRelationship=userRelationship, 928remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 929isFavourite=getFavourite(flask.session.get("username"), username, repository) 930) 931else: 932if getPermissionLevel(flask.session.get("username"), username, repository) != 2: 933flask.abort(401) 934 935if flask.request.form.get("new-username"): 936# Create new relationship 937newUser = User.query.filter_by(username=flask.request.form.get("new-username")).first() 938relationship = RepoAccess(newUser, repoData, flask.request.form.get("new-level")) 939db.session.add(relationship) 940db.session.commit() 941if flask.request.form.get("update-username"): 942# Create new relationship 943updatedUser = User.query.filter_by(username=flask.request.form.get("update-username")).first() 944relationship = RepoAccess.query.filter_by(repo=repoData, user=updatedUser).first() 945if flask.request.form.get("update-level") == -1: 946relationship.delete() 947else: 948relationship.accessLevel = flask.request.form.get("update-level") 949db.session.commit() 950 951return flask.redirect(app.url_for(".repositoryUsers", username=username, repository=repository)) 952 953 954@repositories.route("/<username>/<repository>/branches/") 955def repositoryBranches(username, repository): 956if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 957repository) is not None): 958flask.abort(403) 959 960serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) 961 962app.logger.info(f"Loading {serverRepoLocation}") 963 964if not os.path.exists(serverRepoLocation): 965app.logger.error(f"Cannot load {serverRepoLocation}") 966return flask.render_template("not-found.html"), 404 967 968repo = git.Repo(serverRepoLocation) 969repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 970 971return flask.render_template( 972"repo-branches.html", 973username=username, 974repository=repository, 975repoData=repoData, 976repo=repo, 977remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 978isFavourite=getFavourite(flask.session.get("username"), username, repository) 979) 980 981 982@repositories.route("/<username>/<repository>/log/", defaults={"branch": None}) 983@repositories.route("/<username>/<repository>/log/<branch>/") 984def repositoryLog(username, repository, branch): 985if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username, 986repository) is not None): 987flask.abort(403) 988 989serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) 990 991app.logger.info(f"Loading {serverRepoLocation}") 992 993if not os.path.exists(serverRepoLocation): 994app.logger.error(f"Cannot load {serverRepoLocation}") 995return flask.render_template("not-found.html"), 404 996 997repo = git.Repo(serverRepoLocation) 998repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 999if not repoData.defaultBranch: 1000if repo.heads: 1001repoData.defaultBranch = repo.heads[0].name 1002else: 1003return flask.render_template("empty.html", 1004remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200 1005if not branch: 1006branch = repoData.defaultBranch 1007return flask.redirect(f"./{branch}", code=302) 1008 1009if branch.startswith("tag:"): 1010ref = f"tags/{branch[4:]}" 1011elif branch.startswith("~"): 1012ref = branch[1:] 1013else: 1014ref = f"heads/{branch}" 1015 1016ref = ref.replace("~", "/") # encode slashes for URL support 1017 1018try: 1019repo.git.checkout("-f", ref) 1020except git.exc.GitCommandError: 1021return flask.render_template("not-found.html"), 404 1022 1023branches = repo.heads 1024 1025allRefs = [] 1026for ref in repo.heads: 1027allRefs.append((ref, "head")) 1028for ref in repo.tags: 1029allRefs.append((ref, "tag")) 1030 1031commitList = [f"/{username}/{repository}/{sha}" for sha in gitCommand(serverRepoLocation, None, "log", "--format='%H'").decode().split("\n")] 1032 1033commits = Commit.query.filter(Commit.identifier.in_(commitList)) 1034 1035return flask.render_template( 1036"repo-log.html", 1037username=username, 1038repository=repository, 1039branches=allRefs, 1040current=branch, 1041repoData=repoData, 1042repo=repo, 1043commits=commits, 1044remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 1045isFavourite=getFavourite(flask.session.get("username"), username, repository) 1046) 1047 1048 1049@repositories.route("/<username>/<repository>/settings/") 1050def repositorySettings(username, repository): 1051if getPermissionLevel(flask.session.get("username"), username, repository) != 2: 1052flask.abort(401) 1053 1054return flask.render_template("repo-settings.html", username=username, repository=repository) 1055 1056 1057@app.errorhandler(404) 1058def e404(error): 1059return flask.render_template("not-found.html"), 404 1060 1061 1062@app.errorhandler(401) 1063def e401(error): 1064return flask.render_template("unauthorised.html"), 401 1065 1066 1067@app.errorhandler(403) 1068def e403(error): 1069return flask.render_template("forbidden.html"), 403 1070 1071 1072@app.errorhandler(418) 1073def e418(error): 1074return flask.render_template("teapot.html"), 418 1075 1076 1077@app.errorhandler(405) 1078def e405(error): 1079return flask.render_template("method-not-allowed.html"), 405 1080 1081 1082if __name__ == "__main__": 1083app.run(debug=True, port=8080, host="0.0.0.0") 1084 1085 1086app.register_blueprint(repositories) 1087