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

 gitme.py

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