diff --git a/.gitignore b/.gitignore index ca2e2f8..3a6b11b 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,6 @@ test/ # V compiler directory v/ + +# gdb log file +gdb.txt diff --git a/Makefile b/Makefile index 94b079b..b1b91f2 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,17 @@ vieter: $(SOURCES) .PHONY: debug debug: dvieter dvieter: $(SOURCES) - $(V) -keepc -cg -cc gcc -o dvieter $(SRC_DIR) + $(V_PATH) -showcc -keepc -cg -o dvieter $(SRC_DIR) + +.PHONY: gdb +gdb: dvieter + VIETER_API_KEY=test \ + VIETER_DOWNLOAD_DIR=data/downloads \ + VIETER_REPO_DIR=data/repo \ + VIETER_PKG_DIR=data/pkgs \ + VIETER_LOG_LEVEL=DEBUG \ + VIETER_REPOS_FILE=data/repos.json \ + gdb --args ./dvieter # Optimised production build .PHONY: prod @@ -39,6 +49,7 @@ run: vieter VIETER_REPO_DIR=data/repo \ VIETER_PKG_DIR=data/pkgs \ VIETER_LOG_LEVEL=DEBUG \ + VIETER_REPOS_FILE=data/repos.json \ ./vieter server .PHONY: run-prod diff --git a/src/build.v b/src/build.v index 0acca8f..8d6af14 100644 --- a/src/build.v +++ b/src/build.v @@ -6,7 +6,7 @@ import rand import time import os import json -import git +import server import env const container_build_dir = '/build' @@ -17,7 +17,7 @@ fn build() ? { // Read in the repos from a json file filename := os.join_path_single(conf.repo_dir, 'repos.json') txt := os.read_file(filename) ? - repos := json.decode([]git.GitRepo, txt) ? + repos := json.decode([]server.GitRepo, txt) ? mut commands := [ // Update repos & install required packages diff --git a/src/env.v b/src/env.v index 804d37c..1e601bd 100644 --- a/src/env.v +++ b/src/env.v @@ -11,24 +11,25 @@ const file_suffix = '_FILE' pub struct ServerConfig { pub: - log_level string [default: WARN] - log_file string [default: 'vieter.log'] - pkg_dir string + log_level string [default: WARN] + log_file string [default: 'vieter.log'] + pkg_dir string download_dir string - api_key string - repo_dir string + api_key string + repo_dir string + repos_file string } pub struct BuildConfig { pub: - api_key string + api_key string repo_dir string - address string + address string } fn get_env_var(field_name string) ?string { - env_var_name := '${prefix}${field_name.to_upper()}' - env_file_name := '${prefix}${field_name.to_upper()}${file_suffix}' + env_var_name := '$env.prefix$field_name.to_upper()' + env_file_name := '$env.prefix$field_name.to_upper()$env.file_suffix' env_var := os.getenv(env_var_name) env_file := os.getenv(env_file_name) @@ -42,7 +43,10 @@ fn get_env_var(field_name string) ?string { return error('Only one of $env_var_name or $env_file_name can be defined.') } - // If it's the env var itself, we return it + // If it's the env var itself, we return it. + // I'm pretty sure this also prevents variable ending in _FILE (e.g. + // VIETER_LOG_FILE) from being mistakingely read as an _FILE suffixed env + // var. if env_var != '' { return env_var } @@ -76,6 +80,5 @@ pub fn load() ?T { default } } - return res } diff --git a/src/git.v b/src/git.v deleted file mode 100644 index 76d80d7..0000000 --- a/src/git.v +++ /dev/null @@ -1,7 +0,0 @@ -module git - -pub struct GitRepo { -pub: - url string [required] - branch string [required] -} diff --git a/src/main.v b/src/main.v index 2472ac0..156f0a3 100644 --- a/src/main.v +++ b/src/main.v @@ -1,48 +1,17 @@ module main import os -import io - -[noreturn] -fn exit_with_message(code int, msg string) { - eprintln(msg) - exit(code) -} - -fn reader_to_file(mut reader io.BufferedReader, length int, path string) ? { - mut file := os.create(path) ? - defer { - file.close() - } - - mut buf := []byte{len: buf_size} - mut bytes_left := length - - // Repeat as long as the stream still has data - for bytes_left > 0 { - // TODO check if just breaking here is safe - bytes_read := reader.read(mut buf) or { break } - bytes_left -= bytes_read - - mut to_write := bytes_read - - for to_write > 0 { - // TODO don't just loop infinitely here - bytes_written := file.write(buf[bytes_read - to_write..bytes_read]) or { continue } - - to_write = to_write - bytes_written - } - } -} +import server +import util fn main() { if os.args.len == 1 { - exit_with_message(1, 'No action provided.') + util.exit_with_message(1, 'No action provided.') } match os.args[1] { - 'server' { server() ? } + 'server' { server.server() ? } 'build' { build() ? } - else { exit_with_message(1, 'Unknown action: ${os.args[1]}') } + else { util.exit_with_message(1, 'Unknown action: ${os.args[1]}') } } } diff --git a/src/repo/repo.v b/src/repo/repo.v index 1bf2d0c..f1419ac 100644 --- a/src/repo/repo.v +++ b/src/repo/repo.v @@ -2,17 +2,12 @@ module repo import os import package - -// Dummy struct to work around the fact that you can only share structs, maps & -// arrays -pub struct Dummy { - x int -} +import util // This struct manages a single repository. pub struct Repo { mut: - mutex shared Dummy + mutex shared util.Dummy pub: // Where to store repository files repo_dir string [required] diff --git a/src/auth.v b/src/server/auth.v similarity index 94% rename from src/auth.v rename to src/server/auth.v index 942bdda..8bc9d55 100644 --- a/src/auth.v +++ b/src/server/auth.v @@ -1,4 +1,4 @@ -module main +module server import net.http diff --git a/src/server/git.v b/src/server/git.v new file mode 100644 index 0000000..6a1d929 --- /dev/null +++ b/src/server/git.v @@ -0,0 +1,100 @@ +module server + +import web +import os +import json + +const repos_file = 'repos.json' + +pub struct GitRepo { +pub: + url string [required] + branch string [required] +} + +fn read_repos(path string) ?[]GitRepo { + if !os.exists(path) { + mut f := os.create(path) ? + + defer { + f.close() + } + + f.write_string('{}') ? + + return [] + } + + content := os.read_file(path) ? + return json.decode([]GitRepo, content) +} + +fn write_repos(path string, repos []GitRepo) ? { + mut f := os.create(path) ? + + defer { + f.close() + } + + dump(repos) + value := json.encode(repos) + f.write_string(value) ? +} + +['/api/repos'; get] +pub fn (mut app App) get_repos() 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) + } + } + + return app.json(repos) +} + +['/api/repos'; post] +pub 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) { + return app.server_error(400) + } + + new_repo := 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.') + + return app.server_error(500) + } + } + + // We need to check for duplicates + for r in repos { + if r == new_repo { + return app.text('Duplicate repository.') + } + } + + repos << new_repo + + lock app.git_mutex { + write_repos(app.conf.repos_file, repos) or { + return app.server_error(500) + } + } + + return app.ok('Repo added successfully.') +} diff --git a/src/routes.v b/src/server/routes.v similarity index 82% rename from src/routes.v rename to src/server/routes.v index b152a9f..7a0ee38 100644 --- a/src/routes.v +++ b/src/server/routes.v @@ -1,29 +1,11 @@ -module main +module server import web import os import repo import time import rand - -const prefixes = ['B', 'KB', 'MB', 'GB'] - -// pretty_bytes converts a byte count to human-readable version -fn pretty_bytes(bytes int) string { - mut i := 0 - mut n := f32(bytes) - - for n >= 1024 { - i++ - n /= 1024 - } - - return '${n:.2}${prefixes[i]}' -} - -fn is_pkg_name(s string) bool { - return s.contains('.pkg') -} +import util // healthcheck just returns a string, but can be used to quickly check if the // server is still responsive. @@ -64,12 +46,12 @@ fn (mut app App) put_package() web.Result { pkg_path = os.join_path_single(app.conf.download_dir, rand.uuid_v4()) } - app.ldebug("Uploading $length bytes (${pretty_bytes(length.int())}) to '$pkg_path'.") + app.ldebug("Uploading $length bytes (${util.pretty_bytes(length.int())}) to '$pkg_path'.") // This is used to time how long it takes to upload a file mut sw := time.new_stopwatch(time.StopWatchOptions{ auto_start: true }) - reader_to_file(mut app.reader, length.int(), pkg_path) or { + 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.') diff --git a/src/server.v b/src/server/server.v similarity index 62% rename from src/server.v rename to src/server/server.v index d5c5ab2..ebb9f3b 100644 --- a/src/server.v +++ b/src/server/server.v @@ -1,29 +1,30 @@ -module main +module server import web import os import log import repo import env +import util const port = 8000 -const buf_size = 1_000_000 - struct App { web.Context pub: - conf env.ServerConfig [required: web_global] + conf env.ServerConfig [required; web_global] pub mut: repo repo.Repo [required; web_global] + // This is used to claim the file lock on the repos file + git_mutex shared util.Dummy } -fn server() ? { +pub fn server() ? { conf := env.load() ? // Configure logger log_level := log.level_from_tag(conf.log_level) or { - exit_with_message(1, 'Invalid log level. The allowed values are FATAL, ERROR, WARN, INFO & DEBUG.') + util.exit_with_message(1, 'Invalid log level. The allowed values are FATAL, ERROR, WARN, INFO & DEBUG.') } mut logger := log.Log{ @@ -45,11 +46,13 @@ fn server() ? { exit(1) } - os.mkdir_all(conf.download_dir) or { exit_with_message(1, 'Failed to create download directory.') } + os.mkdir_all(conf.download_dir) or { + util.exit_with_message(1, 'Failed to create download directory.') + } web.run(&App{ logger: logger conf: conf repo: repo - }, port) + }, server.port) } diff --git a/src/util.v b/src/util.v index f81a256..65d5294 100644 --- a/src/util.v +++ b/src/util.v @@ -1,9 +1,52 @@ module util import os +import io import crypto.md5 import crypto.sha256 +const reader_buf_size = 1_000_000 + +const prefixes = ['B', 'KB', 'MB', 'GB'] + +// Dummy struct to work around the fact that you can only share structs, maps & +// arrays +pub struct Dummy { + x int +} + +[noreturn] +pub fn exit_with_message(code int, msg string) { + eprintln(msg) + exit(code) +} + +pub fn reader_to_file(mut reader io.BufferedReader, length int, path string) ? { + mut file := os.create(path) ? + defer { + file.close() + } + + mut buf := []byte{len: util.reader_buf_size} + mut bytes_left := length + + // Repeat as long as the stream still has data + for bytes_left > 0 { + // TODO check if just breaking here is safe + bytes_read := reader.read(mut buf) or { break } + bytes_left -= bytes_read + + mut to_write := bytes_read + + for to_write > 0 { + // TODO don't just loop infinitely here + bytes_written := file.write(buf[bytes_read - to_write..bytes_read]) or { continue } + + to_write = to_write - bytes_written + } + } +} + // hash_file returns the md5 & sha256 hash of a given file // TODO actually implement sha256 pub fn hash_file(path &string) ?(string, string) { @@ -32,3 +75,17 @@ pub fn hash_file(path &string) ?(string, string) { return md5sum.checksum().hex(), sha256sum.checksum().hex() } + +// pretty_bytes converts a byte count to human-readable version +pub fn pretty_bytes(bytes int) string { + mut i := 0 + mut n := f32(bytes) + + for n >= 1024 { + i++ + n /= 1024 + } + + return '${n:.2}${util.prefixes[i]}' +} +