diff --git a/.woodpecker/.build.yml b/.woodpecker/.build.yml index e68c4c9..16e5a69 100644 --- a/.woodpecker/.build.yml +++ b/.woodpecker/.build.yml @@ -2,8 +2,6 @@ matrix: PLATFORM: - linux/amd64 - linux/arm64 - # 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} diff --git a/CHANGELOG.md b/CHANGELOG.md index e1daaec..754f04e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,13 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Changed -* Better config system - * Support for both a config file & environment variables +* 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/build/build.v b/src/build/build.v index c42c98d..934627f 100644 --- a/src/build/build.v +++ b/src/build/build.v @@ -3,7 +3,9 @@ module build import docker import encoding.base64 import time +import net.http import git +import json const container_build_dir = '/build' @@ -61,7 +63,11 @@ fn create_build_image() ?string { fn build(conf Config) ? { // We get the repos list from the Vieter instance - repos := git.get_repos(conf.address, conf.api_key) ? + 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) ? // No point in doing work if there's no repos present if repos.len == 0 { @@ -71,7 +77,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/cli.v b/src/git/cli.v index 4a066d5..17fa984 100644 --- a/src/git/cli.v +++ b/src/git/cli.v @@ -2,6 +2,7 @@ module git import cli import env +import net.http struct Config { address string [required] @@ -27,25 +28,25 @@ pub fn cmd() cli.Command { cli.Command{ name: 'add' required_args: 2 - usage: 'url branch arch...' + usage: 'url branch' 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], cmd.args[2..]) ? + add(conf, cmd.args[0], cmd.args[1]) ? } }, cli.Command{ name: 'remove' - required_args: 1 - usage: 'id' - description: 'Remove a repository that matches the given ID prefix.' + required_args: 2 + usage: 'url branch' + description: 'Remove a repository.' execute: fn (cmd cli.Command) ? { config_file := cmd.flags.get_string('config-file') ? conf := env.load(config_file) ? - remove(conf, cmd.args[0]) ? + remove(conf, cmd.args[0], cmd.args[1]) ? } }, ] @@ -53,41 +54,30 @@ pub fn cmd() cli.Command { } fn list(conf Config) ? { - repos := get_repos(conf.address, conf.api_key) ? + mut req := http.new_request(http.Method.get, '$conf.address/api/repos', '') ? + req.add_custom_header('X-API-Key', conf.api_key) ? - for id, details in repos { - println('${id[..8]}\t$details.url\t$details.branch\t$details.arch') - } + res := req.do() ? + + println(res.text) } -fn add(conf Config, url string, branch string, arch []string) ? { - res := add_repo(conf.address, conf.api_key, url, branch, 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', + '') ? + req.add_custom_header('X-API-Key', conf.api_key) ? - println(res.message) + res := req.do() ? + + println(res.text) } -fn remove(conf Config, id_prefix string) ? { - repos := get_repos(conf.address, conf.api_key) ? +fn remove(conf Config, url string, branch string) ? { + mut req := http.new_request(http.Method.delete, '$conf.address/api/repos?url=$url&branch=$branch', + '') ? + req.add_custom_header('X-API-Key', conf.api_key) ? - mut to_remove := []string{} + res := req.do() ? - 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) - } - - res := remove_repo(conf.address, conf.api_key, to_remove[0]) ? - - println(res.message) + println(res.text) } diff --git a/src/git/client.v b/src/git/client.v deleted file mode 100644 index 97fe9fb..0000000 --- a/src/git/client.v +++ /dev/null @@ -1,39 +0,0 @@ -module git - -import json -import response { Response } -import net.http - -// 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) ? - - res := req.do() ? - data := json.decode(Response, res.text) ? - - return data.data -} - -// 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) ? - - res := req.do() ? - data := json.decode(Response, res.text) ? - - return data -} - -// 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) ? - - res := req.do() ? - data := json.decode(Response, res.text) ? - - return data -} diff --git a/src/git/git.v b/src/git/git.v index c5390b6..913bc39 100644 --- a/src/git/git.v +++ b/src/git/git.v @@ -4,34 +4,13 @@ import os import json pub struct GitRepo { -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 +pub: + url string [required] + branch string [required] } -// 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 { - $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(',') - } - } - } -} - -// read_repos reads the provided path & parses it into a map of GitRepo's. -pub fn read_repos(path string) ?map[string]GitRepo { +// read_repos reads the given JSON file & parses it as a list of Git repos +pub fn read_repos(path string) ?[]GitRepo { if !os.exists(path) { mut f := os.create(path) ? @@ -39,19 +18,18 @@ pub fn read_repos(path string) ?map[string]GitRepo { f.close() } - f.write_string('{}') ? + f.write_string('[]') ? - return {} + return [] } content := os.read_file(path) ? - res := json.decode(map[string]GitRepo, content) ? - + res := json.decode([]GitRepo, content) ? 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) ? { +// write_repos writes a list of repositories back to a given file +pub fn write_repos(path string, repos []GitRepo) ? { mut f := os.create(path) ? defer { @@ -61,20 +39,3 @@ pub fn write_repos(path string, repos &map[string]GitRepo) ? { value := json.encode(repos) 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{} - - // 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 -} diff --git a/src/response.v b/src/response.v deleted file mode 100644 index a06a589..0000000 --- a/src/response.v +++ /dev/null @@ -1,34 +0,0 @@ -module response - -pub struct Response { -pub: - message string - 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 - data: '' - } -} - -// 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: '' - data: data - } -} - -// 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 - data: data - } -} diff --git a/src/server/git.v b/src/server/git.v index 2a682d8..3ec8eeb 100644 --- a/src/server/git.v +++ b/src/server/git.v @@ -2,140 +2,92 @@ module server import web import git -import net.http -import rand -import response { new_data_response, new_response } const repos_file = 'repos.json' ['/api/repos'; get] fn (mut app App) get_repos() web.Result { if !app.is_authorized() { - return app.json(http.Status.unauthorized, new_response('Unauthorized.')) - } - - repos := rlock app.git_mutex { - git.read_repos(app.conf.repos_file) or { - app.lerror('Failed to read repos file: $err.msg') - - return app.status(http.Status.internal_server_error) - } - } - - 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(http.Status.unauthorized, new_response('Unauthorized.')) + return app.text('Unauthorized.') } repos := rlock app.git_mutex { git.read_repos(app.conf.repos_file) or { app.lerror('Failed to read repos file.') - return app.status(http.Status.internal_server_error) + return app.server_error(500) } } - if id !in repos { - return app.not_found() - } - - repo := repos[id] - - return app.json(http.Status.ok, new_data_response(repo)) + return app.json(repos) } ['/api/repos'; post] fn (mut app App) post_repo() web.Result { if !app.is_authorized() { - return app.json(http.Status.unauthorized, new_response('Unauthorized.')) + return app.text('Unauthorized.') } - new_repo := git.repo_from_params(app.query) or { - return app.json(http.Status.bad_request, new_response(err.msg)) + if !('url' in app.query && 'branch' in app.query) { + return app.server_error(400) } - id := rand.uuid_v4() + new_repo := git.GitRepo{ + url: app.query['url'] + branch: app.query['branch'] + } mut repos := rlock app.git_mutex { git.read_repos(app.conf.repos_file) or { app.lerror('Failed to read repos file.') - return app.status(http.Status.internal_server_error) + return app.server_error(500) } } // We need to check for duplicates - for _, repo in repos { - if repo == new_repo { - return app.json(http.Status.bad_request, new_response('Duplicate repository.')) + for r in repos { + if r == new_repo { + return app.text('Duplicate repository.') } } - repos[id] = new_repo + repos << new_repo lock app.git_mutex { - git.write_repos(app.conf.repos_file, &repos) or { - return app.status(http.Status.internal_server_error) - } + git.write_repos(app.conf.repos_file, repos) or { return app.server_error(500) } } - return app.json(http.Status.ok, new_response('Repo added successfully.')) + return app.ok('Repo added successfully.') } -['/api/repos/:id'; delete] -fn (mut app App) delete_repo(id string) web.Result { +['/api/repos'; delete] +fn (mut app App) delete_repo() web.Result { if !app.is_authorized() { - return app.json(http.Status.unauthorized, new_response('Unauthorized.')) + return app.text('Unauthorized.') + } + + if !('url' in app.query && 'branch' in app.query) { + return app.server_error(400) + } + + repo_to_remove := git.GitRepo{ + url: app.query['url'] + branch: app.query['branch'] } mut repos := rlock app.git_mutex { git.read_repos(app.conf.repos_file) or { app.lerror('Failed to read repos file.') - return app.status(http.Status.internal_server_error) + return app.server_error(500) } } - - if id !in repos { - return app.not_found() - } - - repos.delete(id) + filtered := repos.filter(it != repo_to_remove) lock app.git_mutex { - git.write_repos(app.conf.repos_file, &repos) or { return app.server_error(500) } + git.write_repos(app.conf.repos_file, filtered) or { return app.server_error(500) } } - 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(http.Status.unauthorized, new_response('Unauthorized.')) - } - - mut repos := rlock app.git_mutex { - git.read_repos(app.conf.repos_file) or { - app.lerror('Failed to read repos file.') - - return app.status(http.Status.internal_server_error) - } - } - - if id !in repos { - return app.not_found() - } - - repos[id].patch_from_params(app.query) - - lock app.git_mutex { - git.write_repos(app.conf.repos_file, &repos) or { return app.server_error(500) } - } - - return app.json(http.Status.ok, new_response('Repo updated successfully.')) + return app.ok('Repo removed successfully.') } diff --git a/src/server/routes.v b/src/server/routes.v index 55f6f12..0f697f9 100644 --- a/src/server/routes.v +++ b/src/server/routes.v @@ -7,13 +7,12 @@ import time import rand import util import net.http -import response { new_response } // healthcheck just returns a string, but can be used to quickly check if the // server is still responsive. ['/health'; get] pub fn (mut app App) healthcheck() web.Result { - return app.json(http.Status.ok, new_response('Healthy.')) + return app.text('Healthy') } ['/:repo/:arch/:filename'; get; head] @@ -37,7 +36,7 @@ fn (mut app App) get_repo_file(repo string, arch string, filename string) web.Re // Scuffed way to respond to HEAD requests if app.req.method == http.Method.head { if os.exists(full_path) { - return app.status(http.Status.ok) + return app.ok('') } return app.not_found() @@ -49,7 +48,7 @@ fn (mut app App) get_repo_file(repo string, arch string, filename string) web.Re ['/:repo/publish'; post] fn (mut app App) put_package(repo string) web.Result { if !app.is_authorized() { - return app.json(http.Status.unauthorized, new_response('Unauthorized.')) + return app.text('Unauthorized.') } mut pkg_path := '' @@ -70,16 +69,14 @@ fn (mut app App) put_package(repo string) web.Result { util.reader_to_file(mut app.reader, length.int(), pkg_path) or { app.lwarn("Failed to upload '$pkg_path'") - return app.json(http.Status.internal_server_error, new_response('Failed to upload file.')) + return app.text('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.') - - // length required - return app.status(http.Status.length_required) + return app.text("Content-Type header isn't set.") } res := app.repo.add_pkg_from_path(repo, pkg_path) or { @@ -87,7 +84,7 @@ fn (mut app App) put_package(repo string) web.Result { os.rm(pkg_path) or { app.lerror("Failed to remove download '$pkg_path': $err.msg") } - return app.json(http.Status.internal_server_error, new_response('Failed to add package.')) + return app.text('Failed to add package.') } if !res.added { @@ -95,10 +92,10 @@ fn (mut app App) put_package(repo string) web.Result { app.lwarn("Duplicate package '$res.pkg.full_name()' in repo '$repo ($res.pkg.info.arch)'.") - return app.json(http.Status.bad_request, new_response('File already exists.')) + return app.text('File already exists.') } app.linfo("Added '$res.pkg.full_name()' to repo '$repo ($res.pkg.info.arch)'.") - return app.json(http.Status.ok, new_response('Package added successfully.')) + return app.text('Package added successfully.') } diff --git a/src/web/web.v b/src/web/web.v index 000c6a6..ad647f2 100644 --- a/src/web/web.v +++ b/src/web/web.v @@ -12,6 +12,9 @@ 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 {} @@ -138,8 +141,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 string = '200 OK' pub: // HTTP Request req http.Request @@ -183,14 +186,24 @@ 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() {} -// send_string -fn send_string(mut conn net.TcpConn, s string) ? { - conn.write(s.bytes()) ? +pub struct Cookie { + name string + value string + expires time.Time + secure bool + http_only bool } // send_response_to_client sends a response to the client @@ -212,27 +225,34 @@ pub fn (mut ctx Context) send_response_to_client(mimetype string, res string) bo text: res } resp.set_version(.v1_1) - resp.set_status(ctx.status) + resp.set_status(http.status_from_int(ctx.status.int())) send_string(mut ctx.conn, resp.bytestr()) or { return false } 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 +// 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{} +} +// 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(status http.Status, j T) Result { - ctx.status = status - +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{} +} +// 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) + ctx.send_response_to_client('application/json', json_s) return Result{} } @@ -282,7 +302,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(ctx.status) + resp.set_status(http.status_from_int(ctx.status.int())) send_string(mut ctx.conn, resp.bytestr()) or { return Result{} } mut buf := []byte{len: 1_000_000} @@ -308,10 +328,10 @@ 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, '') +// 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{} } // server_error Response a server error @@ -341,7 +361,64 @@ 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(http.Status.not_found) + 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' + } } // add_header Adds an header to the response with key and val @@ -483,6 +560,12 @@ 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 { @@ -578,6 +661,83 @@ 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 { '' } @@ -600,6 +760,16 @@ 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 diff --git a/vieter.toml b/vieter.toml index f3904b6..fb05d6f 100644 --- a/vieter.toml +++ b/vieter.toml @@ -3,7 +3,7 @@ api_key = "test" download_dir = "data/downloads" data_dir = "data" pkg_dir = "data/pkgs" -log_level = "DEBUG" +# log_level = "DEBUG" repos_file = "data/repos.json" address = "http://localhost:8000"