Python script, ASCII text executable
        
            1
            import os 
        
            2
            import random 
        
            3
            import subprocess 
        
            4
            import cairosvg 
        
            6
            import flask 
        
            7
            from flask_sqlalchemy import SQLAlchemy 
        
            8
            import git 
        
            9
            import mimetypes 
        
            10
            import magic 
        
            11
            from flask_bcrypt import Bcrypt 
        
            12
            from markupsafe import escape, Markup 
        
            13
            from flask_migrate import Migrate 
        
            14
            from datetime import datetime 
        
            15
            from enum import Enum 
        
            16
            import shutil 
        
            17
            from PIL import Image 
        
            18
            from cairosvg import svg2png 
        
            19
            import platform 
        
            20
            import config 
        
            22
            app = flask.Flask(__name__) 
        
            24
            from flask_httpauth import HTTPBasicAuth 
        
            26
            auth = HTTPBasicAuth() 
        
            27
            app.config["SQLALCHEMY_DATABASE_URI"] = config.DB_URI 
        
            29
            app.config["SECRET_KEY"] = config.DB_PASSWORD 
        
            30
            app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False 
        
            31
            db = SQLAlchemy(app) 
        
            32
            bcrypt = Bcrypt(app) 
        
            33
            migrate = Migrate(app, db) 
        
            34
            def onlyChars(string, chars): 
        
            37
                for i in string: 
        
            38
                    if i not in chars: 
        
            39
                        return False 
        
            40
                return True 
        
            41
            with app.app_context(): 
        
            44
                class RepoAccess(db.Model): 
        
            45
                    id = db.Column(db.Integer, primary_key=True) 
        
            46
                    userUsername = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 
        
            47
                    repoRoute = db.Column(db.String(98), db.ForeignKey("repo.route"), nullable=False) 
        
            48
                    accessLevel = db.Column(db.SmallInteger(), nullable=False)  # 0 read-only, 1 read-write, 2 admin 
        
            49
                    user = db.relationship("User", back_populates="repoAccess") 
        
            51
                    repo = db.relationship("Repo", back_populates="repoAccess") 
        
            52
                    __table_args__ = (db.UniqueConstraint("userUsername", "repoRoute", name="_user_repo_uc"),) 
        
            54
                    def __init__(self, user, repo, level): 
        
            56
                        self.userUsername = user.username 
        
            57
                        self.repoRoute = repo.route 
        
            58
                        self.accessLevel = level 
        
            59
                class User(db.Model): 
        
            62
                    username = db.Column(db.String(32), unique=True, nullable=False, primary_key=True) 
        
            63
                    displayName = db.Column(db.Unicode(128), unique=False, nullable=True) 
        
            64
                    bio = db.Column(db.Unicode(512), unique=False, nullable=True) 
        
            65
                    passwordHashed = db.Column(db.String(60), nullable=False) 
        
            66
                    email = db.Column(db.String(254), nullable=True) 
        
            67
                    company = db.Column(db.Unicode(64), nullable=True) 
        
            68
                    companyURL = db.Column(db.String(256), nullable=True) 
        
            69
                    URL = db.Column(db.String(256), nullable=True) 
        
            70
                    showMail = db.Column(db.Boolean, default=False, nullable=False) 
        
            71
                    location = db.Column(db.Unicode(64), nullable=True) 
        
            72
                    creationDate = db.Column(db.DateTime, default=datetime.utcnow) 
        
            73
                    repositories = db.relationship("Repo", back_populates="owner") 
        
            75
                    def __init__(self, username, password, email=None, displayName=None): 
        
            77
                        self.username = username 
        
            78
                        self.passwordHashed = bcrypt.generate_password_hash(password, config.HASHING_ROUNDS).decode("utf-8") 
        
            79
                        self.email = email 
        
            80
                        self.displayName = displayName 
        
            81
                        # Create the user's directory 
        
            83
                        if not os.path.exists(os.path.join(config.REPOS_PATH, username)): 
        
            84
                            os.makedirs(os.path.join(config.REPOS_PATH, username)) 
        
            85
                        if not os.path.exists(os.path.join(config.USERDATA_PATH, username)): 
        
            86
                            os.makedirs(os.path.join(config.USERDATA_PATH, username)) 
        
            87
                        avatarName = random.choice(os.listdir(config.DEFAULT_AVATARS_PATH)) 
        
            89
                        if os.path.join(config.DEFAULT_AVATARS_PATH, avatarName).endswith(".svg"): 
        
            90
                            cairosvg.svg2png(url=os.path.join(config.DEFAULT_AVATARS_PATH, avatarName), write_to="/tmp/gitme-avatar.png") 
        
            91
                            avatar = Image.open("/tmp/roundabout-avatar.png") 
        
            92
                        else: 
        
            93
                            avatar = Image.open(os.path.join(config.DEFAULT_AVATARS_PATH, avatarName)) 
        
            94
                        avatar.thumbnail(config.AVATAR_SIZE) 
        
            95
                        avatar.save(os.path.join(config.USERDATA_PATH, username, "avatar.png")) 
        
            96
                class Repo(db.Model): 
        
            99
                    route = db.Column(db.String(98), unique=True, nullable=False, primary_key=True) 
        
            100
                    ownerName = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 
        
            101
                    name = db.Column(db.String(64), nullable=False) 
        
            102
                    owner = db.relationship("User", back_populates="repositories") 
        
            103
                    visibility = db.Column(db.SmallInteger(), nullable=False) 
        
            104
                    info = db.Column(db.Unicode(512), nullable=True) 
        
            105
                    URL = db.Column(db.String(256), nullable=True) 
        
            106
                    creationDate = db.Column(db.DateTime, default=datetime.utcnow) 
        
            107
                    defaultBranch = db.Column(db.String(64), nullable=True, default="master") 
        
            109
                    commits = db.relationship("Commit", back_populates="repo") 
        
            111
                    repoAccess = db.relationship("RepoAccess", back_populates="repo") 
        
            112
                    def __init__(self, owner, name, visibility): 
        
            114
                        self.route = f"/{owner.username}/{name}" 
        
            115
                        self.name = name 
        
            116
                        self.ownerName = owner.username 
        
            117
                        self.owner = owner 
        
            118
                        self.visibility = visibility 
        
            119
                        # Add repo access for the owner 
        
            121
                        repoAccess = RepoAccess(user=owner, repo=self, accessLevel=2) 
        
            122
                        db.session.add(repoAccess) 
        
            123
                        db.session.commit() 
        
            124
                class Commit(db.Model): 
        
            127
                    identifier = db.Column(db.String(227), unique=True, nullable=False, primary_key=True) 
        
            128
                    sha = db.Column(db.String(128), nullable=False) 
        
            129
                    repoName = db.Column(db.String(97), db.ForeignKey("repo.route"), nullable=False) 
        
            130
                    ownerName = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 
        
            131
                    ownerIdentity = db.Column(db.String(321)) 
        
            132
                    receiveDate = db.Column(db.DateTime, default=datetime.utcnow) 
        
            133
                    authorDate = db.Column(db.DateTime) 
        
            134
                    message = db.Column(db.UnicodeText) 
        
            135
                    repo = db.relationship("Repo", back_populates="commits") 
        
            136
                    def __init__(self, sha, owner, repo, date, message, ownerIdentity): 
        
            138
                        self.identifier = f"/{repo.route}/{owner.username}/{sha}" 
        
            139
                        self.sha = sha 
        
            140
                        self.repoName = repo.route 
        
            141
                        self.repo = repo 
        
            142
                        self.ownerName = owner.username 
        
            143
                        self.owner = owner 
        
            144
                        self.authorDate = datetime.fromtimestamp(int(date)) 
        
            145
                        self.message = message 
        
            146
                        self.ownerIdentity = ownerIdentity 
        
            147
            import gitHTTP 
        
            149
            def humanSize(value, decimals=2, scale=1024, units=("B", "kiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB", "RiB", "QiB")): 
        
            152
                for unit in units: 
        
            153
                    if value < scale: 
        
            154
                        break 
        
            155
                    value /= scale 
        
            156
                if int(value) == value: 
        
            157
                    # do not return decimals, if the value is already round 
        
            158
                    return int(value), unit 
        
            159
                return round(value * 10**decimals) / 10**decimals, unit 
        
            160
            def guessMIME(path): 
        
            163
                if os.path.isdir(path): 
        
            164
                    mimetype = "inode/directory" 
        
            165
                elif magic.from_file(path, mime=True): 
        
            166
                    mimetype = magic.from_file(path, mime=True) 
        
            167
                else: 
        
            168
                    mimetype = "application/octet-stream" 
        
            169
                return mimetype 
        
            170
            def convertToHTML(path): 
        
            173
                with open(path, "r") as f: 
        
            174
                    contents = f.read() 
        
            175
                return contents 
        
            176
            @app.context_processor 
        
            179
            def default(): 
        
            180
                username = flask.session.get("username") 
        
            181
                return {"loggedInUser": username} 
        
            183
            @app.route("/") 
        
            186
            def main(): 
        
            187
                return flask.render_template("home.html") 
        
            188
            @app.route("/about") 
        
            191
            def about(): 
        
            192
                return flask.render_template("about.html", platform=platform) 
        
            193
            @app.route("/settings/") 
        
            196
            def settings(): 
        
            197
                if not flask.session.get("username"): 
        
            198
                    flask.abort(401) 
        
            199
                user = User.query.filter_by(username=flask.session.get("username")).first() 
        
            200
                return flask.render_template("user-settings.html", user=user) 
        
            202
            @app.route("/accounts/", methods=["GET", "POST"]) 
        
            205
            def login(): 
        
            206
                if flask.request.method == "GET": 
        
            207
                    return flask.render_template("login.html") 
        
            208
                else: 
        
            209
                    if "login" in flask.request.form: 
        
            210
                        username = flask.request.form["username"] 
        
            211
                        password = flask.request.form["password"] 
        
            212
                        user = User.query.filter_by(username=username).first() 
        
            214
                        if user and bcrypt.check_password_hash(user.passwordHashed, password): 
        
            216
                            flask.session["username"] = user.username 
        
            217
                            flask.flash(Markup(f"<iconify-icon icon='mdi:account'></iconify-icon>Successfully logged in as {username}"), category="success") 
        
            218
                            return flask.redirect("/", code=303) 
        
            219
                        elif not user: 
        
            220
                            flask.flash(Markup("<iconify-icon icon='mdi:account-question'></iconify-icon>User not found"), category="alert") 
        
            221
                            return flask.render_template("login.html") 
        
            222
                        else: 
        
            223
                            flask.flash(Markup("<iconify-icon icon='mdi:account-question'></iconify-icon>Invalid password"), category="error") 
        
            224
                            return flask.render_template("login.html") 
        
            225
                    if "signup" in flask.request.form: 
        
            226
                        username = flask.request.form["username"] 
        
            227
                        password = flask.request.form["password"] 
        
            228
                        password2 = flask.request.form["password2"] 
        
            229
                        email = flask.request.form.get("email") 
        
            230
                        email2 = flask.request.form.get("email2")        # repeat email is a honeypot 
        
            231
                        name = flask.request.form.get("name") 
        
            232
                        if not onlyChars(username, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_"): 
        
            234
                            flask.flash(Markup("<iconify-icon icon='mdi:account-error'></iconify-icon>Usernames may only contain Latin alphabet, numbers, '-' and '_'"), category="error") 
        
            235
                            return flask.render_template("login.html") 
        
            236
                        if username in config.RESERVED_NAMES: 
        
            238
                            flask.flash(Markup(f"<iconify-icon icon='mdi:account-error'></iconify-icon>Sorry, {username} is a system path"), category="error") 
        
            239
                            return flask.render_template("login.html") 
        
            240
                        userCheck = User.query.filter_by(username=username).first() 
        
            242
                        if userCheck: 
        
            243
                            flask.flash(Markup(f"<iconify-icon icon='mdi:account-error'></iconify-icon>The username {username} is taken"), category="error") 
        
            244
                            return flask.render_template("login.html") 
        
            245
                        if password2 != password: 
        
            247
                            flask.flash(Markup("<iconify-icon icon='mdi:key-alert'></iconify-icon>Make sure the passwords match"), category="error") 
        
            248
                            return flask.render_template("login.html") 
        
            249
                        user = User(username, password, email, name) 
        
            251
                        db.session.add(user) 
        
            252
                        db.session.commit() 
        
            253
                        flask.session["username"] = user.username 
        
            254
                        flask.flash(Markup(f"<iconify-icon icon='mdi:account'></iconify-icon>Successfully created and logged in as {username}"), category="success") 
        
            255
                        return flask.redirect("/", code=303) 
        
            256
            @app.route("/newrepo/", methods=["GET", "POST"]) 
        
            259
            def newRepo(): 
        
            260
                if flask.request.method == "GET": 
        
            261
                    return flask.render_template("new-repo.html") 
        
            262
                else: 
        
            263
                    name = flask.request.form["name"] 
        
            264
                    visibility = int(flask.request.form["visibility"]) 
        
            265
                    if not onlyChars(name, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_"): 
        
            267
                        flask.flash(Markup( 
        
            268
                            "<iconify-icon icon='mdi:error'></iconify-icon>Repository names may only contain Latin alphabet, numbers, '-' and '_'"), 
        
            269
                                    category="error") 
        
            270
                        return flask.render_template("new-repo.html") 
        
            271
                    user = User.query.filter_by(username=flask.session.get("username")).first() 
        
            273
                    repo = Repo(user, name, visibility) 
        
            275
                    db.session.add(repo) 
        
            276
                    db.session.commit() 
        
            277
                    if not os.path.exists(os.path.join(config.REPOS_PATH, repo.route)): 
        
            279
                        subprocess.run(["git", "init", repo.name], cwd=os.path.join(config.REPOS_PATH, flask.session.get("username"))) 
        
            280
                    flask.flash(Markup(f"<iconify-icon icon='mdi:folder'></iconify-icon>Successfully created repository {name}"), category="success") 
        
            282
                    return flask.redirect(repo.route, code=303) 
        
            283
            @app.route("/logout") 
        
            286
            def logout(): 
        
            287
                flask.session.clear() 
        
            288
                flask.flash(Markup(f"<iconify-icon icon='mdi:account'></iconify-icon>Successfully logged out"), category="info") 
        
            289
                return flask.redirect("/", code=303) 
        
            290
            @app.route("/<username>/") 
        
            293
            def userProfile(username): 
        
            294
                user = User.query.filter_by(username=username).first() 
        
            295
                repos = Repo.query.filter_by(ownerName=username, visibility=2) 
        
            296
                return flask.render_template("user-profile.html", user=user, repos=repos) 
        
            297
            @app.route("/<username>/<repository>/") 
        
            300
            def repositoryIndex(username, repository): 
        
            301
                return flask.redirect("./tree", code=302) 
        
            302
            @app.route("/<username>/<repository>/raw/<branch>/<path:subpath>") 
        
            305
            def repositoryRaw(username, repository, branch, subpath): 
        
            306
                serverRepoLocation = os.path.join(config.REPOS_PATH, os.path.join(username, repository)) 
        
            307
                app.logger.info(f"Loading {serverRepoLocation}") 
        
            309
                if not os.path.exists(serverRepoLocation): 
        
            311
                    app.logger.error(f"Cannot load {serverRepoLocation}") 
        
            312
                    return flask.render_template("not-found.html"), 404 
        
            313
                repo = git.Repo(serverRepoLocation) 
        
            315
                try: 
        
            316
                    repo.git.checkout(branch) 
        
            317
                except git.exc.GitCommandError: 
        
            318
                    return flask.render_template("not-found.html"), 404 
        
            319
                return flask.send_from_directory(config.REPOS_PATH, os.path.join(username, repository, subpath)) 
        
            321
            @app.route("/info/<username>/avatar") 
        
            324
            def userAvatar(username): 
        
            325
                serverUserdataLocation = os.path.join(config.USERDATA_PATH, username) 
        
            326
                if not os.path.exists(serverUserdataLocation): 
        
            328
                    return flask.render_template("not-found.html"), 404 
        
            329
                return flask.send_from_directory(serverUserdataLocation, "avatar.png") 
        
            331
            @app.route("/<username>/<repository>/tree/", defaults={"branch": None, "subpath": ""}) 
        
            334
            @app.route("/<username>/<repository>/tree/<branch>/", defaults={"subpath": ""}) 
        
            335
            @app.route("/<username>/<repository>/tree/<branch>/<path:subpath>") 
        
            336
            def repositoryTree(username, repository, branch, subpath): 
        
            337
                serverRepoLocation = os.path.join(config.REPOS_PATH, os.path.join(username, repository)) 
        
            338
                app.logger.info(f"Loading {serverRepoLocation}") 
        
            340
                if not os.path.exists(serverRepoLocation): 
        
            342
                    app.logger.error(f"Cannot load {serverRepoLocation}") 
        
            343
                    return flask.render_template("not-found.html"), 404 
        
            344
                repo = git.Repo(serverRepoLocation) 
        
            346
                repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first() 
        
            347
                if not repoData.defaultBranch: 
        
            348
                    return flask.render_template("empty.html", remote=f"http://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200 
        
            349
                else: 
        
            350
                    if not branch: 
        
            351
                        branch = repoData.defaultBranch 
        
            352
                        return flask.redirect(f"./{branch}", code=302) 
        
            353
                    else: 
        
            354
                        try: 
        
            355
                            repo.git.checkout("-f", branch) 
        
            356
                        except git.exc.GitCommandError: 
        
            357
                            return flask.render_template("not-found.html"), 404 
        
            358
                branches = repo.heads 
        
            360
                if os.path.isdir(os.path.join(serverRepoLocation, subpath)): 
        
            361
                    files = [] 
        
            362
                    blobs = [] 
        
            363
                    for entry in os.listdir(os.path.join(serverRepoLocation, subpath)): 
        
            365
                        if not os.path.basename(entry) == ".git": 
        
            366
                            files.append(os.path.join(subpath, entry)) 
        
            367
                    infos = [] 
        
            369
                    for file in files: 
        
            371
                        path = os.path.join(serverRepoLocation, file) 
        
            372
                        mimetype = guessMIME(path) 
        
            373
                        info = { 
        
            375
                            "name": os.path.basename(file), 
        
            376
                            "serverPath": path, 
        
            377
                            "relativePath": file, 
        
            378
                            "link": os.path.join(f"/{username}/{repository}/tree/{branch}/", file), 
        
            379
                            "size": humanSize(os.path.getsize(path)), 
        
            380
                            "mimetype": f"{mimetype}{f' ({mimetypes.guess_type(path)[1]})' if mimetypes.guess_type(path)[1] else ''}", 
        
            381
                        } 
        
            382
                        specialIcon = config.matchIcon(os.path.basename(file)) 
        
            384
                        if specialIcon: 
        
            385
                            info["icon"] = specialIcon 
        
            386
                        elif os.path.isdir(path): 
        
            387
                            info["icon"] = config.folderIcon 
        
            388
                        elif mimetypes.guess_type(path)[0] in config.fileIcons: 
        
            389
                            info["icon"] = config.fileIcons[mimetypes.guess_type(path)[0]] 
        
            390
                        else: 
        
            391
                            info["icon"] = config.unknownIcon 
        
            392
                        if os.path.isdir(path): 
        
            394
                            infos.insert(0, info) 
        
            395
                        else: 
        
            396
                            infos.append(info) 
        
            397
                    return flask.render_template( 
        
            399
                            "repo-tree.html", 
        
            400
                            username=username, 
        
            401
                            repository=repository, 
        
            402
                            files=infos, 
        
            403
                            subpath=os.path.join("/", subpath), 
        
            404
                            branches=branches, 
        
            405
                            current=branch 
        
            406
                    ) 
        
            407
                else: 
        
            408
                    path = os.path.join(serverRepoLocation, subpath) 
        
            409
                    if not os.path.exists(path): 
        
            411
                        return flask.render_template("not-found.html"), 404 
        
            412
                    mimetype = guessMIME(path) 
        
            414
                    mode = mimetype.split("/", 1)[0] 
        
            415
                    size = humanSize(os.path.getsize(path)) 
        
            416
                    specialIcon = config.matchIcon(os.path.basename(path)) 
        
            418
                    if specialIcon: 
        
            419
                        icon = specialIcon 
        
            420
                    elif os.path.isdir(path): 
        
            421
                        icon = config.folderIcon 
        
            422
                    elif guessMIME(path)[0] in config.fileIcons: 
        
            423
                        icon = config.fileIcons[guessMIME(path)[0]] 
        
            424
                    else: 
        
            425
                        icon = config.unknownIcon 
        
            426
                    contents = None 
        
            428
                    if mode == "text": 
        
            429
                        contents = convertToHTML(path) 
        
            430
                    return flask.render_template( 
        
            432
                            "repo-file.html", 
        
            433
                            username=username, 
        
            434
                            repository=repository, 
        
            435
                            file=os.path.join(f"/{username}/{repository}/raw/{branch}/", subpath), 
        
            436
                            branches=branches, 
        
            437
                            current=branch, 
        
            438
                            mode=mode, 
        
            439
                            mimetype=mimetype, 
        
            440
                            detailedtype=magic.from_file(path), 
        
            441
                            size=size, 
        
            442
                            icon=icon, 
        
            443
                            subpath=os.path.join("/", subpath), 
        
            444
                            basename=os.path.basename(path), 
        
            445
                            contents=contents 
        
            446
                    ) 
        
            447
            @app.route("/<username>/<repository>/forum/") 
        
            450
            def repositoryForum(username, repository): 
        
            451
                return flask.render_template("repo-forum.html", username=username, repository=repository) 
        
            452
            @app.route("/<username>/<repository>/docs/") 
        
            455
            def repositoryDocs(username, repository): 
        
            456
                return flask.render_template("repo-docs.html", username=username, repository=repository) 
        
            457
            @app.route("/<username>/<repository>/releases/") 
        
            460
            def repositoryReleases(username, repository): 
        
            461
                return flask.render_template("repo-releases.html", username=username, repository=repository) 
        
            462
            @app.route("/<username>/<repository>/branches/") 
        
            465
            def repositoryBranches(username, repository): 
        
            466
                return flask.render_template("repo-branches.html", username=username, repository=repository) 
        
            467
            @app.route("/<username>/<repository>/people/") 
        
            470
            def repositoryPeople(username, repository): 
        
            471
                return flask.render_template("repo-people.html", username=username, repository=repository) 
        
            472
            @app.route("/<username>/<repository>/activity/") 
        
            475
            def repositoryActivity(username, repository): 
        
            476
                return flask.render_template("repo-activity.html", username=username, repository=repository) 
        
            477
            @app.route("/<username>/<repository>/ci/") 
        
            480
            def repositoryCI(username, repository): 
        
            481
                return flask.render_template("repo-ci.html", username=username, repository=repository) 
        
            482
            @app.route("/<username>/<repository>/settings/") 
        
            485
            def repositorySettings(username, repository): 
        
            486
                flask.abort(401) 
        
            487
                return flask.render_template("repo-settings.html", username=username, repository=repository) 
        
            488
            @app.errorhandler(404) 
        
            491
            def e404(error): 
        
            492
                return flask.render_template("not-found.html"), 404 
        
            493
            @app.errorhandler(401) 
        
            496
            def e401(error): 
        
            497
                return flask.render_template("unauthorised.html"), 401 
        
            498
            @app.errorhandler(403) 
        
            501
            def e403(error): 
        
            502
                return flask.render_template("forbidden.html"), 403 
        
            503
            @app.errorhandler(418) 
        
            506
            def e418(error): 
        
            507
                return flask.render_template("teapot.html"), 418 
        
            508