From a4a71a27974b82ef3a4d8166bdc0272a31c0a841 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 28 Mar 2022 10:19:57 +0200 Subject: [PATCH 01/15] First part of RESTful API (not correct yet) [CI SKIP] --- src/server/git.v | 135 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 103 insertions(+), 32 deletions(-) diff --git a/src/server/git.v b/src/server/git.v index 0147d87..967613f 100644 --- a/src/server/git.v +++ b/src/server/git.v @@ -3,16 +3,44 @@ module server import web import os import json - -const repos_file = 'repos.json' +import rand pub struct GitRepo { -pub: - url string [required] - branch string [required] +pub mut: + // URL of the Git repository + url string + // Branch of the Git repository to use + branch string + // On which architectures the package is allowed to be built. In reality, + // this controls which builders will periodically build the image. + arch []string } -fn read_repos(path string) ?[]GitRepo { +fn (mut r GitRepo) patch_from_params(params &map[string]string) ? { + $for field in GitRepo.fields { + if field.name in params { + $if field.typ is string { + r.$(field.name) = params[field.name] + // This specific type check is needed for the compiler to ensure + // our types are correct + } $else $if field.typ is []string { + r.$(field.name) = params[field.name].split(',') + } + }else{ + return error('Missing parameter: ${field.name}.') + } + } +} + +fn repo_from_params(params &map[string]string) ?GitRepo { + mut repo := GitRepo{} + + repo.patch_from_params(params) ? + + return repo +} + +fn read_repos(path string) ?map[string]GitRepo { if !os.exists(path) { mut f := os.create(path) ? @@ -20,17 +48,17 @@ fn read_repos(path string) ?[]GitRepo { f.close() } - f.write_string('[]') ? + f.write_string('{}') ? - return [] + return {} } content := os.read_file(path) ? - res := json.decode([]GitRepo, content) ? + res := json.decode(map[string]GitRepo, content) ? return res } -fn write_repos(path string, repos []GitRepo) ? { +fn write_repos(path string, repos &map[string]GitRepo) ? { mut f := os.create(path) ? defer { @@ -58,20 +86,40 @@ fn (mut app App) get_repos() web.Result { return app.json(repos) } +['/api/repos/:id'; get] +fn (mut app App) get_single_repo(id string) web.Result { + if !app.is_authorized() { + return app.text('Unauthorized.') + } + + repos := rlock app.git_mutex { + read_repos(app.conf.repos_file) or { + app.lerror('Failed to read repos file.') + + return app.server_error(500) + } + } + + if id !in repos { + return app.not_found() + } + + repo := repos[id] + + return app.json(repo) +} + ['/api/repos'; post] fn (mut app App) post_repo() web.Result { if !app.is_authorized() { return app.text('Unauthorized.') } - if !('url' in app.query && 'branch' in app.query) { + new_repo := repo_from_params(&app.query) or { return app.server_error(400) } - new_repo := GitRepo{ - url: app.query['url'] - branch: app.query['branch'] - } + id := rand.uuid_v4() mut repos := rlock app.git_mutex { read_repos(app.conf.repos_file) or { @@ -82,36 +130,27 @@ fn (mut app App) post_repo() web.Result { } // We need to check for duplicates - for r in repos { - if r == new_repo { + for _, repo in repos { + if repo == new_repo { return app.text('Duplicate repository.') } } - repos << new_repo + repos[id] = new_repo lock app.git_mutex { - write_repos(app.conf.repos_file, repos) or { return app.server_error(500) } + write_repos(app.conf.repos_file, &repos) or { return app.server_error(500) } } return app.ok('Repo added successfully.') } -['/api/repos'; delete] -fn (mut app App) delete_repo() web.Result { +['/api/repos/:id'; delete] +fn (mut app App) delete_repo(id string) web.Result { if !app.is_authorized() { return app.text('Unauthorized.') } - if !('url' in app.query && 'branch' in app.query) { - return app.server_error(400) - } - - repo_to_remove := GitRepo{ - url: app.query['url'] - branch: app.query['branch'] - } - mut repos := rlock app.git_mutex { read_repos(app.conf.repos_file) or { app.lerror('Failed to read repos file.') @@ -119,11 +158,43 @@ fn (mut app App) delete_repo() web.Result { return app.server_error(500) } } - filtered := repos.filter(it != repo_to_remove) + + if id !in repos { + return app.not_found() + } + + repos.delete(id) lock app.git_mutex { - write_repos(app.conf.repos_file, filtered) or { return app.server_error(500) } + write_repos(app.conf.repos_file, &repos) or { return app.server_error(500) } } return app.ok('Repo removed successfully.') } + +['/api/repos/:id'; patch] +fn (mut app App) patch_repo(id string) web.Result { + if !app.is_authorized() { + return app.text('Unauthorized.') + } + + mut repos := rlock app.git_mutex { + read_repos(app.conf.repos_file) or { + app.lerror('Failed to read repos file.') + + return app.server_error(500) + } + } + + if id !in repos { + return app.not_found() + } + + repos[id].patch_from_params(&app.query) + + lock app.git_mutex { + write_repos(app.conf.repos_file, &repos) or { return app.server_error(500) } + } + + return app.ok('Repo updated successfully.') +} From 08821725f921d1e2a1994c6fe03e3007e03e9a9d Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 28 Mar 2022 13:34:22 +0200 Subject: [PATCH 02/15] Added proper constraint for creating repo --- src/server/git.v | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/server/git.v b/src/server/git.v index 967613f..684e75a 100644 --- a/src/server/git.v +++ b/src/server/git.v @@ -16,7 +16,7 @@ pub mut: arch []string } -fn (mut r GitRepo) patch_from_params(params &map[string]string) ? { +fn (mut r GitRepo) patch_from_params(params map[string]string) { $for field in GitRepo.fields { if field.name in params { $if field.typ is string { @@ -26,16 +26,22 @@ fn (mut r GitRepo) patch_from_params(params &map[string]string) ? { } $else $if field.typ is []string { r.$(field.name) = params[field.name].split(',') } - }else{ - return error('Missing parameter: ${field.name}.') } } } -fn repo_from_params(params &map[string]string) ?GitRepo { +fn repo_from_params(params map[string]string) ?GitRepo { mut repo := GitRepo{} - repo.patch_from_params(params) ? + // If we're creating a new GitRepo, we want all fields to be present before + // "patching". + $for field in GitRepo.fields { + if field.name !in params { + return error('Missing parameter: ${field.name}.') + } + } + + repo.patch_from_params(params) return repo } @@ -115,8 +121,9 @@ fn (mut app App) post_repo() web.Result { return app.text('Unauthorized.') } - new_repo := repo_from_params(&app.query) or { - return app.server_error(400) + new_repo := repo_from_params(app.query) or { + app.set_status(400, err.msg) + return app.ok("") } id := rand.uuid_v4() @@ -190,7 +197,7 @@ fn (mut app App) patch_repo(id string) web.Result { return app.not_found() } - repos[id].patch_from_params(&app.query) + repos[id].patch_from_params(app.query) lock app.git_mutex { write_repos(app.conf.repos_file, &repos) or { return app.server_error(500) } From 0a6be87970fd7553eb664800c07e3822c9844564 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 28 Mar 2022 14:24:26 +0200 Subject: [PATCH 03/15] Start of web framework revamp --- src/server/git.v | 37 ++++---- src/server/response.v | 27 ++++++ src/server/routes.v | 18 ++-- src/web/web.v | 204 ++++-------------------------------------- 4 files changed, 70 insertions(+), 216 deletions(-) create mode 100644 src/server/response.v diff --git a/src/server/git.v b/src/server/git.v index 684e75a..0743ecc 100644 --- a/src/server/git.v +++ b/src/server/git.v @@ -78,31 +78,31 @@ fn write_repos(path string, repos &map[string]GitRepo) ? { ['/api/repos'; get] fn (mut app App) get_repos() web.Result { if !app.is_authorized() { - return app.text('Unauthorized.') + return app.json(401, new_response('Unauthorized.')) } repos := rlock app.git_mutex { read_repos(app.conf.repos_file) or { app.lerror('Failed to read repos file.') - return app.server_error(500) + return app.status(500) } } - return app.json(repos) + return app.json(200, new_data_response(repos)) } ['/api/repos/:id'; get] fn (mut app App) get_single_repo(id string) web.Result { if !app.is_authorized() { - return app.text('Unauthorized.') + return app.json(401, new_response('Unauthorized.')) } repos := rlock app.git_mutex { read_repos(app.conf.repos_file) or { app.lerror('Failed to read repos file.') - return app.server_error(500) + return app.status(500) } } @@ -112,18 +112,17 @@ fn (mut app App) get_single_repo(id string) web.Result { repo := repos[id] - return app.json(repo) + return app.json(200, new_data_response(repo)) } ['/api/repos'; post] fn (mut app App) post_repo() web.Result { if !app.is_authorized() { - return app.text('Unauthorized.') + return app.json(401, new_response('Unauthorized.')) } new_repo := repo_from_params(app.query) or { - app.set_status(400, err.msg) - return app.ok("") + return app.json(400, new_response(err.msg)) } id := rand.uuid_v4() @@ -132,37 +131,37 @@ fn (mut app App) post_repo() web.Result { read_repos(app.conf.repos_file) or { app.lerror('Failed to read repos file.') - return app.server_error(500) + return app.status(500) } } // We need to check for duplicates for _, repo in repos { if repo == new_repo { - return app.text('Duplicate repository.') + return app.json(400, new_response('Duplicate repository.')) } } repos[id] = new_repo lock app.git_mutex { - write_repos(app.conf.repos_file, &repos) or { return app.server_error(500) } + write_repos(app.conf.repos_file, &repos) or { return app.status(500) } } - return app.ok('Repo added successfully.') + return app.json(200, new_response('Repo added successfully.')) } ['/api/repos/:id'; delete] fn (mut app App) delete_repo(id string) web.Result { if !app.is_authorized() { - return app.text('Unauthorized.') + return app.json(401, new_response('Unauthorized.')) } mut repos := rlock app.git_mutex { read_repos(app.conf.repos_file) or { app.lerror('Failed to read repos file.') - return app.server_error(500) + return app.status(500) } } @@ -176,20 +175,20 @@ fn (mut app App) delete_repo(id string) web.Result { write_repos(app.conf.repos_file, &repos) or { return app.server_error(500) } } - return app.ok('Repo removed successfully.') + return app.json(200, new_response('Repo removed successfully.')) } ['/api/repos/:id'; patch] fn (mut app App) patch_repo(id string) web.Result { if !app.is_authorized() { - return app.text('Unauthorized.') + return app.json(401, new_response('Unauthorized.')) } mut repos := rlock app.git_mutex { read_repos(app.conf.repos_file) or { app.lerror('Failed to read repos file.') - return app.server_error(500) + return app.status(500) } } @@ -203,5 +202,5 @@ fn (mut app App) patch_repo(id string) web.Result { write_repos(app.conf.repos_file, &repos) or { return app.server_error(500) } } - return app.ok('Repo updated successfully.') + return app.json(200, new_response('Repo updated successfully.')) } diff --git a/src/server/response.v b/src/server/response.v new file mode 100644 index 0000000..376493f --- /dev/null +++ b/src/server/response.v @@ -0,0 +1,27 @@ +module server + +struct Response { + message string + data T +} + +fn new_response(message string) Response { + return Response{ + message: message + data: "" + } +} + +fn new_data_response(data T) Response { + return Response{ + message: "" + data: data + } +} + +fn new_full_response(message string, data T) Response { + return Response{ + message: message + data: data + } +} diff --git a/src/server/routes.v b/src/server/routes.v index 0090666..ef5d5a5 100644 --- a/src/server/routes.v +++ b/src/server/routes.v @@ -12,7 +12,7 @@ import net.http // server is still responsive. ['/health'; get] pub fn (mut app App) healthcheck() web.Result { - return app.text('Healthy') + return app.json(200, new_response('Healthy.')) } // get_root handles a GET request for a file on the root @@ -31,7 +31,7 @@ fn (mut app App) get_root(filename string) web.Result { // Scuffed way to respond to HEAD requests if app.req.method == http.Method.head { if os.exists(full_path) { - return app.ok('') + return app.status(200) } return app.not_found() @@ -43,7 +43,7 @@ fn (mut app App) get_root(filename string) web.Result { ['/publish'; post] fn (mut app App) put_package() web.Result { if !app.is_authorized() { - return app.text('Unauthorized.') + return app.json(401, new_response('Unauthorized.')) } mut pkg_path := '' @@ -64,14 +64,16 @@ fn (mut app App) put_package() web.Result { util.reader_to_file(mut app.reader, length.int(), pkg_path) or { app.lwarn("Failed to upload '$pkg_path'") - return app.text('Failed to upload file.') + return app.json(500, new_response('Failed to upload file.')) } sw.stop() app.ldebug("Upload of '$pkg_path' completed in ${sw.elapsed().seconds():.3}s.") } else { app.lwarn('Tried to upload package without specifying a Content-Length.') - return app.text("Content-Type header isn't set.") + + // length required + return app.status(411) } res := app.repo.add_from_path(pkg_path) or { @@ -79,17 +81,17 @@ fn (mut app App) put_package() web.Result { os.rm(pkg_path) or { app.lerror("Failed to remove download '$pkg_path': $err.msg") } - return app.text('Failed to add package.') + return app.json(500, new_response('Failed to add package.')) } if !res.added { os.rm(pkg_path) or { app.lerror("Failed to remove download '$pkg_path': $err.msg") } app.lwarn("Duplicate package '$res.pkg.full_name()'.") - return app.text('File already exists.') + return app.json(400, new_response('File already exists.')) } app.linfo("Added '$res.pkg.full_name()' to repository.") - return app.text('Package added successfully.') + return app.json(200, new_response('Package added successfully.')) } diff --git a/src/web/web.v b/src/web/web.v index ad647f2..4e93189 100644 --- a/src/web/web.v +++ b/src/web/web.v @@ -12,9 +12,6 @@ import time import json import log -// A type which don't get filtered inside templates -pub type RawHtml = string - // A dummy structure that returns from routes to indicate that you actually sent something to a user [noinit] pub struct Result {} @@ -142,7 +139,7 @@ pub const ( pub struct Context { mut: content_type string = 'text/plain' - status string = '200 OK' + status int = 200 pub: // HTTP Request req http.Request @@ -186,24 +183,14 @@ struct Route { path string } -// Defining this method is optional. -// init_server is called at server start. -// You can use it for initializing globals. -pub fn (ctx Context) init_server() { - eprintln('init_server() has been deprecated, please init your web app in `fn main()`') -} - // Defining this method is optional. // before_request is called before every request (aka middleware). // Probably you can use it for check user session cookie or add header. pub fn (ctx Context) before_request() {} -pub struct Cookie { - name string - value string - expires time.Time - secure bool - http_only bool +// send_string +fn send_string(mut conn net.TcpConn, s string) ? { + conn.write(s.bytes()) ? } // send_response_to_client sends a response to the client @@ -225,33 +212,24 @@ pub fn (mut ctx Context) send_response_to_client(mimetype string, res string) bo text: res } resp.set_version(.v1_1) - resp.set_status(http.status_from_int(ctx.status.int())) + resp.set_status(http.status_from_int(ctx.status)) send_string(mut ctx.conn, resp.bytestr()) or { return false } return true } -// html HTTP_OK with s as payload with content-type `text/html` -pub fn (mut ctx Context) html(s string) Result { - ctx.send_response_to_client('text/html', s) - return Result{} -} +pub fn (mut ctx Context) text(status int, s string) Result { + ctx.status = status -// text HTTP_OK with s as payload with content-type `text/plain` -pub fn (mut ctx Context) text(s string) Result { ctx.send_response_to_client('text/plain', s) + return Result{} } // json HTTP_OK with json_s as payload with content-type `application/json` -pub fn (mut ctx Context) json(j T) Result { - json_s := json.encode(j) - ctx.send_response_to_client('application/json', json_s) - return Result{} -} +pub fn (mut ctx Context) json(status int, j T) Result { + ctx.status = status -// json_pretty Response HTTP_OK with a pretty-printed JSON result -pub fn (mut ctx Context) json_pretty(j T) Result { - json_s := json.encode_pretty(j) + json_s := json.encode(j) ctx.send_response_to_client('application/json', json_s) return Result{} } @@ -302,7 +280,7 @@ pub fn (mut ctx Context) file(f_path string) Result { header: header.join(web.headers_close) } resp.set_version(.v1_1) - resp.set_status(http.status_from_int(ctx.status.int())) + resp.set_status(http.status_from_int(ctx.status)) send_string(mut ctx.conn, resp.bytestr()) or { return Result{} } mut buf := []byte{len: 1_000_000} @@ -328,10 +306,8 @@ pub fn (mut ctx Context) file(f_path string) Result { return Result{} } -// ok Response HTTP_OK with s as payload -pub fn (mut ctx Context) ok(s string) Result { - ctx.send_response_to_client(ctx.content_type, s) - return Result{} +pub fn (mut ctx Context) status(status int) Result { + return ctx.text(status, '') } // server_error Response a server error @@ -361,64 +337,7 @@ pub fn (mut ctx Context) redirect(url string) Result { // not_found Send an not_found response pub fn (mut ctx Context) not_found() Result { - if ctx.done { - return Result{} - } - ctx.done = true - send_string(mut ctx.conn, web.http_404.bytestr()) or {} - return Result{} -} - -// set_cookie Sets a cookie -pub fn (mut ctx Context) set_cookie(cookie Cookie) { - mut cookie_data := []string{} - mut secure := if cookie.secure { 'Secure;' } else { '' } - secure += if cookie.http_only { ' HttpOnly' } else { ' ' } - cookie_data << secure - if cookie.expires.unix > 0 { - cookie_data << 'expires=$cookie.expires.utc_string()' - } - data := cookie_data.join(' ') - ctx.add_header('Set-Cookie', '$cookie.name=$cookie.value; $data') -} - -// set_content_type Sets the response content type -pub fn (mut ctx Context) set_content_type(typ string) { - ctx.content_type = typ -} - -// set_cookie_with_expire_date Sets a cookie with a `expire_data` -pub fn (mut ctx Context) set_cookie_with_expire_date(key string, val string, expire_date time.Time) { - ctx.add_header('Set-Cookie', '$key=$val; Secure; HttpOnly; expires=$expire_date.utc_string()') -} - -// get_cookie Gets a cookie by a key -pub fn (ctx &Context) get_cookie(key string) ?string { // TODO refactor - mut cookie_header := ctx.get_header('cookie') - if cookie_header == '' { - cookie_header = ctx.get_header('Cookie') - } - cookie_header = ' ' + cookie_header - // println('cookie_header="$cookie_header"') - // println(ctx.req.header) - cookie := if cookie_header.contains(';') { - cookie_header.find_between(' $key=', ';') - } else { - cookie_header.find_between(' $key=', '\r') - } - if cookie != '' { - return cookie.trim_space() - } - return error('Cookie not found') -} - -// set_status Sets the response status -pub fn (mut ctx Context) set_status(code int, desc string) { - if code < 100 || code > 599 { - ctx.status = '500 Internal Server Error' - } else { - ctx.status = '$code $desc' - } + return ctx.status(404) } // add_header Adds an header to the response with key and val @@ -560,12 +479,6 @@ fn handle_conn(mut conn net.TcpConn, mut app T, routes map[string]Route) { // Calling middleware... app.before_request() - // Static handling - if serve_if_static(mut app, url) { - // successfully served a static file - return - } - // Route matching $for method in T.methods { $if method.return_type is Result { @@ -661,83 +574,6 @@ fn route_matches(url_words []string, route_words []string) ?[]string { return params } -// serve_if_static checks if request is for a static file and serves it -// returns true if we served a static file, false otherwise -[manualfree] -fn serve_if_static(mut app T, url urllib.URL) bool { - // TODO: handle url parameters properly - for now, ignore them - static_file := app.static_files[url.path] - mime_type := app.static_mime_types[url.path] - if static_file == '' || mime_type == '' { - return false - } - data := os.read_file(static_file) or { - send_string(mut app.conn, web.http_404.bytestr()) or {} - return true - } - app.send_response_to_client(mime_type, data) - unsafe { data.free() } - return true -} - -// scan_static_directory makes a static route for each file in a directory -fn (mut ctx Context) scan_static_directory(directory_path string, mount_path string) { - files := os.ls(directory_path) or { panic(err) } - if files.len > 0 { - for file in files { - full_path := os.join_path(directory_path, file) - if os.is_dir(full_path) { - ctx.scan_static_directory(full_path, mount_path + '/' + file) - } else if file.contains('.') && !file.starts_with('.') && !file.ends_with('.') { - ext := os.file_ext(file) - // Rudimentary guard against adding files not in mime_types. - // Use serve_static directly to add non-standard mime types. - if ext in web.mime_types { - ctx.serve_static(mount_path + '/' + file, full_path) - } - } - } - } -} - -// handle_static Handles a directory static -// If `root` is set the mount path for the dir will be in '/' -pub fn (mut ctx Context) handle_static(directory_path string, root bool) bool { - if ctx.done || !os.exists(directory_path) { - return false - } - dir_path := directory_path.trim_space().trim_right('/') - mut mount_path := '' - if dir_path != '.' && os.is_dir(dir_path) && !root { - // Mount point hygene, "./assets" => "/assets". - mount_path = '/' + dir_path.trim_left('.').trim('/') - } - ctx.scan_static_directory(dir_path, mount_path) - return true -} - -// mount_static_folder_at - makes all static files in `directory_path` and inside it, available at http://server/mount_path -// For example: suppose you have called .mount_static_folder_at('/var/share/myassets', '/assets'), -// and you have a file /var/share/myassets/main.css . -// => That file will be available at URL: http://server/assets/main.css . -pub fn (mut ctx Context) mount_static_folder_at(directory_path string, mount_path string) bool { - if ctx.done || mount_path.len < 1 || mount_path[0] != `/` || !os.exists(directory_path) { - return false - } - dir_path := directory_path.trim_right('/') - ctx.scan_static_directory(dir_path, mount_path[1..]) - return true -} - -// serve_static Serves a file static -// `url` is the access path on the site, `file_path` is the real path to the file, `mime_type` is the file type -pub fn (mut ctx Context) serve_static(url string, file_path string) { - ctx.static_files[url] = file_path - // ctx.static_mime_types[url] = mime_type - ext := os.file_ext(file_path) - ctx.static_mime_types[url] = web.mime_types[ext] -} - // ip Returns the ip address from the current user pub fn (ctx &Context) ip() string { mut ip := ctx.req.header.get(.x_forwarded_for) or { '' } @@ -760,16 +596,6 @@ pub fn (mut ctx Context) error(s string) { ctx.form_error = s } -// not_found Returns an empty result -pub fn not_found() Result { - return Result{} -} - -// send_string -fn send_string(mut conn net.TcpConn, s string) ? { - conn.write(s.bytes()) ? -} - // filter Do not delete. // It used by `vlib/v/gen/c/str_intp.v:130` for string interpolation inside web templates // TODO: move it to template render From e5a630e9907c61f2fa4af116659eed595671cc15 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 28 Mar 2022 14:44:23 +0200 Subject: [PATCH 04/15] Switched to net.http.Status for status codes --- src/server/git.v | 37 +++++++++++++++++++------------------ src/server/routes.v | 16 ++++++++-------- src/web/web.v | 14 +++++++------- 3 files changed, 34 insertions(+), 33 deletions(-) diff --git a/src/server/git.v b/src/server/git.v index 0743ecc..52cd262 100644 --- a/src/server/git.v +++ b/src/server/git.v @@ -4,6 +4,7 @@ import web import os import json import rand +import net.http pub struct GitRepo { pub mut: @@ -78,31 +79,31 @@ fn write_repos(path string, repos &map[string]GitRepo) ? { ['/api/repos'; get] fn (mut app App) get_repos() web.Result { if !app.is_authorized() { - return app.json(401, new_response('Unauthorized.')) + return app.json(http.Status.unauthorized, new_response('Unauthorized.')) } repos := rlock app.git_mutex { read_repos(app.conf.repos_file) or { app.lerror('Failed to read repos file.') - return app.status(500) + return app.status(http.Status.internal_server_error) } } - return app.json(200, new_data_response(repos)) + return app.json(http.Status.ok, new_data_response(repos)) } ['/api/repos/:id'; get] fn (mut app App) get_single_repo(id string) web.Result { if !app.is_authorized() { - return app.json(401, new_response('Unauthorized.')) + return app.json(http.Status.unauthorized, new_response('Unauthorized.')) } repos := rlock app.git_mutex { read_repos(app.conf.repos_file) or { app.lerror('Failed to read repos file.') - return app.status(500) + return app.status(http.Status.internal_server_error) } } @@ -112,17 +113,17 @@ fn (mut app App) get_single_repo(id string) web.Result { repo := repos[id] - return app.json(200, new_data_response(repo)) + return app.json(http.Status.ok, new_data_response(repo)) } ['/api/repos'; post] fn (mut app App) post_repo() web.Result { if !app.is_authorized() { - return app.json(401, new_response('Unauthorized.')) + return app.json(http.Status.unauthorized, new_response('Unauthorized.')) } new_repo := repo_from_params(app.query) or { - return app.json(400, new_response(err.msg)) + return app.json(http.Status.bad_request, new_response(err.msg)) } id := rand.uuid_v4() @@ -131,37 +132,37 @@ fn (mut app App) post_repo() web.Result { read_repos(app.conf.repos_file) or { app.lerror('Failed to read repos file.') - return app.status(500) + return app.status(http.Status.internal_server_error) } } // We need to check for duplicates for _, repo in repos { if repo == new_repo { - return app.json(400, new_response('Duplicate repository.')) + return app.json(http.Status.bad_request, new_response('Duplicate repository.')) } } repos[id] = new_repo lock app.git_mutex { - write_repos(app.conf.repos_file, &repos) or { return app.status(500) } + write_repos(app.conf.repos_file, &repos) or { return app.status(http.Status.internal_server_error) } } - return app.json(200, new_response('Repo added successfully.')) + return app.json(http.Status.ok, new_response('Repo added successfully.')) } ['/api/repos/:id'; delete] fn (mut app App) delete_repo(id string) web.Result { if !app.is_authorized() { - return app.json(401, new_response('Unauthorized.')) + return app.json(http.Status.unauthorized, new_response('Unauthorized.')) } mut repos := rlock app.git_mutex { read_repos(app.conf.repos_file) or { app.lerror('Failed to read repos file.') - return app.status(500) + return app.status(http.Status.internal_server_error) } } @@ -175,20 +176,20 @@ fn (mut app App) delete_repo(id string) web.Result { write_repos(app.conf.repos_file, &repos) or { return app.server_error(500) } } - return app.json(200, new_response('Repo removed successfully.')) + return app.json(http.Status.ok, new_response('Repo removed successfully.')) } ['/api/repos/:id'; patch] fn (mut app App) patch_repo(id string) web.Result { if !app.is_authorized() { - return app.json(401, new_response('Unauthorized.')) + return app.json(http.Status.unauthorized, new_response('Unauthorized.')) } mut repos := rlock app.git_mutex { read_repos(app.conf.repos_file) or { app.lerror('Failed to read repos file.') - return app.status(500) + return app.status(http.Status.internal_server_error) } } @@ -202,5 +203,5 @@ fn (mut app App) patch_repo(id string) web.Result { write_repos(app.conf.repos_file, &repos) or { return app.server_error(500) } } - return app.json(200, new_response('Repo updated successfully.')) + return app.json(http.Status.ok, new_response('Repo updated successfully.')) } diff --git a/src/server/routes.v b/src/server/routes.v index ef5d5a5..0e7d72c 100644 --- a/src/server/routes.v +++ b/src/server/routes.v @@ -12,7 +12,7 @@ import net.http // server is still responsive. ['/health'; get] pub fn (mut app App) healthcheck() web.Result { - return app.json(200, new_response('Healthy.')) + return app.json(http.Status.ok, new_response('Healthy.')) } // get_root handles a GET request for a file on the root @@ -31,7 +31,7 @@ fn (mut app App) get_root(filename string) web.Result { // Scuffed way to respond to HEAD requests if app.req.method == http.Method.head { if os.exists(full_path) { - return app.status(200) + return app.status(http.Status.ok) } return app.not_found() @@ -43,7 +43,7 @@ fn (mut app App) get_root(filename string) web.Result { ['/publish'; post] fn (mut app App) put_package() web.Result { if !app.is_authorized() { - return app.json(401, new_response('Unauthorized.')) + return app.json(http.Status.unauthorized, new_response('Unauthorized.')) } mut pkg_path := '' @@ -64,7 +64,7 @@ fn (mut app App) put_package() web.Result { util.reader_to_file(mut app.reader, length.int(), pkg_path) or { app.lwarn("Failed to upload '$pkg_path'") - return app.json(500, new_response('Failed to upload file.')) + return app.json(http.Status.internal_server_error, new_response('Failed to upload file.')) } sw.stop() @@ -73,7 +73,7 @@ fn (mut app App) put_package() web.Result { app.lwarn('Tried to upload package without specifying a Content-Length.') // length required - return app.status(411) + return app.status(http.Status.length_required) } res := app.repo.add_from_path(pkg_path) or { @@ -81,17 +81,17 @@ fn (mut app App) put_package() web.Result { os.rm(pkg_path) or { app.lerror("Failed to remove download '$pkg_path': $err.msg") } - return app.json(500, new_response('Failed to add package.')) + return app.json(http.Status.internal_server_error, new_response('Failed to add package.')) } if !res.added { os.rm(pkg_path) or { app.lerror("Failed to remove download '$pkg_path': $err.msg") } app.lwarn("Duplicate package '$res.pkg.full_name()'.") - return app.json(400, new_response('File already exists.')) + return app.json(http.Status.bad_request, new_response('File already exists.')) } app.linfo("Added '$res.pkg.full_name()' to repository.") - return app.json(200, new_response('Package added successfully.')) + return app.json(http.Status.ok, new_response('Package added successfully.')) } diff --git a/src/web/web.v b/src/web/web.v index 4e93189..23ae746 100644 --- a/src/web/web.v +++ b/src/web/web.v @@ -139,7 +139,7 @@ pub const ( pub struct Context { mut: content_type string = 'text/plain' - status int = 200 + status http.Status = http.Status.ok pub: // HTTP Request req http.Request @@ -212,12 +212,12 @@ pub fn (mut ctx Context) send_response_to_client(mimetype string, res string) bo text: res } resp.set_version(.v1_1) - resp.set_status(http.status_from_int(ctx.status)) + resp.set_status(ctx.status) send_string(mut ctx.conn, resp.bytestr()) or { return false } return true } -pub fn (mut ctx Context) text(status int, s string) Result { +pub fn (mut ctx Context) text(status http.Status, s string) Result { ctx.status = status ctx.send_response_to_client('text/plain', s) @@ -226,7 +226,7 @@ pub fn (mut ctx Context) text(status int, s string) Result { } // json HTTP_OK with json_s as payload with content-type `application/json` -pub fn (mut ctx Context) json(status int, j T) Result { +pub fn (mut ctx Context) json(status http.Status, j T) Result { ctx.status = status json_s := json.encode(j) @@ -280,7 +280,7 @@ pub fn (mut ctx Context) file(f_path string) Result { header: header.join(web.headers_close) } resp.set_version(.v1_1) - resp.set_status(http.status_from_int(ctx.status)) + resp.set_status(ctx.status) send_string(mut ctx.conn, resp.bytestr()) or { return Result{} } mut buf := []byte{len: 1_000_000} @@ -306,7 +306,7 @@ pub fn (mut ctx Context) file(f_path string) Result { return Result{} } -pub fn (mut ctx Context) status(status int) Result { +pub fn (mut ctx Context) status(status http.Status) Result { return ctx.text(status, '') } @@ -337,7 +337,7 @@ pub fn (mut ctx Context) redirect(url string) Result { // not_found Send an not_found response pub fn (mut ctx Context) not_found() Result { - return ctx.status(404) + return ctx.status(http.Status.not_found) } // add_header Adds an header to the response with key and val From 148ec3ab47a496d63055864b7f15a3323b82c214 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Fri, 1 Apr 2022 21:33:55 +0200 Subject: [PATCH 05/15] Updated changelog --- CHANGELOG.md | 3 +++ src/web/web.v | 1 + 2 files changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9eaf477..f95351f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Better environment variable support * Each env var can now be provided from a file by appending it with `_FILE` & passing the path to the file as value +* Revamped web framework + * All routes now return proper JSON where applicable & the correct status + codes ## Added diff --git a/src/web/web.v b/src/web/web.v index 23ae746..6e07aea 100644 --- a/src/web/web.v +++ b/src/web/web.v @@ -231,6 +231,7 @@ pub fn (mut ctx Context) json(status http.Status, j T) Result { json_s := json.encode(j) ctx.send_response_to_client('application/json', json_s) + return Result{} } From 3a6effad807b5bd7b0acd9c97699555ad1f3dffa Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Fri, 1 Apr 2022 21:34:58 +0200 Subject: [PATCH 06/15] Ran vfmt --- src/server/git.v | 13 +++++++------ src/server/response.v | 6 +++--- src/web/web.v | 4 ++-- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/server/git.v b/src/server/git.v index 52cd262..8862af8 100644 --- a/src/server/git.v +++ b/src/server/git.v @@ -9,12 +9,12 @@ import net.http pub struct GitRepo { pub mut: // URL of the Git repository - url string + url string // Branch of the Git repository to use branch string // On which architectures the package is allowed to be built. In reality, // this controls which builders will periodically build the image. - arch []string + arch []string } fn (mut r GitRepo) patch_from_params(params map[string]string) { @@ -22,8 +22,8 @@ fn (mut r GitRepo) patch_from_params(params map[string]string) { if field.name in params { $if field.typ is string { r.$(field.name) = params[field.name] - // This specific type check is needed for the compiler to ensure - // our types are correct + // This specific type check is needed for the compiler to ensure + // our types are correct } $else $if field.typ is []string { r.$(field.name) = params[field.name].split(',') } @@ -41,7 +41,6 @@ fn repo_from_params(params map[string]string) ?GitRepo { return error('Missing parameter: ${field.name}.') } } - repo.patch_from_params(params) return repo @@ -146,7 +145,9 @@ fn (mut app App) post_repo() web.Result { repos[id] = new_repo lock app.git_mutex { - write_repos(app.conf.repos_file, &repos) or { return app.status(http.Status.internal_server_error) } + write_repos(app.conf.repos_file, &repos) or { + return app.status(http.Status.internal_server_error) + } } return app.json(http.Status.ok, new_response('Repo added successfully.')) diff --git a/src/server/response.v b/src/server/response.v index 376493f..354e7da 100644 --- a/src/server/response.v +++ b/src/server/response.v @@ -2,19 +2,19 @@ module server struct Response { message string - data T + data T } fn new_response(message string) Response { return Response{ message: message - data: "" + data: '' } } fn new_data_response(data T) Response { return Response{ - message: "" + message: '' data: data } } diff --git a/src/web/web.v b/src/web/web.v index 6e07aea..eb8d5df 100644 --- a/src/web/web.v +++ b/src/web/web.v @@ -138,8 +138,8 @@ pub const ( // It has fields for the query, form, files. pub struct Context { mut: - content_type string = 'text/plain' - status http.Status = http.Status.ok + content_type string = 'text/plain' + status http.Status = http.Status.ok pub: // HTTP Request req http.Request From fe247748480c722f0fbb41f5bf2203972af7e851 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Fri, 1 Apr 2022 21:50:00 +0200 Subject: [PATCH 07/15] Added some docs --- src/web/web.v | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/web/web.v b/src/web/web.v index eb8d5df..000c6a6 100644 --- a/src/web/web.v +++ b/src/web/web.v @@ -217,6 +217,7 @@ pub fn (mut ctx Context) send_response_to_client(mimetype string, res string) bo return true } +// text responds to a request with some plaintext. pub fn (mut ctx Context) text(status http.Status, s string) Result { ctx.status = status @@ -307,6 +308,8 @@ pub fn (mut ctx Context) file(f_path string) Result { return Result{} } +// status responds with an empty textual response, essentially only returning +// the given status code. pub fn (mut ctx Context) status(status http.Status) Result { return ctx.text(status, '') } From 56517c0ff011d5d88adeeba406f2596fca9e4b0b Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Fri, 1 Apr 2022 23:15:30 +0200 Subject: [PATCH 08/15] Removed arm/v7 from CI --- .woodpecker/.build.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.woodpecker/.build.yml b/.woodpecker/.build.yml index 4cddc6a..81fa32f 100644 --- a/.woodpecker/.build.yml +++ b/.woodpecker/.build.yml @@ -2,7 +2,8 @@ matrix: PLATFORM: - linux/amd64 - linux/arm64 - - linux/arm/v7 + # I just don't have a performant enough runner for this platform + # - linux/arm/v7 # These checks already get performed on the feature branches platform: ${PLATFORM} From d7f6c87053fda009c614521ec63c532b6b9a1af8 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Wed, 6 Apr 2022 21:08:50 +0200 Subject: [PATCH 09/15] Added some comments --- src/git/git.v | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/git/git.v b/src/git/git.v index 3ad35fd..c5390b6 100644 --- a/src/git/git.v +++ b/src/git/git.v @@ -14,6 +14,8 @@ pub mut: arch []string } +// patch_from_params patches a GitRepo from a map[string]string, usually +// provided from a web.App's params pub fn (mut r GitRepo) patch_from_params(params map[string]string) { $for field in GitRepo.fields { if field.name in params { @@ -28,6 +30,7 @@ pub fn (mut r GitRepo) patch_from_params(params map[string]string) { } } +// read_repos reads the provided path & parses it into a map of GitRepo's. pub fn read_repos(path string) ?map[string]GitRepo { if !os.exists(path) { mut f := os.create(path) ? @@ -47,6 +50,7 @@ pub fn read_repos(path string) ?map[string]GitRepo { return res } +// write_repos writes a map of GitRepo's back to disk given the provided path. pub fn write_repos(path string, repos &map[string]GitRepo) ? { mut f := os.create(path) ? @@ -58,6 +62,8 @@ pub fn write_repos(path string, repos &map[string]GitRepo) ? { f.write_string(value) ? } +// repo_from_params creates a GitRepo from a map[string]string, usually +// provided from a web.App's params pub fn repo_from_params(params map[string]string) ?GitRepo { mut repo := GitRepo{} From f44ce1c17f572bb788e2002c877fc0678d71344c Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Wed, 6 Apr 2022 22:41:19 +0200 Subject: [PATCH 10/15] Started work on better repos cli --- src/git/cli.v | 16 +++++++++++----- src/response.v | 27 +++++++++++++++++++++++++++ src/server/git.v | 3 ++- src/server/response.v | 27 --------------------------- src/server/routes.v | 1 + 5 files changed, 41 insertions(+), 33 deletions(-) create mode 100644 src/response.v delete mode 100644 src/server/response.v diff --git a/src/git/cli.v b/src/git/cli.v index 17fa984..586b5ba 100644 --- a/src/git/cli.v +++ b/src/git/cli.v @@ -3,6 +3,9 @@ module git import cli import env import net.http +import json +import git +import response struct Config { address string [required] @@ -28,13 +31,13 @@ pub fn cmd() cli.Command { cli.Command{ name: 'add' required_args: 2 - usage: 'url branch' + usage: 'url branch arch...' description: 'Add a new repository.' execute: fn (cmd cli.Command) ? { config_file := cmd.flags.get_string('config-file') ? conf := env.load(config_file) ? - add(conf, cmd.args[0], cmd.args[1]) ? + add(conf, cmd.args[0], cmd.args[1], cmd.args[2..]) ? } }, cli.Command{ @@ -58,12 +61,15 @@ fn list(conf Config) ? { req.add_custom_header('X-API-Key', conf.api_key) ? res := req.do() ? + data := json.decode(response.Response, res.text) ? - println(res.text) + for id, details in data.data { + println("${id[..8]}\t$details.url\t$details.branch\t$details.arch") + } } -fn add(conf Config, url string, branch string) ? { - mut req := http.new_request(http.Method.post, '$conf.address/api/repos?url=$url&branch=$branch', +fn add(conf Config, url string, branch string, arch []string) ? { + mut req := http.new_request(http.Method.post, '$conf.address/api/repos?url=$url&branch=$branch&arch=${arch.join(',')}', '') ? req.add_custom_header('X-API-Key', conf.api_key) ? diff --git a/src/response.v b/src/response.v new file mode 100644 index 0000000..1618fcf --- /dev/null +++ b/src/response.v @@ -0,0 +1,27 @@ +module response + +pub struct Response { + message string + data T +} + +pub fn new_response(message string) Response { + return Response{ + message: message + data: '' + } +} + +pub fn new_data_response(data T) Response { + return Response{ + message: '' + data: data + } +} + +pub fn new_full_response(message string, data T) Response { + return Response{ + message: message + data: data + } +} diff --git a/src/server/git.v b/src/server/git.v index 7d177d7..359d6ce 100644 --- a/src/server/git.v +++ b/src/server/git.v @@ -4,6 +4,7 @@ import web import git import net.http import rand +import response { new_response, new_data_response } const repos_file = 'repos.json' @@ -15,7 +16,7 @@ fn (mut app App) get_repos() web.Result { repos := rlock app.git_mutex { git.read_repos(app.conf.repos_file) or { - app.lerror('Failed to read repos file.') + app.lerror('Failed to read repos file: $err.msg') return app.status(http.Status.internal_server_error) } diff --git a/src/server/response.v b/src/server/response.v deleted file mode 100644 index 354e7da..0000000 --- a/src/server/response.v +++ /dev/null @@ -1,27 +0,0 @@ -module server - -struct Response { - message string - data T -} - -fn new_response(message string) Response { - return Response{ - message: message - data: '' - } -} - -fn new_data_response(data T) Response { - return Response{ - message: '' - data: data - } -} - -fn new_full_response(message string, data T) Response { - return Response{ - message: message - data: data - } -} diff --git a/src/server/routes.v b/src/server/routes.v index 0e7d72c..71da02a 100644 --- a/src/server/routes.v +++ b/src/server/routes.v @@ -7,6 +7,7 @@ import time import rand import util import net.http +import response { new_response, new_data_response } // healthcheck just returns a string, but can be used to quickly check if the // server is still responsive. From 7eb0aa76e179d041e817ccbdb0632fad93256e82 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Thu, 7 Apr 2022 11:54:20 +0200 Subject: [PATCH 11/15] CLI tool can now work with new repo UUIDs --- src/git/cli.v | 42 ++++++++++++++++++++++++++++++++++-------- vieter.toml | 2 +- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/src/git/cli.v b/src/git/cli.v index 586b5ba..c4d4d80 100644 --- a/src/git/cli.v +++ b/src/git/cli.v @@ -42,28 +42,34 @@ pub fn cmd() cli.Command { }, cli.Command{ name: 'remove' - required_args: 2 - usage: 'url branch' - description: 'Remove a repository.' + required_args: 1 + usage: 'id' + description: 'Remove a repository that matches the given ID prefix.' execute: fn (cmd cli.Command) ? { config_file := cmd.flags.get_string('config-file') ? conf := env.load(config_file) ? - remove(conf, cmd.args[0], cmd.args[1]) ? + remove(conf, cmd.args[0]) ? } }, ] } } -fn list(conf Config) ? { +fn get_repos(conf Config) ?map[string]git.GitRepo { mut req := http.new_request(http.Method.get, '$conf.address/api/repos', '') ? req.add_custom_header('X-API-Key', conf.api_key) ? res := req.do() ? data := json.decode(response.Response, res.text) ? - for id, details in data.data { + return data.data +} + +fn list(conf Config) ? { + repos := get_repos(conf) ? + + for id, details in repos { println("${id[..8]}\t$details.url\t$details.branch\t$details.arch") } } @@ -78,8 +84,28 @@ fn add(conf Config, url string, branch string, arch []string) ? { println(res.text) } -fn remove(conf Config, url string, branch string) ? { - mut req := http.new_request(http.Method.delete, '$conf.address/api/repos?url=$url&branch=$branch', +fn remove(conf Config, id_prefix string) ? { + repos := get_repos(conf) ? + + mut to_remove := []string{} + + for id, _ in repos { + if id.starts_with(id_prefix) { + to_remove << id + } + } + + if to_remove.len == 0 { + eprintln("No repo found for given prefix.") + exit(1) + } + + if to_remove.len > 1 { + eprintln("Multiple repos found for given prefix.") + exit(1) + } + + mut req := http.new_request(http.Method.delete, '$conf.address/api/repos/${to_remove[0]}', '') ? req.add_custom_header('X-API-Key', conf.api_key) ? diff --git a/vieter.toml b/vieter.toml index c17c3c3..7592942 100644 --- a/vieter.toml +++ b/vieter.toml @@ -3,7 +3,7 @@ api_key = "test" download_dir = "data/downloads" repo_dir = "data/repo" pkg_dir = "data/pkgs" -# log_level = "DEBUG" +log_level = "DEBUG" repos_file = "data/repos.json" address = "http://localhost:8000" From b31b4cbd7a9fc1b64061c253e4260f66d56e0430 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Thu, 7 Apr 2022 11:58:05 +0200 Subject: [PATCH 12/15] Pleased vfmt & vet --- src/git/cli.v | 11 +++++------ src/response.v | 6 ++++++ src/server/git.v | 2 +- src/server/routes.v | 2 +- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/git/cli.v b/src/git/cli.v index c4d4d80..7e41b1c 100644 --- a/src/git/cli.v +++ b/src/git/cli.v @@ -4,7 +4,6 @@ import cli import env import net.http import json -import git import response struct Config { @@ -56,12 +55,12 @@ pub fn cmd() cli.Command { } } -fn get_repos(conf Config) ?map[string]git.GitRepo { +fn get_repos(conf Config) ?map[string]GitRepo { mut req := http.new_request(http.Method.get, '$conf.address/api/repos', '') ? req.add_custom_header('X-API-Key', conf.api_key) ? res := req.do() ? - data := json.decode(response.Response, res.text) ? + data := json.decode(response.Response, res.text) ? return data.data } @@ -70,7 +69,7 @@ fn list(conf Config) ? { repos := get_repos(conf) ? for id, details in repos { - println("${id[..8]}\t$details.url\t$details.branch\t$details.arch") + println('${id[..8]}\t$details.url\t$details.branch\t$details.arch') } } @@ -96,12 +95,12 @@ fn remove(conf Config, id_prefix string) ? { } if to_remove.len == 0 { - eprintln("No repo found for given prefix.") + eprintln('No repo found for given prefix.') exit(1) } if to_remove.len > 1 { - eprintln("Multiple repos found for given prefix.") + eprintln('Multiple repos found for given prefix.') exit(1) } diff --git a/src/response.v b/src/response.v index 1618fcf..7e268b1 100644 --- a/src/response.v +++ b/src/response.v @@ -5,6 +5,8 @@ pub struct Response { data T } +// new_response constructs a new Response object with the given message +// & an empty data field. pub fn new_response(message string) Response { return Response{ message: message @@ -12,6 +14,8 @@ pub fn new_response(message string) Response { } } +// new_data_response constructs a new Response object with the given data +// & an empty message field. pub fn new_data_response(data T) Response { return Response{ message: '' @@ -19,6 +23,8 @@ pub fn new_data_response(data T) Response { } } +// new_full_response constructs a new Response object with the given +// message & data. pub fn new_full_response(message string, data T) Response { return Response{ message: message diff --git a/src/server/git.v b/src/server/git.v index 359d6ce..2a682d8 100644 --- a/src/server/git.v +++ b/src/server/git.v @@ -4,7 +4,7 @@ import web import git import net.http import rand -import response { new_response, new_data_response } +import response { new_data_response, new_response } const repos_file = 'repos.json' diff --git a/src/server/routes.v b/src/server/routes.v index 71da02a..07279cb 100644 --- a/src/server/routes.v +++ b/src/server/routes.v @@ -7,7 +7,7 @@ import time import rand import util import net.http -import response { new_response, new_data_response } +import response { new_response } // healthcheck just returns a string, but can be used to quickly check if the // server is still responsive. From fc1d4480dc220e6f6245b6d6add8a24550f39493 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Thu, 7 Apr 2022 12:07:56 +0200 Subject: [PATCH 13/15] Split Git client code into separate module --- src/git/cli.v | 33 ++++++--------------------------- src/git/client.v | 36 ++++++++++++++++++++++++++++++++++++ src/response.v | 1 + 3 files changed, 43 insertions(+), 27 deletions(-) create mode 100644 src/git/client.v diff --git a/src/git/cli.v b/src/git/cli.v index 7e41b1c..4a066d5 100644 --- a/src/git/cli.v +++ b/src/git/cli.v @@ -2,9 +2,6 @@ module git import cli import env -import net.http -import json -import response struct Config { address string [required] @@ -55,18 +52,8 @@ pub fn cmd() cli.Command { } } -fn get_repos(conf Config) ?map[string]GitRepo { - mut req := http.new_request(http.Method.get, '$conf.address/api/repos', '') ? - req.add_custom_header('X-API-Key', conf.api_key) ? - - res := req.do() ? - data := json.decode(response.Response, res.text) ? - - return data.data -} - fn list(conf Config) ? { - repos := get_repos(conf) ? + repos := get_repos(conf.address, conf.api_key) ? for id, details in repos { println('${id[..8]}\t$details.url\t$details.branch\t$details.arch') @@ -74,17 +61,13 @@ fn list(conf Config) ? { } fn add(conf Config, url string, branch string, arch []string) ? { - mut req := http.new_request(http.Method.post, '$conf.address/api/repos?url=$url&branch=$branch&arch=${arch.join(',')}', - '') ? - req.add_custom_header('X-API-Key', conf.api_key) ? + res := add_repo(conf.address, conf.api_key, url, branch, arch) ? - res := req.do() ? - - println(res.text) + println(res.message) } fn remove(conf Config, id_prefix string) ? { - repos := get_repos(conf) ? + repos := get_repos(conf.address, conf.api_key) ? mut to_remove := []string{} @@ -104,11 +87,7 @@ fn remove(conf Config, id_prefix string) ? { exit(1) } - mut req := http.new_request(http.Method.delete, '$conf.address/api/repos/${to_remove[0]}', - '') ? - req.add_custom_header('X-API-Key', conf.api_key) ? + res := remove_repo(conf.address, conf.api_key, to_remove[0]) ? - res := req.do() ? - - println(res.text) + println(res.message) } diff --git a/src/git/client.v b/src/git/client.v new file mode 100644 index 0000000..3da1a61 --- /dev/null +++ b/src/git/client.v @@ -0,0 +1,36 @@ +module git + +import json +import response { Response } +import net.http + +fn get_repos(address string, api_key string) ?map[string]GitRepo { + mut req := http.new_request(http.Method.get, '$address/api/repos', '') ? + req.add_custom_header('X-API-Key', api_key) ? + + res := req.do() ? + data := json.decode(Response, res.text) ? + + return data.data +} + +fn add_repo(address string, api_key string, url string, branch string, arch []string) ?Response { + mut req := http.new_request(http.Method.post, '$address/api/repos?url=$url&branch=$branch&arch=${arch.join(',')}', + '') ? + req.add_custom_header('X-API-Key', api_key) ? + + res := req.do() ? + data := json.decode(Response, res.text) ? + + return data +} + +fn remove_repo(address string, api_key string, id string) ?Response { + mut req := http.new_request(http.Method.delete, '$address/api/repos/$id', '') ? + req.add_custom_header('X-API-Key', api_key) ? + + res := req.do() ? + data := json.decode(Response, res.text) ? + + return data +} diff --git a/src/response.v b/src/response.v index 7e268b1..a06a589 100644 --- a/src/response.v +++ b/src/response.v @@ -1,6 +1,7 @@ module response pub struct Response { +pub: message string data T } From 78310fa1e425652fb0f2f2e29865a6f2b9310cf0 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Thu, 7 Apr 2022 12:10:37 +0200 Subject: [PATCH 14/15] Builder now uses new Git repos API --- src/build/build.v | 10 ++-------- src/git/client.v | 9 ++++++--- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/build/build.v b/src/build/build.v index 934627f..c42c98d 100644 --- a/src/build/build.v +++ b/src/build/build.v @@ -3,9 +3,7 @@ module build import docker import encoding.base64 import time -import net.http import git -import json const container_build_dir = '/build' @@ -63,11 +61,7 @@ fn create_build_image() ?string { fn build(conf Config) ? { // We get the repos list from the Vieter instance - mut req := http.new_request(http.Method.get, '$conf.address/api/repos', '') ? - req.add_custom_header('X-Api-Key', conf.api_key) ? - - res := req.do() ? - repos := json.decode([]git.GitRepo, res.text) ? + repos := git.get_repos(conf.address, conf.api_key) ? // No point in doing work if there's no repos present if repos.len == 0 { @@ -77,7 +71,7 @@ fn build(conf Config) ? { // First, we create a base image which has updated repos n stuff image_id := create_build_image() ? - for repo in repos { + for _, repo in repos { // TODO what to do with PKGBUILDs that build multiple packages? commands := [ 'git clone --single-branch --depth 1 --branch $repo.branch $repo.url repo', diff --git a/src/git/client.v b/src/git/client.v index 3da1a61..97fe9fb 100644 --- a/src/git/client.v +++ b/src/git/client.v @@ -4,7 +4,8 @@ import json import response { Response } import net.http -fn get_repos(address string, api_key string) ?map[string]GitRepo { +// get_repos returns the current list of repos. +pub fn get_repos(address string, api_key string) ?map[string]GitRepo { mut req := http.new_request(http.Method.get, '$address/api/repos', '') ? req.add_custom_header('X-API-Key', api_key) ? @@ -14,7 +15,8 @@ fn get_repos(address string, api_key string) ?map[string]GitRepo { return data.data } -fn add_repo(address string, api_key string, url string, branch string, arch []string) ?Response { +// add_repo adds a new repo to the server. +pub fn add_repo(address string, api_key string, url string, branch string, arch []string) ?Response { mut req := http.new_request(http.Method.post, '$address/api/repos?url=$url&branch=$branch&arch=${arch.join(',')}', '') ? req.add_custom_header('X-API-Key', api_key) ? @@ -25,7 +27,8 @@ fn add_repo(address string, api_key string, url string, branch string, arch []st return data } -fn remove_repo(address string, api_key string, id string) ?Response { +// remove_repo removes the repo with the given ID from the server. +pub fn remove_repo(address string, api_key string, id string) ?Response { mut req := http.new_request(http.Method.delete, '$address/api/repos/$id', '') ? req.add_custom_header('X-API-Key', api_key) ? From 7895240e9b538d5544bf063bad549fcebc8d10cb Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Thu, 7 Apr 2022 12:22:41 +0200 Subject: [PATCH 15/15] Updatd CHANGELOG.md --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f95351f..96f7dcf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Changed -* Better environment variable support +* Better config system + * Support for both a config file & environment variables * Each env var can now be provided from a file by appending it with `_FILE` & passing the path to the file as value * Revamped web framework @@ -21,7 +22,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Very basic build system * Build is triggered by separate cron container * Packages build on cron container's system - * Packages are always rebuilt, even if they haven't changed * Hardcoded planning of builds * Builds are sequential * API for managing Git repositories to build