plz / gitly.v
396 lines · 344 sloc · 8.39 KB · d4104e7627c1594b9de8e026959b196932f293c8
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_gen_time string
45 page_gen_start i64
46 is_tree bool
47 logged_in bool
48 path_split []string
49 branch string
50 lang Lang = .en //.ru
51}
52
53// fn C.sqlite3_config(int)
54
55fn new_app() !&App {
56 // C.sqlite3_config(3)
57 conf := config.read_config('./config.json') or {
58 panic('Config not found or has syntax errors')
59 }
60
61 mut app := &App{
62 // db: sqlite.connect('gitly.sqlite') or { panic(err) }
63 db: connect_db(conf)!
64 config: conf
65 started_at: time.now().unix()
66 }
67
68 set_rand_crypto_safe_seed()
69
70 app.create_tables()!
71 app.migrate_tables()!
72
73 create_directory_if_not_exists('logs')
74
75 app.setup_logger()
76
77 version_path := os.join_path('static', 'assets', 'version')
78 create_directory_if_not_exists(os.dir(version_path))
79
80 stored_version := os.read_file(version_path) or { 'unknown' }
81 mut version := stored_version
82 git_result := git.Git.exec(['rev-parse', '--short', 'HEAD'])
83
84 if git_result.exit_code == 0 && !git_result.output.contains('fatal') {
85 version = git_result.output.trim_space()
86 }
87
88 if version != stored_version {
89 os.write_file(version_path, version) or { panic(err) }
90 }
91
92 app.version = version
93
94 build_unix := os.file_last_mod_unix(os.executable())
95 app.build_time = time.unix(build_unix).format()
96
97 app.handle_static('static', true)!
98 app.serve_static('/favicon.ico', 'static/assets/favicon.svg')!
99 if !os.exists('avatars') {
100 os.mkdir('avatars')!
101 }
102 app.handle_static('avatars', false)!
103
104 app.load_settings()
105
106 create_directory_if_not_exists(app.config.repo_storage_path)
107 create_directory_if_not_exists(app.config.archive_path)
108 create_directory_if_not_exists(app.config.avatars_path)
109
110 // Create the first admin user if the db is empty
111 app.get_user_by_id(1) or {}
112
113 if '-cmdapi' in os.args {
114 spawn app.command_fetcher()
115 }
116
117 return app
118}
119
120fn (mut app App) setup_logger() {
121 app.logger.set_level(.debug)
122
123 app.logger.set_full_logpath('./logs/log_${time.now().ymmdd()}.log')
124 app.logger.log_to_console_too()
125}
126
127pub fn (mut app App) warn(msg string) {
128 app.logger.warn(msg)
129
130 app.logger.flush()
131}
132
133pub fn (mut app App) info(msg string) {
134 app.logger.info(msg)
135
136 app.logger.flush()
137}
138
139pub fn (mut app App) debug(msg string) {
140 app.logger.debug(msg)
141
142 app.logger.flush()
143}
144
145pub fn (mut app App) init_server() {
146}
147
148pub fn (mut app App) before_request(mut ctx Context) bool {
149 ctx.page_gen_start = time.ticks()
150 $if trace_prealloc ? {
151 unsafe { prealloc_scope_checkpoint(c'gitly before_request start') }
152 }
153 ctx.logged_in = app.is_logged_in(mut ctx)
154 $if trace_prealloc ? {
155 unsafe { prealloc_scope_checkpoint(c'gitly checked login') }
156 }
157 if ctx.logged_in {
158 ctx.user = app.get_user_from_cookies(ctx) or {
159 ctx.logged_in = false
160 User{}
161 }
162 }
163 $if trace_prealloc ? {
164 unsafe { prealloc_scope_checkpoint(c'gitly loaded user') }
165 }
166 lang_cookie := ctx.get_cookie('lang') or { '' }
167 ctx.lang = match lang_cookie {
168 'ru' { Lang.ru }
169 'es' { Lang.es }
170 'jp' { Lang.jp }
171 'cn' { Lang.cn }
172 'pt' { Lang.pt }
173 else { Lang.en }
174 }
175
176 $if trace_prealloc ? {
177 unsafe { prealloc_scope_checkpoint(c'gitly loaded lang') }
178 }
179 return true
180}
181
182@['/open-source']
183pub fn (mut app App) open_source() veb.Result {
184 return $veb.html()
185}
186
187@['/']
188pub fn (mut app App) index(mut ctx Context) veb.Result {
189 user_count := app.get_users_count_with_reconnect() or { return ctx.db_error(err) }
190 if user_count == 0 {
191 return ctx.redirect('/register')
192 }
193
194 return $veb.html()
195}
196
197@['/change_lang/:lang'; post]
198pub fn (mut app App) change_lang(lang string) veb.Result {
199 eprintln('CHANGING LANG ${lang}')
200 expire_date := time.now().add_days(400)
201 ctx.set_cookie(name: 'lang', value: lang, path: '/', expires: expire_date)
202 // return ctx.redirect('/')
203 return ctx.json('ok')
204}
205
206pub fn (mut ctx Context) redirect_to_index() veb.Result {
207 return ctx.redirect('/')
208}
209
210pub fn (mut ctx Context) redirect_to_login() veb.Result {
211 return ctx.redirect('/login')
212}
213
214pub fn (mut ctx Context) redirect_to_repository(username string, repo_name string) veb.Result {
215 return ctx.redirect('/${username}/${repo_name}')
216}
217
218fn (mut app App) create_tables() ! {
219 sql app.db {
220 create table Repo
221 }!
222 // unix time default now
223 sql app.db {
224 create table File
225 }! // missing ON CONFLIC REPLACE
226 //"created_at int default (strftime('%s', 'now'))"
227 sql app.db {
228 create table Issue
229 }!
230 sql app.db {
231 create table Label
232 }!
233 sql app.db {
234 create table IssueLabel
235 }!
236 //"created_at int default (strftime('%s', 'now'))"
237 sql app.db {
238 create table Commit
239 }!
240 sql app.db {
241 create table BranchCommit
242 }!
243 // author text default '' is to to avoid joins
244 sql app.db {
245 create table LangStat
246 }!
247 sql app.db {
248 create table User
249 }!
250 sql app.db {
251 create table Email
252 }!
253 sql app.db {
254 create table Contributor
255 }!
256 sql app.db {
257 create table Activity
258 }!
259 sql app.db {
260 create table Tag
261 }!
262 sql app.db {
263 create table Release
264 }!
265 sql app.db {
266 create table SshKey
267 }!
268 sql app.db {
269 create table Comment
270 }!
271 sql app.db {
272 create table Branch
273 }!
274 sql app.db {
275 create table Settings
276 }!
277 sql app.db {
278 create table Token
279 }!
280 sql app.db {
281 create table SecurityLog
282 }!
283 sql app.db {
284 create table Star
285 }!
286 sql app.db {
287 create table Watch
288 }!
289 sql app.db {
290 create table CiStatus
291 }!
292 sql app.db {
293 create table PullRequest
294 }!
295 sql app.db {
296 create table PrComment
297 }!
298 sql app.db {
299 create table PrReview
300 }!
301 sql app.db {
302 create table PrReviewComment
303 }!
304 sql app.db {
305 create table Webhook
306 }!
307 sql app.db {
308 create table WebhookDelivery
309 }!
310 sql app.db {
311 create table Discussion
312 }!
313 sql app.db {
314 create table DiscussionComment
315 }!
316 sql app.db {
317 create table Project
318 }!
319 sql app.db {
320 create table ProjectColumn
321 }!
322 sql app.db {
323 create table ProjectCard
324 }!
325 sql app.db {
326 create table Milestone
327 }!
328 sql app.db {
329 create table TwoFactor
330 }!
331 sql app.db {
332 create table ApiToken
333 }!
334 sql app.db {
335 create table Org
336 }!
337 sql app.db {
338 create table OrgMember
339 }!
340}
341
342fn (mut app App) migrate_tables() ! {
343 app.add_missing_column('File', 'is_size_calculated', db_bool_column_type())!
344 app.add_missing_column('Settings', 'disable_tree_folder_size', db_bool_column_type())!
345 app.add_missing_column('Repo', 'is_deleted', db_bool_column_type())!
346 app.add_missing_column('Repo', 'disable_discussions', db_bool_column_type())!
347 app.add_missing_column('Repo', 'disable_projects', db_bool_column_type())!
348 app.add_missing_column('Repo', 'disable_milestones', db_bool_column_type())!
349 app.add_missing_column('Repo', 'disable_wiki', db_bool_column_type())!
350 app.add_missing_column('Repo', 'is_pinned', db_bool_column_type())!
351
352 app.db.exec('create index if not exists idx_commit_repo_created on ${sql_table('Commit')} (repo_id, created_at desc)')!
353}
354
355fn (mut app App) add_missing_column(table_name string, column_name string, column_type string) ! {
356 if db_column_exists(mut app.db, table_name, column_name)! {
357 return
358 }
359
360 app.db.exec('alter table ${sql_table(table_name)} add column ${sql_table(column_name)} ${column_type}')!
361}
362
363fn (mut ctx Context) json_success[T](result T) veb.Result {
364 response := api.ApiSuccessResponse[T]{
365 success: true
366 result: result
367 }
368
369 return ctx.json(response)
370}
371
372fn (mut ctx Context) json_error(message string) veb.Result {
373 return ctx.json(api.ApiErrorResponse{
374 success: false
375 message: message
376 })
377}
378
379// maybe it should be implemented with another static server, in dev
380fn (mut app App) send_file(filname string, content string) veb.Result {
381 ctx.set_header(.content_disposition, 'attachment; filename="${filname}"')
382
383 return ctx.ok(content)
384}
385
386fn (mut ctx Context) page_gen_time() string {
387 if ctx.page_gen_start == 0 {
388 return '<1ms'
389 }
390 diff := int(time.ticks() - ctx.page_gen_start)
391 return if diff == 0 {
392 '<1ms'
393 } else {
394 '${diff}ms'
395 }
396}
397