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