app.py
Python script, ASCII text executable
1import os 2import shutil 3import random 4import subprocess 5import platform 6import git 7import mimetypes 8import magic 9import flask 10import cairosvg 11from functools import wraps 12from datetime import datetime 13from enum import Enum 14from cairosvg import svg2png 15from flask_sqlalchemy import SQLAlchemy 16from flask_bcrypt import Bcrypt 17from markupsafe import escape, Markup 18from flask_migrate import Migrate 19from PIL import Image 20from flask_httpauth import HTTPBasicAuth 21from celery import Celery, Task 22import config 23import celery_integration 24 25app = flask.Flask(__name__) 26app.config.from_mapping( 27CELERY=dict( 28broker_url=config.REDIS_URI, 29result_backend=config.REDIS_URI, 30task_ignore_result=True, 31), 32) 33worker = celery_integration.init_celery_app(app) 34 35auth = HTTPBasicAuth() 36 37app.config["SQLALCHEMY_DATABASE_URI"] = config.DB_URI 38app.config["SECRET_KEY"] = config.DB_PASSWORD 39app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False 40db = SQLAlchemy(app) 41bcrypt = Bcrypt(app) 42migrate = Migrate(app, db) 43from models import * 44 45import celery_tasks 46 47 48def git_command(repo, data, *args): 49if not os.path.isdir(repo): 50raise FileNotFoundError("Repo not found") 51env = os.environ.copy() 52 53command = ["git", *args] 54 55proc = subprocess.Popen(" ".join(command), cwd=repo, env=env, shell=True, stdout=subprocess.PIPE, 56stdin=subprocess.PIPE) 57print(command) 58 59if data: 60proc.stdin.write(data) 61 62out, err = proc.communicate() 63return out 64 65 66def only_chars(string, chars): 67for i in string: 68if i not in chars: 69return False 70return True 71 72 73def get_permission_level(logged_in, username, repository): 74user = User.query.filter_by(username=logged_in).first() 75repo = Repo.query.filter_by(route=f"/{username}/{repository}").first() 76 77if user and repo: 78permission = RepoAccess.query.filter_by(user=user, repo=repo).first() 79if permission: 80return permission.access_level 81 82return None 83 84 85def get_visibility(username, repository): 86repo = Repo.query.filter_by(route=f"/{username}/{repository}").first() 87 88if repo: 89return repo.visibility 90 91return None 92 93 94def get_favourite(logged_in, username, repository): 95print(logged_in, username, repository) 96relationship = RepoFavourite.query.filter_by(user_username=logged_in, 97repo_route=f"/{username}/{repository}").first() 98return relationship 99 100 101import git_http 102import jinja_utils 103 104 105def human_size(value, decimals=2, scale=1024, 106units=("B", "kiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB", "RiB", "QiB")): 107for unit in units: 108if value < scale: 109break 110value /= scale 111if int(value) == value: 112# do not return decimals if the value is already round 113return int(value), unit 114return round(value * 10 ** decimals) / 10 ** decimals, unit 115 116 117def guess_mime(path): 118if os.path.isdir(path): 119mimetype = "inode/directory" 120elif magic.from_file(path, mime=True): 121mimetype = magic.from_file(path, mime=True) 122else: 123mimetype = "application/octet-stream" 124return mimetype 125 126 127def convert_to_html(path): 128with open(path, "r") as f: 129contents = f.read() 130return contents 131 132 133repositories = flask.Blueprint("repository", __name__, template_folder="templates/repository/") 134 135 136@app.context_processor 137def default(): 138username = flask.session.get("username") 139 140user_object = User.query.filter_by(username=username).first() 141 142return { 143"logged_in_user": username, 144"user_object": user_object, 145"Notification": Notification, 146"unread": UserNotification.query.filter_by(user_username=username).filter( 147UserNotification.attention_level > 0).count() 148} 149 150 151@app.route("/") 152def main(): 153return flask.render_template("home.html") 154 155 156@app.route("/about/") 157def about(): 158return flask.render_template("about.html", platform=platform) 159 160 161@app.route("/settings/", methods=["GET", "POST"]) 162def settings(): 163if not flask.session.get("username"): 164flask.abort(401) 165if flask.request.method == "GET": 166user = User.query.filter_by(username=flask.session.get("username")).first() 167 168return flask.render_template("user-settings.html", user=user) 169else: 170user = User.query.filter_by(username=flask.session.get("username")).first() 171 172user.display_name = flask.request.form["displayname"] 173user.URL = flask.request.form["url"] 174user.company = flask.request.form["company"] 175user.company_URL = flask.request.form["companyurl"] 176user.location = flask.request.form["location"] 177user.show_mail = True if flask.request.form.get("showmail") else False 178user.bio = flask.request.form.get("bio") 179 180db.session.commit() 181 182flask.flash(Markup("<iconify-icon icon='mdi:check'></iconify-icon>Settings saved"), category="success") 183return flask.redirect(f"/{flask.session.get('username')}", code=303) 184 185 186@app.route("/favourites/", methods=["GET", "POST"]) 187def favourites(): 188if not flask.session.get("username"): 189flask.abort(401) 190if flask.request.method == "GET": 191relationships = RepoFavourite.query.filter_by(user_username=flask.session.get("username")) 192 193return flask.render_template("favourites.html", favourites=relationships) 194 195 196@app.route("/notifications/", methods=["GET", "POST"]) 197def notifications(): 198if not flask.session.get("username"): 199flask.abort(401) 200if flask.request.method == "GET": 201return flask.render_template("notifications.html", notifications=UserNotification.query.filter_by( 202user_username=flask.session.get("username"))) 203 204 205@app.route("/accounts/", methods=["GET", "POST"]) 206def login(): 207if flask.request.method == "GET": 208return flask.render_template("login.html") 209else: 210if "login" in flask.request.form: 211username = flask.request.form["username"] 212password = flask.request.form["password"] 213 214user = User.query.filter_by(username=username).first() 215 216if user and bcrypt.check_password_hash(user.password_hashed, password): 217flask.session["username"] = user.username 218flask.flash( 219Markup(f"<iconify-icon icon='mdi:account'></iconify-icon>Successfully logged in as {username}"), 220category="success") 221return flask.redirect("/", code=303) 222elif not user: 223flask.flash(Markup("<iconify-icon icon='mdi:account-question'></iconify-icon>User not found"), 224category="alert") 225return flask.render_template("login.html") 226else: 227flask.flash(Markup("<iconify-icon icon='mdi:account-question'></iconify-icon>Invalid password"), 228category="error") 229return flask.render_template("login.html") 230if "signup" in flask.request.form: 231username = flask.request.form["username"] 232password = flask.request.form["password"] 233password2 = flask.request.form["password2"] 234email = flask.request.form.get("email") 235email2 = flask.request.form.get("email2") # repeat email is a honeypot 236name = flask.request.form.get("name") 237 238if not only_chars(username, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_"): 239flask.flash(Markup( 240"<iconify-icon icon='mdi:account-error'></iconify-icon>Usernames may only contain Latin alphabet, numbers, '-' and '_'"), 241category="error") 242return flask.render_template("login.html") 243 244if username in config.RESERVED_NAMES: 245flask.flash( 246Markup( 247f"<iconify-icon icon='mdi:account-error'></iconify-icon>Sorry, {username} is a system path"), 248category="error") 249return flask.render_template("login.html") 250 251user_check = User.query.filter_by(username=username).first() 252if user_check: 253flask.flash( 254Markup( 255f"<iconify-icon icon='mdi:account-error'></iconify-icon>The username {username} is taken"), 256category="error") 257return flask.render_template("login.html") 258 259if password2 != password: 260flask.flash(Markup("<iconify-icon icon='mdi:key-alert'></iconify-icon>Make sure the passwords match"), 261category="error") 262return flask.render_template("login.html") 263 264user = User(username, password, email, name) 265db.session.add(user) 266db.session.commit() 267flask.session["username"] = user.username 268flask.flash(Markup( 269f"<iconify-icon icon='mdi:account'></iconify-icon>Successfully created and logged in as {username}"), 270category="success") 271return flask.redirect("/", code=303) 272 273 274@app.route("/newrepo/", methods=["GET", "POST"]) 275def new_repo(): 276if not flask.session.get("username"): 277flask.abort(401) 278if flask.request.method == "GET": 279return flask.render_template("new-repo.html") 280else: 281name = flask.request.form["name"] 282visibility = int(flask.request.form["visibility"]) 283 284if not only_chars(name, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_"): 285flask.flash(Markup( 286"<iconify-icon icon='mdi:error'></iconify-icon>Repository names may only contain Latin alphabet, numbers, '-' and '_'"), 287category="error") 288return flask.render_template("new-repo.html") 289 290user = User.query.filter_by(username=flask.session.get("username")).first() 291 292repo = Repo(user, name, visibility) 293db.session.add(repo) 294db.session.commit() 295 296if not os.path.exists(os.path.join(config.REPOS_PATH, repo.route)): 297subprocess.run(["git", "init", repo.name], 298cwd=os.path.join(config.REPOS_PATH, flask.session.get("username"))) 299 300flask.flash(Markup(f"<iconify-icon icon='mdi:folder'></iconify-icon>Successfully created repository {name}"), 301category="success") 302return flask.redirect(repo.route, code=303) 303 304 305@app.route("/logout") 306def logout(): 307flask.session.clear() 308flask.flash(Markup(f"<iconify-icon icon='mdi:account'></iconify-icon>Successfully logged out"), category="info") 309return flask.redirect("/", code=303) 310 311 312@app.route("/<username>/", methods=["GET", "POST"]) 313def user_profile(username): 314old_relationship = UserFollow.query.filter_by(followerUsername=flask.session.get("username"), 315followedUsername=username).first() 316if flask.request.method == "GET": 317user = User.query.filter_by(username=username).first() 318match flask.request.args.get("action"): 319case "repositories": 320repos = Repo.query.filter_by(ownerName=username, visibility=2) 321return flask.render_template("user-profile-repositories.html", user=user, repos=repos, 322relationship=old_relationship) 323case "followers": 324return flask.render_template("user-profile-followers.html", user=user, relationship=old_relationship) 325case "follows": 326return flask.render_template("user-profile-follows.html", user=user, relationship=old_relationship) 327case _: 328return flask.render_template("user-profile-overview.html", user=user, relationship=old_relationship) 329 330elif flask.request.method == "POST": 331match flask.request.args.get("action"): 332case "follow": 333if username == flask.session.get("username"): 334flask.abort(403) 335if old_relationship: 336db.session.delete(old_relationship) 337else: 338relationship = UserFollow( 339flask.session.get("username"), 340username 341) 342db.session.add(relationship) 343db.session.commit() 344 345user = db.session.get(User, username) 346author = db.session.get(User, flask.session.get("username")) 347notification = Notification({"type": "update", "version": "0.0.0"}) 348db.session.add(notification) 349db.session.commit() 350 351result = celery_tasks.send_notification.delay(notification.id, [username], 1) 352flask.flash(f"Sending notification in task {result.id}", "success") 353 354db.session.commit() 355return flask.redirect("?", code=303) 356 357 358@app.route("/<username>/<repository>/") 359def repository_index(username, repository): 360return flask.redirect("./tree", code=302) 361 362 363@app.route("/info/<username>/avatar") 364def user_avatar(username): 365serverUserdataLocation = os.path.join(config.USERDATA_PATH, username) 366 367if not os.path.exists(serverUserdataLocation): 368return flask.render_template("not-found.html"), 404 369 370return flask.send_from_directory(serverUserdataLocation, "avatar.png") 371 372 373@app.route("/<username>/<repository>/raw/<branch>/<path:subpath>") 374def repository_raw(username, repository, branch, subpath): 375if not (get_visibility(username, repository) or get_permission_level(flask.session.get("username"), username, 376repository) is not None): 377flask.abort(403) 378 379serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository) 380 381app.logger.info(f"Loading {serverRepoLocation}") 382 383if not os.path.exists(serverRepoLocation): 384app.logger.error(f"Cannot load {serverRepoLocation}") 385return flask.render_template("not-found.html"), 404 386 387repo = git.Repo(serverRepoLocation) 388repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 389if not repoData.defaultBranch: 390if repo.heads: 391repoData.defaultBranch = repo.heads[0].name 392else: 393return flask.render_template("empty.html", 394remote=f"http://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200 395if not branch: 396branch = repoData.defaultBranch 397return flask.redirect(f"./{branch}", code=302) 398 399if branch.startswith("tag:"): 400ref = f"tags/{branch[4:]}" 401elif branch.startswith("~"): 402ref = branch[1:] 403else: 404ref = f"heads/{branch}" 405 406ref = ref.replace("~", "/") # encode slashes for URL support 407 408try: 409repo.git.checkout("-f", ref) 410except git.exc.GitCommandError: 411return flask.render_template("not-found.html"), 404 412 413return flask.send_from_directory(config.REPOS_PATH, os.path.join(username, repository, subpath)) 414 415 416@repositories.route("/<username>/<repository>/tree/", defaults={"branch": None, "subpath": ""}) 417@repositories.route("/<username>/<repository>/tree/<branch>/", defaults={"subpath": ""}) 418@repositories.route("/<username>/<repository>/tree/<branch>/<path:subpath>") 419def repository_tree(username, repository, branch, subpath): 420if not (get_visibility(username, repository) or get_permission_level(flask.session.get("username"), username, 421repository) is not None): 422flask.abort(403) 423 424server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 425 426app.logger.info(f"Loading {server_repo_location}") 427 428if not os.path.exists(server_repo_location): 429app.logger.error(f"Cannot load {server_repo_location}") 430return flask.render_template("not-found.html"), 404 431 432repo = git.Repo(server_repo_location) 433repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 434if not repo_data.defaultBranch: 435if repo.heads: 436repo_data.defaultBranch = repo.heads[0].name 437else: 438return flask.render_template("empty.html", 439remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200 440if not branch: 441branch = repo_data.defaultBranch 442return flask.redirect(f"./{branch}", code=302) 443 444if branch.startswith("tag:"): 445ref = f"tags/{branch[4:]}" 446elif branch.startswith("~"): 447ref = branch[1:] 448else: 449ref = f"heads/{branch}" 450 451ref = ref.replace("~", "/") # encode slashes for URL support 452 453try: 454repo.git.checkout("-f", ref) 455except git.exc.GitCommandError: 456return flask.render_template("not-found.html"), 404 457 458branches = repo.heads 459 460all_refs = [] 461for ref in repo.heads: 462all_refs.append((ref, "head")) 463for ref in repo.tags: 464all_refs.append((ref, "tag")) 465 466if os.path.isdir(os.path.join(server_repo_location, subpath)): 467files = [] 468blobs = [] 469 470for entry in os.listdir(os.path.join(server_repo_location, subpath)): 471if not os.path.basename(entry) == ".git": 472files.append(os.path.join(subpath, entry)) 473 474infos = [] 475 476for file in files: 477path = os.path.join(server_repo_location, file) 478mimetype = guess_mime(path) 479 480text = git_command(server_repo_location, None, "log", "--format='%H\n'", file).decode() 481 482sha = text.split("\n")[0] 483identifier = f"/{username}/{repository}/{sha}" 484last_commit = Commit.query.filter_by(identifier=identifier).first() 485 486info = { 487"name": os.path.basename(file), 488"serverPath": path, 489"relativePath": file, 490"link": os.path.join(f"/{username}/{repository}/tree/{branch}/", file), 491"size": human_size(os.path.getsize(path)), 492"mimetype": f"{mimetype}{f' ({mimetypes.guess_type(path)[1]})' if mimetypes.guess_type(path)[1] else ''}", 493"commit": last_commit, 494"shaSize": 7, 495} 496 497special_icon = config.matchIcon(os.path.basename(file)) 498if special_icon: 499info["icon"] = special_icon 500elif os.path.isdir(path): 501info["icon"] = config.folderIcon 502elif mimetypes.guess_type(path)[0] in config.fileIcons: 503info["icon"] = config.fileIcons[mimetypes.guess_type(path)[0]] 504else: 505info["icon"] = config.unknownIcon 506 507if os.path.isdir(path): 508infos.insert(0, info) 509else: 510infos.append(info) 511 512return flask.render_template( 513"repo-tree.html", 514username=username, 515repository=repository, 516files=infos, 517subpath=os.path.join("/", subpath), 518branches=all_refs, 519current=branch, 520remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 521is_favourite=get_favourite(flask.session.get("username"), username, repository) 522) 523else: 524path = os.path.join(server_repo_location, subpath) 525 526if not os.path.exists(path): 527return flask.render_template("not-found.html"), 404 528 529mimetype = guess_mime(path) 530mode = mimetype.split("/", 1)[0] 531size = human_size(os.path.getsize(path)) 532 533special_icon = config.matchIcon(os.path.basename(path)) 534if special_icon: 535icon = special_icon 536elif os.path.isdir(path): 537icon = config.folderIcon 538elif mimetypes.guess_type(path)[0] in config.fileIcons: 539icon = config.fileIcons[mimetypes.guess_type(path)[0]] 540else: 541icon = config.unknownIcon 542 543contents = None 544if mode == "text": 545contents = convert_to_html(path) 546 547return flask.render_template( 548"repo-file.html", 549username=username, 550repository=repository, 551file=os.path.join(f"/{username}/{repository}/raw/{branch}/", subpath), 552branches=all_refs, 553current=branch, 554mode=mode, 555mimetype=mimetype, 556detailedtype=magic.from_file(path), 557size=size, 558icon=icon, 559subpath=os.path.join("/", subpath), 560basename=os.path.basename(path), 561contents=contents, 562remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 563is_favourite=get_favourite(flask.session.get("username"), username, repository) 564) 565 566 567@repositories.route("/<username>/<repository>/forum/") 568def repository_forum(username, repository): 569if not (get_visibility(username, repository) or get_permission_level(flask.session.get("username"), username, 570repository) is not None): 571flask.abort(403) 572 573server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 574 575app.logger.info(f"Loading {server_repo_location}") 576 577if not os.path.exists(server_repo_location): 578app.logger.error(f"Cannot load {server_repo_location}") 579return flask.render_template("not-found.html"), 404 580 581repo = git.Repo(server_repo_location) 582repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 583user = User.query.filter_by(username=flask.session.get("username")).first() 584relationships = RepoAccess.query.filter_by(repo=repo_data) 585user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 586 587return flask.render_template( 588"repo-forum.html", 589username=username, 590repository=repository, 591repo_data=repo_data, 592relationships=relationships, 593repo=repo, 594user_relationship=user_relationship, 595Post=Post, 596remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 597is_favourite=get_favourite(flask.session.get("username"), username, repository), 598default_branch=repo_data.defaultBranch 599) 600 601 602@repositories.route("/<username>/<repository>/forum/topic/<int:id>") 603def repository_forum_topic(username, repository, id): 604if not (get_visibility(username, repository) or get_permission_level(flask.session.get("username"), username, 605repository) is not None): 606flask.abort(403) 607 608server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 609 610app.logger.info(f"Loading {server_repo_location}") 611 612if not os.path.exists(server_repo_location): 613app.logger.error(f"Cannot load {server_repo_location}") 614return flask.render_template("not-found.html"), 404 615 616repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 617user = User.query.filter_by(username=flask.session.get("username")).first() 618relationships = RepoAccess.query.filter_by(repo=repo_data) 619user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 620 621post = Post.query.filter_by(id=id).first() 622 623return flask.render_template( 624"repo-topic.html", 625username=username, 626repository=repository, 627repo_data=repo_data, 628relationships=relationships, 629user_relationship=user_relationship, 630post=post, 631remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 632is_favourite=get_favourite(flask.session.get("username"), username, repository), 633default_branch=repo_data.defaultBranch 634) 635 636 637@repositories.route("/<username>/<repository>/forum/create-topic", methods=["POST", "GET"]) 638def repository_forum_create_topic(username, repository): 639if not (get_visibility(username, repository) or get_permission_level(flask.session.get("username"), username, 640repository) is not None): 641flask.abort(403) 642 643server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 644 645app.logger.info(f"Loading {server_repo_location}") 646 647if not os.path.exists(server_repo_location): 648app.logger.error(f"Cannot load {server_repo_location}") 649return flask.render_template("not-found.html"), 404 650 651repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 652user = User.query.filter_by(username=flask.session.get("username")).first() 653relationships = RepoAccess.query.filter_by(repo=repo_data) 654user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 655 656if flask.request.method == "POST": 657# Check if the user has permission to create a topic 658if not user_relationship or not user_relationship.permissionCreateTopic: 659flask.abort(403) 660 661# Get form data 662title = flask.request.form.get("title") 663content = flask.request.form.get("content") 664 665# Validate form data 666if not title or not content: 667flask.flash("Title and content are required", "error") 668return flask.redirect( 669flask.url_for("repository_forum_create_topic", username=username, repository=repository)) 670 671# Create a new topic 672new_topic = Post( 673title=title, 674content=content, 675author=user, 676repo=repo_data 677) 678 679db.session.add(new_topic) 680db.session.commit() 681 682flask.flash("Topic created successfully!", "success") 683return flask.redirect( 684flask.url_for("repository_forum_topic", username=username, repository=repository, id=new_topic.id)) 685 686return flask.render_template( 687"repo-create-topic.html", 688username=username, 689repository=repository, 690repo_data=repo_data, 691relationships=relationships, 692user_relationship=user_relationship, 693remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 694is_favourite=get_favourite(flask.session.get("username"), username, repository), 695default_branch=repo_data.defaultBranch 696) 697 698 699@repositories.route("/<username>/<repository>/forum/<int:post_id>") 700def repository_forum_thread(username, repository, post_id): 701if not (get_visibility(username, repository) or get_permission_level(flask.session.get("username"), username, 702repository) is not None): 703flask.abort(403) 704 705server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 706 707app.logger.info(f"Loading {server_repo_location}") 708 709if not os.path.exists(server_repo_location): 710app.logger.error(f"Cannot load {server_repo_location}") 711return flask.render_template("not-found.html"), 404 712 713repo = git.Repo(server_repo_location) 714repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 715user = User.query.filter_by(username=flask.session.get("username")).first() 716relationships = RepoAccess.query.filter_by(repo=repo_data) 717user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 718 719return flask.render_template( 720"repo-forum-thread.html", 721username=username, 722repository=repository, 723repo_data=repo_data, 724relationships=relationships, 725repo=repo, 726user_relationship=user_relationship, 727post_id=post_id, 728max_post_nesting=4, 729remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 730is_favourite=get_favourite(flask.session.get("username"), username, repository) 731) 732 733 734@repositories.route("/<username>/<repository>/forum/<int:post_id>/reply", methods=["POST"]) 735def repository_forum_reply(username, repository, post_id): 736if not (get_visibility(username, repository) or get_permission_level(flask.session.get("username"), username, 737repository) is not None): 738flask.abort(403) 739 740server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 741 742app.logger.info(f"Loading {server_repo_location}") 743 744if not os.path.exists(server_repo_location): 745app.logger.error(f"Cannot load {server_repo_location}") 746return flask.render_template("not-found.html"), 404 747 748repo = git.Repo(server_repo_location) 749repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 750user = User.query.filter_by(username=flask.session.get("username")).first() 751relationships = RepoAccess.query.filter_by(repo=repo_data) 752user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 753if not user: 754flask.abort(401) 755 756parent = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first() 757post = Post(user, repo_data, parent, flask.request.form["subject"], flask.request.form["message"]) 758 759db.session.add(post) 760post.update_date() 761db.session.commit() 762 763return flask.redirect( 764flask.url_for(".repository_forum_thread", username=username, repository=repository, post_id=post_id), code=303) 765 766 767@repositories.route("/<username>/<repository>/forum/<int:post_id>/voteup", defaults={"score": 1}) 768@repositories.route("/<username>/<repository>/forum/<int:post_id>/votedown", defaults={"score": -1}) 769@repositories.route("/<username>/<repository>/forum/<int:post_id>/votes", defaults={"score": 0}) 770def repository_forum_vote(username, repository, post_id, score): 771if not (get_visibility(username, repository) or get_permission_level(flask.session.get("username"), username, 772repository) is not None): 773flask.abort(403) 774 775server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 776 777app.logger.info(f"Loading {server_repo_location}") 778 779if not os.path.exists(server_repo_location): 780app.logger.error(f"Cannot load {server_repo_location}") 781return flask.render_template("not-found.html"), 404 782 783repo = git.Repo(server_repo_location) 784repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 785user = User.query.filter_by(username=flask.session.get("username")).first() 786relationships = RepoAccess.query.filter_by(repo=repo_data) 787user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 788if not user: 789flask.abort(401) 790 791post = Post.query.filter_by(identifier=f"/{username}/{repository}/{post_id}").first() 792 793if score: 794old_relationship = PostVote.query.filter_by(user_username=user.username, 795post_identifier=post.identifier).first() 796if old_relationship: 797if score == old_relationship.vote_score: 798db.session.delete(old_relationship) 799post.vote_sum -= old_relationship.vote_score 800else: 801post.vote_sum -= old_relationship.vote_score 802post.vote_sum += score 803old_relationship.vote_score = score 804else: 805relationship = PostVote(user, post, score) 806post.vote_sum += score 807db.session.add(relationship) 808 809db.session.commit() 810 811user_vote = PostVote.query.filter_by(user_username=user.username, post_identifier=post.identifier).first() 812response = flask.make_response(str(post.vote_sum) + " " + str(user_vote.vote_score if user_vote else 0)) 813response.content_type = "text/plain" 814 815return response 816 817 818@repositories.route("/<username>/<repository>/favourite") 819def repository_favourite(username, repository): 820if not (get_visibility(username, repository) or get_permission_level(flask.session.get("username"), username, 821repository) is not None): 822flask.abort(403) 823 824server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 825 826app.logger.info(f"Loading {server_repo_location}") 827 828if not os.path.exists(server_repo_location): 829app.logger.error(f"Cannot load {server_repo_location}") 830return flask.render_template("not-found.html"), 404 831 832repo = git.Repo(server_repo_location) 833repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 834user = User.query.filter_by(username=flask.session.get("username")).first() 835relationships = RepoAccess.query.filter_by(repo=repo_data) 836user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 837if not user: 838flask.abort(401) 839 840old_relationship = RepoFavourite.query.filter_by(user_username=user.username, repo_route=repo_data.route).first() 841if old_relationship: 842db.session.delete(old_relationship) 843else: 844relationship = RepoFavourite(user, repo_data) 845db.session.add(relationship) 846 847db.session.commit() 848 849return flask.redirect(flask.url_for("favourites"), code=303) 850 851 852@repositories.route("/<username>/<repository>/users/", methods=["GET", "POST"]) 853def repository_users(username, repository): 854if not (get_visibility(username, repository) or get_permission_level(flask.session.get("username"), username, 855repository) is not None): 856flask.abort(403) 857 858server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 859 860app.logger.info(f"Loading {server_repo_location}") 861 862if not os.path.exists(server_repo_location): 863app.logger.error(f"Cannot load {server_repo_location}") 864return flask.render_template("not-found.html"), 404 865 866repo = git.Repo(server_repo_location) 867repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 868user = User.query.filter_by(username=flask.session.get("username")).first() 869relationships = RepoAccess.query.filter_by(repo=repo_data) 870user_relationship = RepoAccess.query.filter_by(repo=repo_data, user=user).first() 871 872if flask.request.method == "GET": 873return flask.render_template( 874"repo-users.html", 875username=username, 876repository=repository, 877repo_data=repo_data, 878relationships=relationships, 879repo=repo, 880user_relationship=user_relationship, 881remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 882is_favourite=get_favourite(flask.session.get("username"), username, repository) 883) 884else: 885if get_permission_level(flask.session.get("username"), username, repository) != 2: 886flask.abort(401) 887 888if flask.request.form.get("new-username"): 889# Create new relationship 890new_user = User.query.filter_by(username=flask.request.form.get("new-username")).first() 891relationship = RepoAccess(new_user, repo_data, flask.request.form.get("new-level")) 892db.session.add(relationship) 893db.session.commit() 894if flask.request.form.get("update-username"): 895# Create new relationship 896updated_user = User.query.filter_by(username=flask.request.form.get("update-username")).first() 897relationship = RepoAccess.query.filter_by(repo=repo_data, user=updated_user).first() 898if flask.request.form.get("update-level") == -1: 899relationship.delete() 900else: 901relationship.access_level = flask.request.form.get("update-level") 902db.session.commit() 903 904return flask.redirect(app.url_for(".repository_users", username=username, repository=repository)) 905 906 907@repositories.route("/<username>/<repository>/branches/") 908def repository_branches(username, repository): 909if not (get_visibility(username, repository) or get_permission_level(flask.session.get("username"), username, 910repository) is not None): 911flask.abort(403) 912 913server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 914 915app.logger.info(f"Loading {server_repo_location}") 916 917if not os.path.exists(server_repo_location): 918app.logger.error(f"Cannot load {server_repo_location}") 919return flask.render_template("not-found.html"), 404 920 921repo = git.Repo(server_repo_location) 922repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 923 924return flask.render_template( 925"repo-branches.html", 926username=username, 927repository=repository, 928repo_data=repo_data, 929repo=repo, 930remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 931is_favourite=get_favourite(flask.session.get("username"), username, repository) 932) 933 934 935@repositories.route("/<username>/<repository>/log/", defaults={"branch": None}) 936@repositories.route("/<username>/<repository>/log/<branch>/") 937def repository_log(username, repository, branch): 938if not (get_visibility(username, repository) or get_permission_level(flask.session.get("username"), username, 939repository) is not None): 940flask.abort(403) 941 942server_repo_location = os.path.join(config.REPOS_PATH, username, repository) 943 944app.logger.info(f"Loading {server_repo_location}") 945 946if not os.path.exists(server_repo_location): 947app.logger.error(f"Cannot load {server_repo_location}") 948return flask.render_template("not-found.html"), 404 949 950repo = git.Repo(server_repo_location) 951repo_data = Repo.query.filter_by(route=f"/{username}/{repository}").first() 952if not repo_data.default_branch: 953if repo.heads: 954repo_data.default_branch = repo.heads[0].name 955else: 956return flask.render_template("empty.html", 957remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200 958if not branch: 959branch = repo_data.default_branch 960return flask.redirect(f"./{branch}", code=302) 961 962if branch.startswith("tag:"): 963ref = f"tags/{branch[4:]}" 964elif branch.startswith("~"): 965ref = branch[1:] 966else: 967ref = f"heads/{branch}" 968 969ref = ref.replace("~", "/") # encode slashes for URL support 970 971try: 972repo.git.checkout("-f", ref) 973except git.exc.GitCommandError: 974return flask.render_template("not-found.html"), 404 975 976branches = repo.heads 977 978all_refs = [] 979for ref in repo.heads: 980all_refs.append((ref, "head")) 981for ref in repo.tags: 982all_refs.append((ref, "tag")) 983 984commit_list = [f"/{username}/{repository}/{sha}" for sha in 985git_command(server_repo_location, None, "log", "--format='%H'").decode().split("\n")] 986 987commits = Commit.query.filter(Commit.identifier.in_(commit_list)) 988 989return flask.render_template( 990"repo-log.html", 991username=username, 992repository=repository, 993branches=all_refs, 994current=branch, 995repo_data=repo_data, 996repo=repo, 997commits=commits, 998remote=f"http{'s' if config.suggestHTTPS else ''}://{config.BASE_DOMAIN}/git/{username}/{repository}", 999is_favourite=get_favourite(flask.session.get("username"), username, repository) 1000) 1001 1002 1003@repositories.route("/<username>/<repository>/settings/") 1004def repository_settings(username, repository): 1005if get_permission_level(flask.session.get("username"), username, repository) != 2: 1006flask.abort(401) 1007 1008return flask.render_template("repo-settings.html", username=username, repository=repository) 1009 1010 1011@app.errorhandler(404) 1012def e404(error): 1013return flask.render_template("not-found.html"), 404 1014 1015 1016@app.errorhandler(401) 1017def e401(error): 1018return flask.render_template("unauthorised.html"), 401 1019 1020 1021@app.errorhandler(403) 1022def e403(error): 1023return flask.render_template("forbidden.html"), 403 1024 1025 1026@app.errorhandler(418) 1027def e418(error): 1028return flask.render_template("teapot.html"), 418 1029 1030 1031@app.errorhandler(405) 1032def e405(error): 1033return flask.render_template("method-not-allowed.html"), 405 1034 1035 1036if __name__ == "__main__": 1037app.run(debug=True, port=8080, host="0.0.0.0") 1038 1039app.register_blueprint(repositories) 1040