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