gitlyx / gitly.v
495 lines · 436 sloc · 10.91 KB · 03ce80574e9d3cb5892de161e92055e76b30af1c
Raw
1// Copyright (c) 2020-2021 Alexander Medvednikov. All rights reserved.
2// Use of this source code is governed by a GPL license that can be found in the LICENSE file.
3module main
4
5import veb
6import time
7import os
8import log
9import api
10import config
11import git
12
13const commits_per_page = 35
14const expire_length = 200
15const posts_per_day = 5
16const max_username_len = 40
17const max_login_attempts = 5
18const max_user_repos = 10
19const max_repo_name_len = 100
20const max_namechanges = 3
21const namechange_period = time.hour * 24
22
23@[heap]
24pub struct App {
25 veb.StaticHandler
26 veb.Middleware[Context]
27 started_at i64
28pub mut:
29 db GitlyDb
30mut:
31 version string
32 build_time string
33 logger log.Log
34 config config.Config
35 settings Settings
36 port int
37}
38
39pub struct Context {
40 veb.Context
41mut:
42 user User
43 current_path string
44 page_title string
45 page_gen_time string
46 page_gen_start i64
47 is_tree bool
48 logged_in bool
49 path_split []string
50 branch string
51 lang Lang = .en //.ru
52}
53
54// fn C.sqlite3_config(int)
55
56fn new_app() !&App {
57 // C.sqlite3_config(3)
58 conf := config.read_config('./config.json') or {
59 panic('Config not found or has syntax errors')
60 }
61
62 mut app := &App{
63 // db: sqlite.connect('gitly.sqlite') or { panic(err) }
64 db: connect_db(conf)!
65 config: conf
66 started_at: time.now().unix()
67 }
68
69 set_rand_crypto_safe_seed()
70
71 app.create_tables()!
72 app.migrate_tables()!
73
74 create_directory_if_not_exists('logs')
75
76 app.setup_logger()
77
78 version_path := os.join_path('static', 'assets', 'version')
79 create_directory_if_not_exists(os.dir(version_path))
80
81 stored_version := os.read_file(version_path) or { 'unknown' }
82 mut version := stored_version
83 git_result := git.Git.exec(['rev-parse', '--short', 'HEAD'])
84
85 if git_result.exit_code == 0 && !git_result.output.contains('fatal') {
86 version = git_result.output.trim_space()
87 }
88
89 if version != stored_version {
90 os.write_file(version_path, version) or { panic(err) }
91 }
92
93 app.version = version
94
95 build_unix := os.file_last_mod_unix(os.executable())
96 app.build_time = time.unix(build_unix).format()
97
98 app.handle_static('static', true)!
99 app.serve_static('/favicon.ico', 'static/assets/favicon.svg')!
100 if !os.exists('avatars') {
101 os.mkdir('avatars')!
102 }
103 app.handle_static('avatars', false)!
104
105 app.load_settings()
106
107 create_directory_if_not_exists(app.config.repo_storage_path)
108 create_directory_if_not_exists(app.config.archive_path)
109 create_directory_if_not_exists(app.config.avatars_path)
110
111 // Create the first admin user if the db is empty
112 app.get_user_by_id(1) or {}
113
114 if '-cmdapi' in os.args {
115 spawn app.command_fetcher()
116 }
117
118 return app
119}
120
121fn (mut app App) setup_logger() {
122 app.logger.set_level(.debug)
123
124 app.logger.set_full_logpath('./logs/log_${time.now().ymmdd()}.log')
125 app.logger.log_to_console_too()
126}
127
128pub fn (mut app App) warn(msg string) {
129 app.logger.warn(msg)
130
131 app.logger.flush()
132}
133
134pub fn (mut app App) info(msg string) {
135 app.logger.info(msg)
136
137 app.logger.flush()
138}
139
140pub fn (mut app App) debug(msg string) {
141 app.logger.debug(msg)
142
143 app.logger.flush()
144}
145
146pub fn (mut app App) init_server() {
147}
148
149pub fn (mut app App) before_request(mut ctx Context) bool {
150 ctx.page_gen_start = time.ticks()
151 ctx.set_page_title_from_path(ctx.req.url)
152 $if trace_prealloc ? {
153 unsafe { prealloc_scope_checkpoint(c'gitly before_request start') }
154 }
155 ctx.logged_in = app.is_logged_in(mut ctx)
156 $if trace_prealloc ? {
157 unsafe { prealloc_scope_checkpoint(c'gitly checked login') }
158 }
159 if ctx.logged_in {
160 ctx.user = app.get_user_from_cookies(ctx) or {
161 ctx.logged_in = false
162 User{}
163 }
164 }
165 $if trace_prealloc ? {
166 unsafe { prealloc_scope_checkpoint(c'gitly loaded user') }
167 }
168 lang_cookie := ctx.get_cookie('lang') or { '' }
169 ctx.lang = match lang_cookie {
170 'ru' { Lang.ru }
171 'es' { Lang.es }
172 'jp' { Lang.jp }
173 'cn' { Lang.cn }
174 'pt' { Lang.pt }
175 else { Lang.en }
176 }
177
178 $if trace_prealloc ? {
179 unsafe { prealloc_scope_checkpoint(c'gitly loaded lang') }
180 }
181 return true
182}
183
184@['/open-source']
185pub fn (mut app App) open_source() veb.Result {
186 return $veb.html()
187}
188
189@['/pricing']
190pub fn (mut app App) pricing() veb.Result {
191 return $veb.html('templates/pricing.html')
192}
193
194@['/prcing']
195pub fn (mut app App) prcing() veb.Result {
196 return $veb.html('templates/pricing.html')
197}
198
199@['/']
200pub fn (mut app App) index(mut ctx Context) veb.Result {
201 user_count := app.get_users_count_with_reconnect() or { return ctx.db_error(err) }
202 if user_count == 0 {
203 return ctx.redirect('/register')
204 }
205
206 return $veb.html()
207}
208
209@['/change_lang/:lang'; post]
210pub fn (mut app App) change_lang(lang string) veb.Result {
211 eprintln('CHANGING LANG ${lang}')
212 expire_date := time.now().add_days(400)
213 ctx.set_cookie(name: 'lang', value: lang, path: '/', expires: expire_date)
214 // return ctx.redirect('/')
215 return ctx.json('ok')
216}
217
218pub fn (mut ctx Context) redirect_to_index() veb.Result {
219 return ctx.redirect('/')
220}
221
222pub fn (mut ctx Context) redirect_to_login() veb.Result {
223 return ctx.redirect('/login')
224}
225
226pub fn (mut ctx Context) redirect_to_repository(username string, repo_name string) veb.Result {
227 return ctx.redirect('/${username}/${repo_name}')
228}
229
230fn (mut ctx Context) set_page_title(parts []string) {
231 mut clean_parts := []string{}
232 for part in parts {
233 clean := part.replace('\r', ' ').replace('\n', ' ').trim_space()
234 if clean != '' {
235 clean_parts << clean
236 }
237 }
238 clean_parts << 'Gitly'
239 ctx.page_title = clean_parts.join(' - ')
240}
241
242fn (mut ctx Context) set_page_title_from_path(raw_url string) {
243 path := raw_url.all_before('?').trim('/')
244 if path == '' {
245 ctx.set_page_title([]string{})
246 return
247 }
248
249 segments := path.split('/')
250 if segments.len >= 2 && segments[0] == 'api' {
251 ctx.set_page_title([]string{})
252 return
253 }
254 if segments.len >= 4 && segments[2] in ['issue', 'pull'] {
255 ctx.set_page_title(['${page_title_label(segments[2])} #${segments[3]}',
256 '${segments[0]}/${segments[1]}'])
257 return
258 }
259 if segments.len >= 3 {
260 ctx.set_page_title([page_title_label(segments[2]), '${segments[0]}/${segments[1]}'])
261 return
262 }
263 if segments.len == 2 {
264 if is_user_page_segment(segments[1]) {
265 ctx.set_page_title([page_title_label(segments[1]), segments[0]])
266 } else {
267 ctx.set_page_title(['${segments[0]}/${segments[1]}'])
268 }
269 return
270 }
271 ctx.set_page_title([page_title_label(segments[0])])
272}
273
274fn is_user_page_segment(slug string) bool {
275 return slug in ['feed', 'issues', 'pulls', 'repos', 'settings', 'stars']
276}
277
278fn page_title_label(slug string) string {
279 return match slug {
280 '2fa' { 'Two-factor authentication' }
281 'api-tokens' { 'API tokens' }
282 'blob' { 'File' }
283 'branches' { 'Branches' }
284 'ci' { 'CI' }
285 'commit' { 'Commit' }
286 'compare' { 'New pull request' }
287 'contributors' { 'Contributors' }
288 'discussions' { 'Discussions' }
289 'edit' { 'Edit file' }
290 'feed' { 'Feed' }
291 'issue' { 'Issue' }
292 'issues' { 'Issues' }
293 'login' { 'Login' }
294 'milestones' { 'Milestones' }
295 'new' { 'New repository' }
296 'open-source' { 'Open source' }
297 'organizations' { 'Organizations' }
298 'pricing' { 'Pricing' }
299 'projects' { 'Projects' }
300 'pull' { 'Pull request' }
301 'pulls' { 'Pull requests' }
302 'register' { 'Register' }
303 'releases' { 'Releases' }
304 'repos' { 'Repositories' }
305 'search' { 'Search' }
306 'security' { 'Security' }
307 'settings' { 'Settings' }
308 'ssh-keys' { 'SSH keys' }
309 'stars' { 'Stars' }
310 'tag' { 'Tag' }
311 'tree' { 'Code' }
312 'webhooks' { 'Webhooks' }
313 else { slug.replace('-', ' ').replace('_', ' ') }
314 }
315}
316
317fn (mut app App) create_tables() ! {
318 sql app.db {
319 create table Repo
320 }!
321 // unix time default now
322 sql app.db {
323 create table File
324 }! // missing ON CONFLIC REPLACE
325 //"created_at int default (strftime('%s', 'now'))"
326 sql app.db {
327 create table Issue
328 }!
329 sql app.db {
330 create table Label
331 }!
332 sql app.db {
333 create table IssueLabel
334 }!
335 //"created_at int default (strftime('%s', 'now'))"
336 sql app.db {
337 create table Commit
338 }!
339 sql app.db {
340 create table BranchCommit
341 }!
342 // author text default '' is to to avoid joins
343 sql app.db {
344 create table LangStat
345 }!
346 sql app.db {
347 create table User
348 }!
349 sql app.db {
350 create table Email
351 }!
352 sql app.db {
353 create table Contributor
354 }!
355 sql app.db {
356 create table Activity
357 }!
358 sql app.db {
359 create table Tag
360 }!
361 sql app.db {
362 create table Release
363 }!
364 sql app.db {
365 create table SshKey
366 }!
367 sql app.db {
368 create table Comment
369 }!
370 sql app.db {
371 create table Branch
372 }!
373 sql app.db {
374 create table Settings
375 }!
376 sql app.db {
377 create table Token
378 }!
379 sql app.db {
380 create table SecurityLog
381 }!
382 sql app.db {
383 create table Star
384 }!
385 sql app.db {
386 create table Watch
387 }!
388 sql app.db {
389 create table CiStatus
390 }!
391 sql app.db {
392 create table PullRequest
393 }!
394 sql app.db {
395 create table PrComment
396 }!
397 sql app.db {
398 create table PrReview
399 }!
400 sql app.db {
401 create table PrReviewComment
402 }!
403 sql app.db {
404 create table Webhook
405 }!
406 sql app.db {
407 create table WebhookDelivery
408 }!
409 sql app.db {
410 create table Discussion
411 }!
412 sql app.db {
413 create table DiscussionComment
414 }!
415 sql app.db {
416 create table Project
417 }!
418 sql app.db {
419 create table ProjectColumn
420 }!
421 sql app.db {
422 create table ProjectCard
423 }!
424 sql app.db {
425 create table Milestone
426 }!
427 sql app.db {
428 create table TwoFactor
429 }!
430 sql app.db {
431 create table ApiToken
432 }!
433 sql app.db {
434 create table Org
435 }!
436 sql app.db {
437 create table OrgMember
438 }!
439}
440
441fn (mut app App) migrate_tables() ! {
442 app.add_missing_column('File', 'is_size_calculated', db_bool_column_type())!
443 app.add_missing_column('Settings', 'disable_tree_folder_size', db_bool_column_type())!
444 app.add_missing_column('Repo', 'is_deleted', db_bool_column_type())!
445 app.add_missing_column('Repo', 'disable_discussions', db_bool_column_type())!
446 app.add_missing_column('Repo', 'disable_projects', db_bool_column_type())!
447 app.add_missing_column('Repo', 'disable_milestones', db_bool_column_type())!
448 app.add_missing_column('Repo', 'disable_wiki', db_bool_column_type())!
449 app.add_missing_column('Repo', 'is_pinned', db_bool_column_type())!
450
451 app.db.exec('create index if not exists idx_commit_repo_created on ${sql_table('Commit')} (repo_id, created_at desc)')!
452}
453
454fn (mut app App) add_missing_column(table_name string, column_name string, column_type string) ! {
455 if db_column_exists(mut app.db, table_name, column_name)! {
456 return
457 }
458
459 app.db.exec('alter table ${sql_table(table_name)} add column ${sql_table(column_name)} ${column_type}')!
460}
461
462fn (mut ctx Context) json_success[T](result T) veb.Result {
463 response := api.ApiSuccessResponse[T]{
464 success: true
465 result: result
466 }
467
468 return ctx.json(response)
469}
470
471fn (mut ctx Context) json_error(message string) veb.Result {
472 return ctx.json(api.ApiErrorResponse{
473 success: false
474 message: message
475 })
476}
477
478// maybe it should be implemented with another static server, in dev
479fn (mut app App) send_file(filname string, content string) veb.Result {
480 ctx.set_header(.content_disposition, 'attachment; filename="${filname}"')
481
482 return ctx.ok(content)
483}
484
485fn (mut ctx Context) page_gen_time() string {
486 if ctx.page_gen_start == 0 {
487 return '<1ms'
488 }
489 diff := int(time.ticks() - ctx.page_gen_start)
490 return if diff == 0 {
491 '<1ms'
492 } else {
493 '${diff}ms'
494 }
495}
496