By using this site, you agree to have cookies stored on your device, strictly for functional purposes, such as storing your session and preferences.

Dismiss

 app.py

View raw Download
text/x-script.python • 19.18 kiB
Python script, ASCII text executable
        
            
1
import os
2
import random
3
import subprocess
4
5
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
21
import config
22
23
app = flask.Flask(__name__)
24
25
from flask_httpauth import HTTPBasicAuth
26
auth = HTTPBasicAuth()
27
28
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
35
36
def onlyChars(string, chars):
37
for i in string:
38
if i not in chars:
39
return False
40
return True
41
42
43
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
50
user = db.relationship("User", back_populates="repoAccess")
51
repo = db.relationship("Repo", back_populates="repoAccess")
52
53
__table_args__ = (db.UniqueConstraint("userUsername", "repoRoute", name="_user_repo_uc"),)
54
55
def __init__(self, user, repo, level):
56
self.userUsername = user.username
57
self.repoRoute = repo.route
58
self.accessLevel = level
59
60
61
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
74
repositories = db.relationship("Repo", back_populates="owner")
75
76
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
82
# 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
88
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
97
98
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
108
defaultBranch = db.Column(db.String(64), nullable=True, default="master")
109
110
commits = db.relationship("Commit", back_populates="repo")
111
repoAccess = db.relationship("RepoAccess", back_populates="repo")
112
113
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
120
# 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
125
126
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
137
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
148
import gitHTTP
149
150
151
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
161
162
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
171
172
def convertToHTML(path):
173
with open(path, "r") as f:
174
contents = f.read()
175
return contents
176
177
178
@app.context_processor
179
def default():
180
username = flask.session.get("username")
181
182
return {"loggedInUser": username}
183
184
185
@app.route("/")
186
def main():
187
return flask.render_template("home.html")
188
189
190
@app.route("/about")
191
def about():
192
return flask.render_template("about.html", platform=platform)
193
194
195
@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
201
return flask.render_template("user-settings.html", user=user)
202
203
204
@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
213
user = User.query.filter_by(username=username).first()
214
215
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
233
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
237
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
241
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
246
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
250
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
257
258
@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
266
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
272
user = User.query.filter_by(username=flask.session.get("username")).first()
273
274
repo = Repo(user, name, visibility)
275
db.session.add(repo)
276
db.session.commit()
277
278
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
281
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
284
285
@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
291
292
@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
298
299
@app.route("/<username>/<repository>/")
300
def repositoryIndex(username, repository):
301
return flask.redirect("./tree", code=302)
302
303
304
@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
308
app.logger.info(f"Loading {serverRepoLocation}")
309
310
if not os.path.exists(serverRepoLocation):
311
app.logger.error(f"Cannot load {serverRepoLocation}")
312
return flask.render_template("not-found.html"), 404
313
314
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
320
return flask.send_from_directory(config.REPOS_PATH, os.path.join(username, repository, subpath))
321
322
323
@app.route("/info/<username>/avatar")
324
def userAvatar(username):
325
serverUserdataLocation = os.path.join(config.USERDATA_PATH, username)
326
327
if not os.path.exists(serverUserdataLocation):
328
return flask.render_template("not-found.html"), 404
329
330
return flask.send_from_directory(serverUserdataLocation, "avatar.png")
331
332
333
@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
339
app.logger.info(f"Loading {serverRepoLocation}")
340
341
if not os.path.exists(serverRepoLocation):
342
app.logger.error(f"Cannot load {serverRepoLocation}")
343
return flask.render_template("not-found.html"), 404
344
345
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
359
branches = repo.heads
360
if os.path.isdir(os.path.join(serverRepoLocation, subpath)):
361
files = []
362
blobs = []
363
364
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
368
infos = []
369
370
for file in files:
371
path = os.path.join(serverRepoLocation, file)
372
mimetype = guessMIME(path)
373
374
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
383
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
393
if os.path.isdir(path):
394
infos.insert(0, info)
395
else:
396
infos.append(info)
397
398
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
410
if not os.path.exists(path):
411
return flask.render_template("not-found.html"), 404
412
413
mimetype = guessMIME(path)
414
mode = mimetype.split("/", 1)[0]
415
size = humanSize(os.path.getsize(path))
416
417
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
427
contents = None
428
if mode == "text":
429
contents = convertToHTML(path)
430
431
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
448
449
@app.route("/<username>/<repository>/forum/")
450
def repositoryForum(username, repository):
451
return flask.render_template("repo-forum.html", username=username, repository=repository)
452
453
454
@app.route("/<username>/<repository>/docs/")
455
def repositoryDocs(username, repository):
456
return flask.render_template("repo-docs.html", username=username, repository=repository)
457
458
459
@app.route("/<username>/<repository>/releases/")
460
def repositoryReleases(username, repository):
461
return flask.render_template("repo-releases.html", username=username, repository=repository)
462
463
464
@app.route("/<username>/<repository>/branches/")
465
def repositoryBranches(username, repository):
466
return flask.render_template("repo-branches.html", username=username, repository=repository)
467
468
469
@app.route("/<username>/<repository>/people/")
470
def repositoryPeople(username, repository):
471
return flask.render_template("repo-people.html", username=username, repository=repository)
472
473
474
@app.route("/<username>/<repository>/activity/")
475
def repositoryActivity(username, repository):
476
return flask.render_template("repo-activity.html", username=username, repository=repository)
477
478
479
@app.route("/<username>/<repository>/ci/")
480
def repositoryCI(username, repository):
481
return flask.render_template("repo-ci.html", username=username, repository=repository)
482
483
484
@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
489
490
@app.errorhandler(404)
491
def e404(error):
492
return flask.render_template("not-found.html"), 404
493
494
495
@app.errorhandler(401)
496
def e401(error):
497
return flask.render_template("unauthorised.html"), 401
498
499
500
@app.errorhandler(403)
501
def e403(error):
502
return flask.render_template("forbidden.html"), 403
503
504
505
@app.errorhandler(418)
506
def e418(error):
507
return flask.render_template("teapot.html"), 418
508
509