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 • 37.28 kiB
Python script, ASCII text executable
        
            
1
import os
2
import random
3
import subprocess
4
from functools import wraps
5
6
import cairosvg
7
import flask
8
from flask_sqlalchemy import SQLAlchemy
9
import git
10
import mimetypes
11
import magic
12
from flask_bcrypt import Bcrypt
13
from markupsafe import escape, Markup
14
from flask_migrate import Migrate
15
from datetime import datetime
16
from enum import Enum
17
import shutil
18
from PIL import Image
19
from cairosvg import svg2png
20
import platform
21
22
import config
23
24
app = flask.Flask(__name__)
25
26
from flask_httpauth import HTTPBasicAuth
27
28
auth = HTTPBasicAuth()
29
30
app.config["SQLALCHEMY_DATABASE_URI"] = config.DB_URI
31
app.config["SECRET_KEY"] = config.DB_PASSWORD
32
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
33
db = SQLAlchemy(app)
34
bcrypt = Bcrypt(app)
35
migrate = Migrate(app, db)
36
37
38
def gitCommand(repo, data, *args):
39
if not os.path.isdir(repo):
40
raise FileNotFoundError("Repo not found")
41
env = os.environ.copy()
42
43
command = ["git", *args]
44
45
proc = subprocess.Popen(" ".join(command), cwd=repo, env=env, shell=True, stdout=subprocess.PIPE,
46
stdin=subprocess.PIPE)
47
print(command)
48
49
if data:
50
proc.stdin.write(data)
51
52
out, err = proc.communicate()
53
return out
54
55
56
def onlyChars(string, chars):
57
for i in string:
58
if i not in chars:
59
return False
60
return True
61
62
63
with app.app_context():
64
class RepoAccess(db.Model):
65
id = db.Column(db.Integer, primary_key=True)
66
userUsername = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
67
repoRoute = db.Column(db.String(98), db.ForeignKey("repo.route"), nullable=False)
68
accessLevel = db.Column(db.SmallInteger(), nullable=False) # 0 read-only, 1 read-write, 2 admin
69
70
user = db.relationship("User", back_populates="repoAccess")
71
repo = db.relationship("Repo", back_populates="repoAccess")
72
73
__table_args__ = (db.UniqueConstraint("userUsername", "repoRoute", name="_user_repo_uc"),)
74
75
def __init__(self, user, repo, level):
76
self.userUsername = user.username
77
self.repoRoute = repo.route
78
self.accessLevel = level
79
80
81
class RepoFavourite(db.Model):
82
id = db.Column(db.Integer, primary_key=True)
83
userUsername = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
84
repoRoute = db.Column(db.String(98), db.ForeignKey("repo.route"), nullable=False)
85
86
user = db.relationship("User", back_populates="favourites")
87
repo = db.relationship("Repo", back_populates="favourites")
88
89
__table_args__ = (db.UniqueConstraint("userUsername", "repoRoute", name="_user_repo_uc"),)
90
91
def __init__(self, user, repo):
92
self.userUsername = user.username
93
self.repoRoute = repo.route
94
95
96
class PostVote(db.Model):
97
id = db.Column(db.Integer, primary_key=True)
98
userUsername = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
99
postIdentifier = db.Column(db.String(109), db.ForeignKey("post.identifier"), nullable=False)
100
voteScore = db.Column(db.SmallInteger(), nullable=False)
101
102
user = db.relationship("User", back_populates="votes")
103
post = db.relationship("Post", back_populates="votes")
104
105
__table_args__ = (db.UniqueConstraint("userUsername", "postIdentifier", name="_user_post_uc"),)
106
107
def __init__(self, user, post, score):
108
self.userUsername = user.username
109
self.postIdentifier = post.identifier
110
self.voteScore = score
111
112
113
class User(db.Model):
114
username = db.Column(db.String(32), unique=True, nullable=False, primary_key=True)
115
displayName = db.Column(db.Unicode(128), unique=False, nullable=True)
116
bio = db.Column(db.Unicode(512), unique=False, nullable=True)
117
passwordHashed = db.Column(db.String(60), nullable=False)
118
email = db.Column(db.String(254), nullable=True)
119
company = db.Column(db.Unicode(64), nullable=True)
120
companyURL = db.Column(db.String(256), nullable=True)
121
URL = db.Column(db.String(256), nullable=True)
122
showMail = db.Column(db.Boolean, default=False, nullable=False)
123
location = db.Column(db.Unicode(64), nullable=True)
124
creationDate = db.Column(db.DateTime, default=datetime.utcnow)
125
126
repositories = db.relationship("Repo", back_populates="owner")
127
repoAccess = db.relationship("RepoAccess", back_populates="user")
128
votes = db.relationship("PostVote", back_populates="user")
129
favourites = db.relationship("RepoFavourite", back_populates="user")
130
131
commits = db.relationship("Commit", back_populates="owner")
132
posts = db.relationship("Post", back_populates="owner")
133
134
def __init__(self, username, password, email=None, displayName=None):
135
self.username = username
136
self.passwordHashed = bcrypt.generate_password_hash(password, config.HASHING_ROUNDS).decode("utf-8")
137
self.email = email
138
self.displayName = displayName
139
140
# Create the user's directory
141
if not os.path.exists(os.path.join(config.REPOS_PATH, username)):
142
os.makedirs(os.path.join(config.REPOS_PATH, username))
143
if not os.path.exists(os.path.join(config.USERDATA_PATH, username)):
144
os.makedirs(os.path.join(config.USERDATA_PATH, username))
145
146
avatarName = random.choice(os.listdir(config.DEFAULT_AVATARS_PATH))
147
if os.path.join(config.DEFAULT_AVATARS_PATH, avatarName).endswith(".svg"):
148
cairosvg.svg2png(url=os.path.join(config.DEFAULT_AVATARS_PATH, avatarName),
149
write_to="/tmp/roundabout-avatar.png")
150
avatar = Image.open("/tmp/roundabout-avatar.png")
151
else:
152
avatar = Image.open(os.path.join(config.DEFAULT_AVATARS_PATH, avatarName))
153
avatar.thumbnail(config.AVATAR_SIZE)
154
avatar.save(os.path.join(config.USERDATA_PATH, username, "avatar.png"))
155
156
157
class Repo(db.Model):
158
route = db.Column(db.String(98), unique=True, nullable=False, primary_key=True)
159
ownerName = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
160
name = db.Column(db.String(64), nullable=False)
161
owner = db.relationship("User", back_populates="repositories")
162
visibility = db.Column(db.SmallInteger(), nullable=False)
163
info = db.Column(db.Unicode(512), nullable=True)
164
URL = db.Column(db.String(256), nullable=True)
165
creationDate = db.Column(db.DateTime, default=datetime.utcnow)
166
167
defaultBranch = db.Column(db.String(64), nullable=True, default="")
168
169
commits = db.relationship("Commit", back_populates="repo")
170
posts = db.relationship("Post", back_populates="repo")
171
repoAccess = db.relationship("RepoAccess", back_populates="repo")
172
favourites = db.relationship("RepoFavourite", back_populates="repo")
173
174
lastPostID = db.Column(db.Integer, nullable=False, default=0)
175
176
def __init__(self, owner, name, visibility):
177
self.route = f"/{owner.username}/{name}"
178
self.name = name
179
self.ownerName = owner.username
180
self.owner = owner
181
self.visibility = visibility
182
183
# Add the owner as an admin
184
repoAccess = RepoAccess(owner, self, 2)
185
db.session.add(repoAccess)
186
187
188
class Commit(db.Model):
189
identifier = db.Column(db.String(227), unique=True, nullable=False, primary_key=True)
190
sha = db.Column(db.String(128), nullable=False)
191
repoName = db.Column(db.String(98), db.ForeignKey("repo.route"), nullable=False)
192
ownerName = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
193
ownerIdentity = db.Column(db.String(321))
194
receiveDate = db.Column(db.DateTime, default=datetime.now)
195
authorDate = db.Column(db.DateTime)
196
message = db.Column(db.UnicodeText)
197
repo = db.relationship("Repo", back_populates="commits")
198
owner = db.relationship("User", back_populates="commits")
199
200
def __init__(self, sha, owner, repo, date, message, ownerIdentity):
201
self.identifier = f"{repo.route}/{sha}"
202
self.sha = sha
203
self.repoName = repo.route
204
self.repo = repo
205
self.ownerName = owner.username
206
self.owner = owner
207
self.authorDate = datetime.fromtimestamp(int(date))
208
self.message = message
209
self.ownerIdentity = ownerIdentity
210
211
212
class Post(db.Model):
213
identifier = db.Column(db.String(109), unique=True, nullable=False, primary_key=True)
214
number = db.Column(db.Integer, nullable=False)
215
repoName = db.Column(db.String(98), db.ForeignKey("repo.route"), nullable=False)
216
ownerName = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
217
votes = db.relationship("PostVote", back_populates="post")
218
voteSum = db.Column(db.Integer, nullable=False, default=0)
219
220
parentID = db.Column(db.String(109), db.ForeignKey("post.identifier"), nullable=True)
221
state = db.Column(db.SmallInteger, nullable=True, default=1)
222
223
date = db.Column(db.DateTime, default=datetime.now)
224
lastUpdated = db.Column(db.DateTime, default=datetime.now)
225
subject = db.Column(db.Unicode(384))
226
message = db.Column(db.UnicodeText)
227
repo = db.relationship("Repo", back_populates="posts")
228
owner = db.relationship("User", back_populates="posts")
229
parent = db.relationship("Post", back_populates="children", remote_side="Post.identifier")
230
children = db.relationship("Post", back_populates="parent", remote_side="Post.parentID")
231
232
def __init__(self, owner, repo, parent, subject, message):
233
self.identifier = f"{repo.route}/{repo.lastPostID}"
234
self.number = repo.lastPostID
235
self.repoName = repo.route
236
self.repo = repo
237
self.ownerName = owner.username
238
self.owner = owner
239
self.subject = subject
240
self.message = message
241
self.parent = parent
242
repo.lastPostID += 1
243
244
def updateDate(self):
245
self.lastUpdated = datetime.now()
246
with db.session.no_autoflush:
247
if self.parent is not None:
248
self.parent.updateDate()
249
250
251
def getPermissionLevel(loggedIn, username, repository):
252
user = User.query.filter_by(username=loggedIn).first()
253
repo = Repo.query.filter_by(route=f"/{username}/{repository}").first()
254
255
if user and repo:
256
permission = RepoAccess.query.filter_by(user=user, repo=repo).first()
257
if permission:
258
return permission.accessLevel
259
260
return None
261
262
263
def getVisibility(username, repository):
264
repo = Repo.query.filter_by(route=f"/{username}/{repository}").first()
265
266
if repo:
267
return repo.visibility
268
269
return None
270
271
272
import gitHTTP
273
import jinjaUtils
274
275
276
def humanSize(value, decimals=2, scale=1024,
277
units=("B", "kiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB", "RiB", "QiB")):
278
for unit in units:
279
if value < scale:
280
break
281
value /= scale
282
if int(value) == value:
283
# do not return decimals, if the value is already round
284
return int(value), unit
285
return round(value * 10 ** decimals) / 10 ** decimals, unit
286
287
288
def guessMIME(path):
289
if os.path.isdir(path):
290
mimetype = "inode/directory"
291
elif magic.from_file(path, mime=True):
292
mimetype = magic.from_file(path, mime=True)
293
else:
294
mimetype = "application/octet-stream"
295
return mimetype
296
297
298
def convertToHTML(path):
299
with open(path, "r") as f:
300
contents = f.read()
301
return contents
302
303
304
@app.context_processor
305
def default():
306
username = flask.session.get("username")
307
308
return {"loggedInUser": username}
309
310
311
@app.route("/")
312
def main():
313
return flask.render_template("home.html")
314
315
316
@app.route("/about/")
317
def about():
318
return flask.render_template("about.html", platform=platform)
319
320
321
@app.route("/settings/", methods=["GET", "POST"])
322
def settings():
323
if flask.request.method == "GET":
324
if not flask.session.get("username"):
325
flask.abort(401)
326
user = User.query.filter_by(username=flask.session.get("username")).first()
327
328
return flask.render_template("user-settings.html", user=user)
329
else:
330
user = User.query.filter_by(username=flask.session.get("username")).first()
331
332
user.displayName = flask.request.form["displayname"]
333
user.URL = flask.request.form["url"]
334
user.company = flask.request.form["company"]
335
user.companyURL = flask.request.form["companyurl"]
336
user.location = flask.request.form["location"]
337
user.showMail = flask.request.form.get("showmail", user.showMail)
338
339
db.session.commit()
340
341
flask.flash(Markup("<iconify-icon icon='mdi:check'></iconify-icon>Settings saved"), category="success")
342
return flask.redirect(f"/{flask.session.get('username')}", code=303)
343
344
345
@app.route("/accounts/", methods=["GET", "POST"])
346
def login():
347
if flask.request.method == "GET":
348
return flask.render_template("login.html")
349
else:
350
if "login" in flask.request.form:
351
username = flask.request.form["username"]
352
password = flask.request.form["password"]
353
354
user = User.query.filter_by(username=username).first()
355
356
if user and bcrypt.check_password_hash(user.passwordHashed, password):
357
flask.session["username"] = user.username
358
flask.flash(
359
Markup(f"<iconify-icon icon='mdi:account'></iconify-icon>Successfully logged in as {username}"),
360
category="success")
361
return flask.redirect("/", code=303)
362
elif not user:
363
flask.flash(Markup("<iconify-icon icon='mdi:account-question'></iconify-icon>User not found"),
364
category="alert")
365
return flask.render_template("login.html")
366
else:
367
flask.flash(Markup("<iconify-icon icon='mdi:account-question'></iconify-icon>Invalid password"),
368
category="error")
369
return flask.render_template("login.html")
370
if "signup" in flask.request.form:
371
username = flask.request.form["username"]
372
password = flask.request.form["password"]
373
password2 = flask.request.form["password2"]
374
email = flask.request.form.get("email")
375
email2 = flask.request.form.get("email2") # repeat email is a honeypot
376
name = flask.request.form.get("name")
377
378
if not onlyChars(username, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_"):
379
flask.flash(Markup(
380
"<iconify-icon icon='mdi:account-error'></iconify-icon>Usernames may only contain Latin alphabet, numbers, '-' and '_'"),
381
category="error")
382
return flask.render_template("login.html")
383
384
if username in config.RESERVED_NAMES:
385
flask.flash(
386
Markup(f"<iconify-icon icon='mdi:account-error'></iconify-icon>Sorry, {username} is a system path"),
387
category="error")
388
return flask.render_template("login.html")
389
390
userCheck = User.query.filter_by(username=username).first()
391
if userCheck:
392
flask.flash(
393
Markup(f"<iconify-icon icon='mdi:account-error'></iconify-icon>The username {username} is taken"),
394
category="error")
395
return flask.render_template("login.html")
396
397
if password2 != password:
398
flask.flash(Markup("<iconify-icon icon='mdi:key-alert'></iconify-icon>Make sure the passwords match"),
399
category="error")
400
return flask.render_template("login.html")
401
402
user = User(username, password, email, name)
403
db.session.add(user)
404
db.session.commit()
405
flask.session["username"] = user.username
406
flask.flash(Markup(
407
f"<iconify-icon icon='mdi:account'></iconify-icon>Successfully created and logged in as {username}"),
408
category="success")
409
return flask.redirect("/", code=303)
410
411
412
@app.route("/newrepo/", methods=["GET", "POST"])
413
def newRepo():
414
if flask.request.method == "GET":
415
return flask.render_template("new-repo.html")
416
else:
417
name = flask.request.form["name"]
418
visibility = int(flask.request.form["visibility"])
419
420
if not onlyChars(name, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_"):
421
flask.flash(Markup(
422
"<iconify-icon icon='mdi:error'></iconify-icon>Repository names may only contain Latin alphabet, numbers, '-' and '_'"),
423
category="error")
424
return flask.render_template("new-repo.html")
425
426
user = User.query.filter_by(username=flask.session.get("username")).first()
427
428
repo = Repo(user, name, visibility)
429
db.session.add(repo)
430
db.session.commit()
431
432
if not os.path.exists(os.path.join(config.REPOS_PATH, repo.route)):
433
subprocess.run(["git", "init", repo.name],
434
cwd=os.path.join(config.REPOS_PATH, flask.session.get("username")))
435
436
flask.flash(Markup(f"<iconify-icon icon='mdi:folder'></iconify-icon>Successfully created repository {name}"),
437
category="success")
438
return flask.redirect(repo.route, code=303)
439
440
441
@app.route("/logout")
442
def logout():
443
flask.session.clear()
444
flask.flash(Markup(f"<iconify-icon icon='mdi:account'></iconify-icon>Successfully logged out"), category="info")
445
return flask.redirect("/", code=303)
446
447
448
@app.route("/<username>/")
449
def userProfile(username):
450
user = User.query.filter_by(username=username).first()
451
repos = Repo.query.filter_by(ownerName=username, visibility=2)
452
return flask.render_template("user-profile.html", user=user, repos=repos)
453
454
455
@app.route("/<username>/<repository>/")
456
def repositoryIndex(username, repository):
457
return flask.redirect("./tree", code=302)
458
459
460
@app.route("/<username>/<repository>/raw/<branch>/<path:subpath>")
461
def repositoryRaw(username, repository, branch, subpath):
462
if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("user"), username,
463
repository) is not None):
464
flask.abort(403)
465
466
serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository)
467
468
app.logger.info(f"Loading {serverRepoLocation}")
469
470
if not os.path.exists(serverRepoLocation):
471
app.logger.error(f"Cannot load {serverRepoLocation}")
472
return flask.render_template("not-found.html"), 404
473
474
repo = git.Repo(serverRepoLocation)
475
repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first()
476
if not repoData.defaultBranch:
477
if repo.heads:
478
repoData.defaultBranch = repo.heads[0].name
479
else:
480
return flask.render_template("empty.html",
481
remote=f"http://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200
482
if not branch:
483
branch = repoData.defaultBranch
484
return flask.redirect(f"./{branch}", code=302)
485
486
if branch.startswith("tag:"):
487
ref = f"tags/{branch[4:]}"
488
elif branch.startswith("~"):
489
ref = branch[1:]
490
else:
491
ref = f"heads/{branch}"
492
493
ref = ref.replace("~", "/") # encode slashes for URL support
494
495
try:
496
repo.git.checkout("-f", ref)
497
except git.exc.GitCommandError:
498
return flask.render_template("not-found.html"), 404
499
500
return flask.send_from_directory(config.REPOS_PATH, os.path.join(username, repository, subpath))
501
502
503
@app.route("/info/<username>/avatar")
504
def userAvatar(username):
505
serverUserdataLocation = os.path.join(config.USERDATA_PATH, username)
506
507
if not os.path.exists(serverUserdataLocation):
508
return flask.render_template("not-found.html"), 404
509
510
return flask.send_from_directory(serverUserdataLocation, "avatar.png")
511
512
513
@app.route("/<username>/<repository>/tree/", defaults={"branch": None, "subpath": ""})
514
@app.route("/<username>/<repository>/tree/<branch>/", defaults={"subpath": ""})
515
@app.route("/<username>/<repository>/tree/<branch>/<path:subpath>")
516
def repositoryTree(username, repository, branch, subpath):
517
if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username,
518
repository) is not None):
519
flask.abort(403)
520
521
serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository)
522
523
app.logger.info(f"Loading {serverRepoLocation}")
524
525
if not os.path.exists(serverRepoLocation):
526
app.logger.error(f"Cannot load {serverRepoLocation}")
527
return flask.render_template("not-found.html"), 404
528
529
repo = git.Repo(serverRepoLocation)
530
repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first()
531
if not repoData.defaultBranch:
532
if repo.heads:
533
repoData.defaultBranch = repo.heads[0].name
534
else:
535
return flask.render_template("empty.html",
536
remote=f"http://{config.BASE_DOMAIN}/git/{username}/{repository}"), 200
537
if not branch:
538
branch = repoData.defaultBranch
539
return flask.redirect(f"./{branch}", code=302)
540
541
if branch.startswith("tag:"):
542
ref = f"tags/{branch[4:]}"
543
elif branch.startswith("~"):
544
ref = branch[1:]
545
else:
546
ref = f"heads/{branch}"
547
548
ref = ref.replace("~", "/") # encode slashes for URL support
549
550
try:
551
repo.git.checkout("-f", ref)
552
except git.exc.GitCommandError:
553
return flask.render_template("not-found.html"), 404
554
555
branches = repo.heads
556
557
allRefs = []
558
for ref in repo.heads:
559
allRefs.append((ref, "head"))
560
for ref in repo.tags:
561
allRefs.append((ref, "tag"))
562
563
if os.path.isdir(os.path.join(serverRepoLocation, subpath)):
564
files = []
565
blobs = []
566
567
for entry in os.listdir(os.path.join(serverRepoLocation, subpath)):
568
if not os.path.basename(entry) == ".git":
569
files.append(os.path.join(subpath, entry))
570
571
infos = []
572
573
for file in files:
574
path = os.path.join(serverRepoLocation, file)
575
mimetype = guessMIME(path)
576
577
text = gitCommand(serverRepoLocation, None, "log", "--format='%H\n'", file).decode()
578
579
sha = text.split("\n")[0]
580
identifier = f"/{username}/{repository}/{sha}"
581
lastCommit = Commit.query.filter_by(identifier=identifier).first()
582
583
info = {
584
"name": os.path.basename(file),
585
"serverPath": path,
586
"relativePath": file,
587
"link": os.path.join(f"/{username}/{repository}/tree/{branch}/", file),
588
"size": humanSize(os.path.getsize(path)),
589
"mimetype": f"{mimetype}{f' ({mimetypes.guess_type(path)[1]})' if mimetypes.guess_type(path)[1] else ''}",
590
"commit": lastCommit,
591
"shaSize": 7,
592
}
593
594
specialIcon = config.matchIcon(os.path.basename(file))
595
if specialIcon:
596
info["icon"] = specialIcon
597
elif os.path.isdir(path):
598
info["icon"] = config.folderIcon
599
elif mimetypes.guess_type(path)[0] in config.fileIcons:
600
info["icon"] = config.fileIcons[mimetypes.guess_type(path)[0]]
601
else:
602
info["icon"] = config.unknownIcon
603
604
if os.path.isdir(path):
605
infos.insert(0, info)
606
else:
607
infos.append(info)
608
609
return flask.render_template(
610
"repo-tree.html",
611
username=username,
612
repository=repository,
613
files=infos,
614
subpath=os.path.join("/", subpath),
615
branches=allRefs,
616
current=branch
617
)
618
else:
619
path = os.path.join(serverRepoLocation, subpath)
620
621
if not os.path.exists(path):
622
return flask.render_template("not-found.html"), 404
623
624
mimetype = guessMIME(path)
625
mode = mimetype.split("/", 1)[0]
626
size = humanSize(os.path.getsize(path))
627
628
specialIcon = config.matchIcon(os.path.basename(path))
629
if specialIcon:
630
icon = specialIcon
631
elif os.path.isdir(path):
632
icon = config.folderIcon
633
elif mimetypes.guess_type(path)[0] in config.fileIcons:
634
icon = config.fileIcons[mimetypes.guess_type(path)[0]]
635
else:
636
icon = config.unknownIcon
637
638
contents = None
639
if mode == "text":
640
contents = convertToHTML(path)
641
642
return flask.render_template(
643
"repo-file.html",
644
username=username,
645
repository=repository,
646
file=os.path.join(f"/{username}/{repository}/raw/{branch}/", subpath),
647
branches=allRefs,
648
current=branch,
649
mode=mode,
650
mimetype=mimetype,
651
detailedtype=magic.from_file(path),
652
size=size,
653
icon=icon,
654
subpath=os.path.join("/", subpath),
655
basename=os.path.basename(path),
656
contents=contents
657
)
658
659
660
@app.route("/<username>/<repository>/forum/")
661
def repositoryForum(username, repository):
662
if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username,
663
repository) is not None):
664
flask.abort(403)
665
666
serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository)
667
668
app.logger.info(f"Loading {serverRepoLocation}")
669
670
if not os.path.exists(serverRepoLocation):
671
app.logger.error(f"Cannot load {serverRepoLocation}")
672
return flask.render_template("not-found.html"), 404
673
674
repo = git.Repo(serverRepoLocation)
675
repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first()
676
user = User.query.filter_by(username=flask.session.get("username")).first()
677
relationships = RepoAccess.query.filter_by(repo=repoData)
678
userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first()
679
680
return flask.render_template("repo-forum.html", username=username, repository=repository, repoData=repoData, relationships=relationships, repo=repo, userRelationship=userRelationship, Post=Post)
681
682
683
@app.route("/<username>/<repository>/forum/new", methods=["POST"])
684
def repositoryForumAdd(username, repository):
685
if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username,
686
repository) is not None):
687
flask.abort(403)
688
689
serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository)
690
691
app.logger.info(f"Loading {serverRepoLocation}")
692
693
if not os.path.exists(serverRepoLocation):
694
app.logger.error(f"Cannot load {serverRepoLocation}")
695
return flask.render_template("not-found.html"), 404
696
697
repo = git.Repo(serverRepoLocation)
698
repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first()
699
user = User.query.filter_by(username=flask.session.get("username")).first()
700
relationships = RepoAccess.query.filter_by(repo=repoData)
701
userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first()
702
703
post = Post(user, repoData, None, flask.request.form["subject"], flask.request.form["message"])
704
705
db.session.add(post)
706
db.session.commit()
707
708
return flask.redirect(flask.url_for("repositoryForumThread", username=username, repository=repository, postID=post.number), code=303)
709
710
711
@app.route("/<username>/<repository>/forum/<int:postID>")
712
def repositoryForumThread(username, repository, postID):
713
if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username,
714
repository) is not None):
715
flask.abort(403)
716
717
serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository)
718
719
app.logger.info(f"Loading {serverRepoLocation}")
720
721
if not os.path.exists(serverRepoLocation):
722
app.logger.error(f"Cannot load {serverRepoLocation}")
723
return flask.render_template("not-found.html"), 404
724
725
repo = git.Repo(serverRepoLocation)
726
repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first()
727
user = User.query.filter_by(username=flask.session.get("username")).first()
728
relationships = RepoAccess.query.filter_by(repo=repoData)
729
userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first()
730
731
return flask.render_template("repo-forum-thread.html", username=username, repository=repository, repoData=repoData, relationships=relationships, repo=repo, userRelationship=userRelationship, Post=Post, postID=postID, maxPostNesting=4)
732
733
734
@app.route("/<username>/<repository>/forum/<int:postID>/reply", methods=["POST"])
735
def repositoryForumReply(username, repository, postID):
736
if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username,
737
repository) is not None):
738
flask.abort(403)
739
740
serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository)
741
742
app.logger.info(f"Loading {serverRepoLocation}")
743
744
if not os.path.exists(serverRepoLocation):
745
app.logger.error(f"Cannot load {serverRepoLocation}")
746
return flask.render_template("not-found.html"), 404
747
748
repo = git.Repo(serverRepoLocation)
749
repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first()
750
user = User.query.filter_by(username=flask.session.get("username")).first()
751
relationships = RepoAccess.query.filter_by(repo=repoData)
752
userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first()
753
if not user:
754
flask.abort(401)
755
756
parent = Post.query.filter_by(identifier=f"/{username}/{repository}/{postID}").first()
757
post = Post(user, repoData, parent, flask.request.form["subject"], flask.request.form["message"])
758
759
db.session.add(post)
760
post.updateDate()
761
db.session.commit()
762
763
return flask.redirect(flask.url_for("repositoryForumThread", username=username, repository=repository, postID=postID), code=303)
764
765
766
@app.route("/<username>/<repository>/forum/<int:postID>/voteup", defaults={"score": 1})
767
@app.route("/<username>/<repository>/forum/<int:postID>/votedown", defaults={"score": -1})
768
@app.route("/<username>/<repository>/forum/<int:postID>/votes", defaults={"score": 0})
769
def repositoryForumVote(username, repository, postID, score):
770
if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username,
771
repository) is not None):
772
flask.abort(403)
773
774
serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository)
775
776
app.logger.info(f"Loading {serverRepoLocation}")
777
778
if not os.path.exists(serverRepoLocation):
779
app.logger.error(f"Cannot load {serverRepoLocation}")
780
return flask.render_template("not-found.html"), 404
781
782
repo = git.Repo(serverRepoLocation)
783
repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first()
784
user = User.query.filter_by(username=flask.session.get("username")).first()
785
relationships = RepoAccess.query.filter_by(repo=repoData)
786
userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first()
787
if not user:
788
flask.abort(401)
789
790
post = Post.query.filter_by(identifier=f"/{username}/{repository}/{postID}").first()
791
792
if score:
793
oldRelationship = PostVote.query.filter_by(userUsername=user.username, postIdentifier=post.identifier).first()
794
if oldRelationship:
795
if score == oldRelationship.voteScore:
796
db.session.delete(oldRelationship)
797
post.voteSum -= oldRelationship.voteScore
798
else:
799
post.voteSum -= oldRelationship.voteScore
800
post.voteSum += score
801
oldRelationship.voteScore = score
802
else:
803
relationship = PostVote(user, post, score)
804
post.voteSum += score
805
db.session.add(relationship)
806
807
db.session.commit()
808
809
userVote = PostVote.query.filter_by(userUsername=user.username, postIdentifier=post.identifier).first()
810
response = flask.make_response(str(post.voteSum) + " " + str(userVote.voteScore if userVote else 0))
811
response.content_type = "text/plain"
812
813
return response
814
815
816
@app.route("/<username>/<repository>/users/", methods=["GET", "POST"])
817
def repositoryUsers(username, repository):
818
if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username,
819
repository) is not None):
820
flask.abort(403)
821
822
serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository)
823
824
app.logger.info(f"Loading {serverRepoLocation}")
825
826
if not os.path.exists(serverRepoLocation):
827
app.logger.error(f"Cannot load {serverRepoLocation}")
828
return flask.render_template("not-found.html"), 404
829
830
repo = git.Repo(serverRepoLocation)
831
repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first()
832
user = User.query.filter_by(username=flask.session.get("username")).first()
833
relationships = RepoAccess.query.filter_by(repo=repoData)
834
userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first()
835
836
if flask.request.method == "GET":
837
return flask.render_template("repo-users.html", username=username, repository=repository, repoData=repoData, relationships=relationships, repo=repo, userRelationship=userRelationship)
838
else:
839
if getPermissionLevel(flask.session.get("username"), username, repository) != 2:
840
flask.abort(401)
841
842
if flask.request.form.get("new-username"):
843
# Create new relationship
844
newUser = User.query.filter_by(username=flask.request.form.get("new-username")).first()
845
relationship = RepoAccess(newUser, repoData, flask.request.form.get("new-level"))
846
db.session.add(relationship)
847
db.session.commit()
848
if flask.request.form.get("update-username"):
849
# Create new relationship
850
updatedUser = User.query.filter_by(username=flask.request.form.get("update-username")).first()
851
relationship = RepoAccess.query.filter_by(repo=repoData, user=updatedUser).first()
852
if flask.request.form.get("update-level") == -1:
853
relationship.delete()
854
else:
855
relationship.accessLevel = flask.request.form.get("update-level")
856
db.session.commit()
857
858
return flask.redirect(app.url_for("repositoryUsers", username=username, repository=repository))
859
860
861
@app.route("/<username>/<repository>/branches/")
862
def repositoryBranches(username, repository):
863
if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username,
864
repository) is not None):
865
flask.abort(403)
866
867
serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository)
868
869
app.logger.info(f"Loading {serverRepoLocation}")
870
871
if not os.path.exists(serverRepoLocation):
872
app.logger.error(f"Cannot load {serverRepoLocation}")
873
return flask.render_template("not-found.html"), 404
874
875
repo = git.Repo(serverRepoLocation)
876
repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first()
877
878
return flask.render_template("repo-branches.html", username=username, repository=repository, repoData=repoData, repo=repo)
879
880
881
@app.route("/<username>/<repository>/log/")
882
def repositoryLog(username, repository):
883
if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username,
884
repository) is not None):
885
flask.abort(403)
886
887
serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository)
888
889
app.logger.info(f"Loading {serverRepoLocation}")
890
891
if not os.path.exists(serverRepoLocation):
892
app.logger.error(f"Cannot load {serverRepoLocation}")
893
return flask.render_template("not-found.html"), 404
894
895
repo = git.Repo(serverRepoLocation)
896
repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first()
897
commits = Commit.query.filter_by(repo=repoData)
898
899
return flask.render_template("repo-log.html", username=username, repository=repository, repoData=repoData, repo=repo, commits=commits)
900
901
902
@app.route("/<username>/<repository>/settings/")
903
def repositorySettings(username, repository):
904
if getPermissionLevel(flask.session.get("username"), username, repository) != 2:
905
flask.abort(401)
906
907
return flask.render_template("repo-settings.html", username=username, repository=repository)
908
909
910
@app.errorhandler(404)
911
def e404(error):
912
return flask.render_template("not-found.html"), 404
913
914
915
@app.errorhandler(401)
916
def e401(error):
917
return flask.render_template("unauthorised.html"), 401
918
919
920
@app.errorhandler(403)
921
def e403(error):
922
return flask.render_template("forbidden.html"), 403
923
924
925
@app.errorhandler(418)
926
def e418(error):
927
return flask.render_template("teapot.html"), 418
928
929
930
@app.errorhandler(405)
931
def e405(error):
932
return flask.render_template("method-not-allowed.html"), 405
933
934
935
if __name__ == "__main__":
936
app.run(debug=True, port=8080, host="0.0.0.0")
937