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