From 0bac221aee85bc24514733eee3f5280a6b1bce41 Mon Sep 17 00:00:00 2001 From: LordMZTE Date: Tue, 10 May 2022 13:36:07 +0200 Subject: [PATCH 001/236] fix: don't pass --nodeps to initial build step (#173) This fixes packages that require their dependencies in `pkgver` or `prepare` failing to build. Reviewed-on: https://git.rustybever.be/vieter/vieter/pulls/173 Co-authored-by: LordMZTE Co-committed-by: LordMZTE --- docs/content/builder.md | 2 +- src/build/build.v | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/content/builder.md b/docs/content/builder.md index 6a1bc3ab..659717df 100644 --- a/docs/content/builder.md +++ b/docs/content/builder.md @@ -15,7 +15,7 @@ repositories. After the image has been created, each repository returned by previously created image as a base. Each container goes through the following steps: 1. The repository is cloned -2. `makepkg --nobuild --nodeps` is ran to update the `pkgver` variable inside +2. `makepkg --nobuild --syncdeps --needed --noconfirm` is ran to update the `pkgver` variable inside the `PKGBUILD` file 3. A HEAD request is sent to the Vieter server to check whether the specific version of the package is already present. If it is, the container exits. diff --git a/src/build/build.v b/src/build/build.v index 0a978aa0..7bd63a78 100644 --- a/src/build/build.v +++ b/src/build/build.v @@ -91,7 +91,7 @@ pub fn build_repo(address string, api_key string, base_image_id string, repo &db commands := [ 'git clone --single-branch --depth 1 --branch $repo.branch $repo.url repo', 'cd repo', - 'makepkg --nobuild --nodeps', + 'makepkg --nobuild --syncdeps --needed --noconfirm', 'source PKGBUILD', // The build container checks whether the package is already // present on the server From c018aad14382fd32746d5f245290375b25501b98 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Tue, 10 May 2022 13:43:09 +0200 Subject: [PATCH 002/236] chore: updated CHANGELOG --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d9eb4d6..da09449b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Web API for adding & querying build logs * CLI commands to access build logs API +* Cron build logs are uploaded to above API + +### Changed + +- `vieter build` command now only builds a single repository & uploads the + build logs ## [0.3.0-alpha.1](https://git.rustybever.be/vieter/vieter/src/tag/0.3.0-alpha.1) From ad207bdb7024ed37b63acce7409f7a5a5e359725 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Tue, 10 May 2022 19:30:26 +0200 Subject: [PATCH 003/236] feat(ci): split Arch releases into vieter & vieter-git --- .woodpecker/.arch-rel.yml | 39 +++++++++++++++++++++++++++++++++++++++ .woodpecker/.arch.yml | 2 +- CHANGELOG.md | 5 ++++- PKGBUILD | 17 ++++++----------- PKGBUILD.dev | 35 +++++++++++++++++++++++++++++++++++ 5 files changed, 85 insertions(+), 13 deletions(-) create mode 100644 .woodpecker/.arch-rel.yml create mode 100644 PKGBUILD.dev diff --git a/.woodpecker/.arch-rel.yml b/.woodpecker/.arch-rel.yml new file mode 100644 index 00000000..b8f4c7ae --- /dev/null +++ b/.woodpecker/.arch-rel.yml @@ -0,0 +1,39 @@ +matrix: + PLATFORM: + - linux/amd64 + - linux/arm64 + +platform: ${PLATFORM} +branches: [main] +skip_clone: true + +pipeline: + build: + image: 'menci/archlinuxarm:base-devel' + commands: + # Add the vieter repository so we can use the compiler + - echo -e '[vieter]\nServer = https://arch.r8r.be/$repo/$arch\nSigLevel = Optional' >> /etc/pacman.conf + # Update packages + - pacman -Syu --noconfirm + # Create non-root user to perform build & switch to their home + - groupadd -g 1000 builder + - useradd -mg builder builder + - chown -R builder:builder "$PWD" + - "echo 'builder ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers" + - su builder + # Due to a bug with the V compiler, we can't just use the PKGBUILD from + # inside the repo + - curl -OL "https://git.rustybever.be/vieter/vieter/raw/tag/$CI_COMMIT_TAG/PKGBUILD" + - makepkg -s --noconfirm --needed + when: + event: tag + + publish: + image: 'curlimages/curl' + commands: + # Publish the package + - 'for pkg in $(ls -1 *.pkg*); do curl -XPOST -T "$pkg" -H "X-API-KEY: $VIETER_API_KEY" https://arch.r8r.be/vieter/publish; done' + secrets: + - vieter_api_key + when: + event: tag diff --git a/.woodpecker/.arch.yml b/.woodpecker/.arch.yml index 6b8f8f2f..b2a59ba5 100644 --- a/.woodpecker/.arch.yml +++ b/.woodpecker/.arch.yml @@ -23,7 +23,7 @@ pipeline: - su builder # Due to a bug with the V compiler, we can't just use the PKGBUILD from # inside the repo - - curl -OL https://git.rustybever.be/vieter/vieter/raw/branch/dev/PKGBUILD + - curl -o PKGBUILD -L https://git.rustybever.be/vieter/vieter/raw/branch/dev/PKGBUILD.dev - makepkg -s --noconfirm --needed when: event: push diff --git a/CHANGELOG.md b/CHANGELOG.md index da09449b..2e17cd60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,8 +15,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- `vieter build` command now only builds a single repository & uploads the +* `vieter build` command now only builds a single repository & uploads the build logs +* Official Arch packages are now split between `vieter` & `vieter-git` + * `vieter` is the latest release + * `vieter-git` is the latest commit on the dev branch ## [0.3.0-alpha.1](https://git.rustybever.be/vieter/vieter/src/tag/0.3.0-alpha.1) diff --git a/PKGBUILD b/PKGBUILD index 87c575ff..83ab8961 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,23 +1,18 @@ +# vim: ft=bash # Maintainer: Jef Roosens pkgbase='vieter' pkgname='vieter' -pkgver=0.2.0.r25.g20112b8 +pkgver='0.3.0_alpha.1' pkgrel=1 -depends=('glibc' 'openssl' 'libarchive' 'gc' 'sqlite') -makedepends=('git' 'gcc' 'vieter-v') +depends=('glibc' 'openssl' 'libarchive' 'sqlite') +makedepends=('git' 'vieter-v') arch=('x86_64' 'aarch64') url='https://git.rustybever.be/vieter/vieter' license=('AGPL3') -source=($pkgname::git+https://git.rustybever.be/vieter/vieter#branch=dev) +source=("$pkgname::git+https://git.rustybever.be/vieter/vieter#tag=${pkgver//_/-}") md5sums=('SKIP') -pkgver() { - cd "$pkgname" - - git describe --long --tags | sed 's/^v//;s/\([^-]*-g\)/r\1/;s/-/./g' -} - build() { cd "$pkgname" @@ -28,5 +23,5 @@ package() { pkgdesc="Vieter is a lightweight implementation of an Arch repository server." install -dm755 "$pkgdir/usr/bin" - install -Dm755 "$pkgbase/pvieter" "$pkgdir/usr/bin/vieter" + install -Dm755 "$pkgname/pvieter" "$pkgdir/usr/bin/vieter" } diff --git a/PKGBUILD.dev b/PKGBUILD.dev new file mode 100644 index 00000000..d0176d86 --- /dev/null +++ b/PKGBUILD.dev @@ -0,0 +1,35 @@ +# vim: ft=bash +# Maintainer: Jef Roosens + +pkgbase='vieter-git' +pkgname='vieter-git' +pkgver=0.2.0.r25.g20112b8 +pkgrel=1 +depends=('glibc' 'openssl' 'libarchive' 'sqlite') +makedepends=('git' 'vieter-v') +arch=('x86_64' 'aarch64') +url='https://git.rustybever.be/vieter/vieter' +license=('AGPL3') +source=("$pkgname::git+https://git.rustybever.be/vieter/vieter#branch=dev") +md5sums=('SKIP') +provides=('vieter') +conflicts=('vieter') + +pkgver() { + cd "$pkgname" + + git describe --long --tags | sed 's/^v//;s/\([^-]*-g\)/r\1/;s/-/./g' +} + +build() { + cd "$pkgname" + + make prod +} + +package() { + pkgdesc="Vieter is a lightweight implementation of an Arch repository server." + + install -dm755 "$pkgdir/usr/bin" + install -Dm755 "$pkgname/pvieter" "$pkgdir/usr/bin/vieter" +} From 06bab98a88ebd3e0818cae3f199f66934d8a19d6 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Tue, 10 May 2022 19:49:28 +0200 Subject: [PATCH 004/236] chore: update README --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 08f1e759..138282a2 100644 --- a/README.md +++ b/README.md @@ -55,3 +55,13 @@ clone my compiler in the `v` directory & build it. Afterwards, you can use this compiler with make by prepending all make commands with `V_PATH=v/v`. If you do encounter this issue, please let me know so I can update my mirror & the codebase to fix it! + +## Contributing + +If you wish to contribute to the project, please take note of the following: + +* Rebase instead of merging whenever possible, e.g. when updating your branch + with the dev branch. +* Please follow the + [Conventional Commits](https://www.conventionalcommits.org/) style for your + commit messages. From 5f21e256eefb4d9049f7ff90bb06c11f1d92e1b9 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 14 May 2022 20:06:08 +0200 Subject: [PATCH 005/236] refactor: apply new vfmt defaults --- src/build/build.v | 34 ++++++++--------- src/build/cli.v | 6 +-- src/client/client.v | 12 +++--- src/client/git.v | 10 ++--- src/client/logs.v | 10 ++--- src/console/git/git.v | 42 ++++++++++----------- src/console/logs/logs.v | 28 +++++++------- src/cron/cli.v | 6 +-- src/cron/cron.v | 2 +- src/cron/expression/expression.v | 2 +- src/cron/expression/expression_parse_test.v | 18 ++++----- src/cron/expression/expression_test.v | 20 +++++----- src/db/db.v | 2 +- src/docker/containers.v | 20 +++++----- src/docker/docker.v | 4 +- src/docker/images.v | 6 +-- src/env/env.v | 4 +- src/package/package.v | 2 +- src/repo/repo.v | 32 ++++++++-------- src/repo/sync.v | 2 +- src/server/cli.v | 6 +-- src/server/logs.v | 4 +- src/util/util.v | 2 +- src/web/web.v | 2 +- 24 files changed, 138 insertions(+), 138 deletions(-) diff --git a/src/build/build.v b/src/build/build.v index 7bd63a78..41f68e28 100644 --- a/src/build/build.v +++ b/src/build/build.v @@ -46,14 +46,14 @@ pub fn create_build_image(base_image string) ?string { image_tag := if image_parts.len > 1 { image_parts[1] } else { 'latest' } // We pull the provided image - docker.pull_image(image_name, image_tag) ? + docker.pull_image(image_name, image_tag)? - id := docker.create_container(c) ? - docker.start_container(id) ? + id := docker.create_container(c)? + docker.start_container(id)? // This loop waits until the container has stopped, so we can remove it after for { - data := docker.inspect_container(id) ? + data := docker.inspect_container(id)? if !data.state.running { break @@ -67,8 +67,8 @@ pub fn create_build_image(base_image string) ?string { // TODO also add the base image's name into the image name to prevent // conflicts. tag := time.sys_mono_now().str() - image := docker.create_image_from_container(id, 'vieter-build', tag) ? - docker.remove_container(id) ? + image := docker.create_image_from_container(id, 'vieter-build', tag)? + docker.remove_container(id)? return image.id } @@ -112,21 +112,21 @@ pub fn build_repo(address string, api_key string, base_image_id string, repo &db user: 'builder:builder' } - id := docker.create_container(c) ? - docker.start_container(id) ? + id := docker.create_container(c)? + docker.start_container(id)? - mut data := docker.inspect_container(id) ? + mut data := docker.inspect_container(id)? // This loop waits until the container has stopped, so we can remove it after for data.state.running { time.sleep(1 * time.second) - data = docker.inspect_container(id) ? + data = docker.inspect_container(id)? } - logs := docker.get_container_logs(id) ? + logs := docker.get_container_logs(id)? - docker.remove_container(id) ? + docker.remove_container(id)? return BuildResult{ start_time: data.state.start_time @@ -139,20 +139,20 @@ pub fn build_repo(address string, api_key string, base_image_id string, repo &db // build builds every Git repo in the server's list. fn build(conf Config, repo_id int) ? { c := client.new(conf.address, conf.api_key) - repo := c.get_git_repo(repo_id) ? + repo := c.get_git_repo(repo_id)? build_arch := os.uname().machine println('Creating base image...') - image_id := create_build_image(conf.base_image) ? + image_id := create_build_image(conf.base_image)? println('Running build...') - res := build_repo(conf.address, conf.api_key, image_id, repo) ? + res := build_repo(conf.address, conf.api_key, image_id, repo)? println('Removing build image...') - docker.remove_image(image_id) ? + docker.remove_image(image_id)? println('Uploading logs to Vieter...') c.add_build_log(repo.id, res.start_time, res.end_time, build_arch, res.exit_code, - res.logs) ? + res.logs)? } diff --git a/src/build/cli.v b/src/build/cli.v index 5247e871..64814cb2 100644 --- a/src/build/cli.v +++ b/src/build/cli.v @@ -18,12 +18,12 @@ pub fn cmd() cli.Command { usage: 'id' description: 'Build the repository with the given ID.' execute: fn (cmd cli.Command) ? { - config_file := cmd.flags.get_string('config-file') ? - conf := env.load(config_file) ? + config_file := cmd.flags.get_string('config-file')? + conf := env.load(config_file)? id := cmd.args[0].int() - build(conf, id) ? + build(conf, id)? } } } diff --git a/src/client/client.v b/src/client/client.v index 25224a51..3b280736 100644 --- a/src/client/client.v +++ b/src/client/client.v @@ -37,10 +37,10 @@ fn (c &Client) send_request_raw(method Method, url string, params map[string]str full_url = '$full_url?$params_str' } - mut req := http.new_request(method, full_url, body) ? - req.add_custom_header('X-Api-Key', c.api_key) ? + mut req := http.new_request(method, full_url, body)? + req.add_custom_header('X-Api-Key', c.api_key)? - res := req.do() ? + res := req.do()? return res } @@ -53,15 +53,15 @@ fn (c &Client) send_request(method Method, url string, params map[string]stri // send_request_with_body calls send_request_raw_response & parses its // output as a Response object. fn (c &Client) send_request_with_body(method Method, url string, params map[string]string, body string) ?Response { - res_text := c.send_request_raw_response(method, url, params, body) ? - data := json.decode(Response, res_text) ? + res_text := c.send_request_raw_response(method, url, params, body)? + data := json.decode(Response, res_text)? return data } // send_request_raw_response returns the raw text response for an HTTP request. fn (c &Client) send_request_raw_response(method Method, url string, params map[string]string, body string) ?string { - res := c.send_request_raw(method, url, params, body) ? + res := c.send_request_raw(method, url, params, body)? return res.text } diff --git a/src/client/git.v b/src/client/git.v index b09d4c22..280caabe 100644 --- a/src/client/git.v +++ b/src/client/git.v @@ -6,14 +6,14 @@ import response { Response } // get_git_repos returns the current list of repos. pub fn (c &Client) get_git_repos() ?[]GitRepo { - data := c.send_request<[]GitRepo>(Method.get, '/api/repos', {}) ? + data := c.send_request<[]GitRepo>(Method.get, '/api/repos', {})? return data.data } // get_git_repo returns the repo for a specific ID. pub fn (c &Client) get_git_repo(id int) ?GitRepo { - data := c.send_request(Method.get, '/api/repos/$id', {}) ? + data := c.send_request(Method.get, '/api/repos/$id', {})? return data.data } @@ -30,14 +30,14 @@ pub fn (c &Client) add_git_repo(url string, branch string, repo string, arch []s params['arch'] = arch.join(',') } - data := c.send_request(Method.post, '/api/repos', params) ? + data := c.send_request(Method.post, '/api/repos', params)? return data } // remove_git_repo removes the repo with the given ID from the server. pub fn (c &Client) remove_git_repo(id int) ?Response { - data := c.send_request(Method.delete, '/api/repos/$id', {}) ? + data := c.send_request(Method.delete, '/api/repos/$id', {})? return data } @@ -45,7 +45,7 @@ pub fn (c &Client) remove_git_repo(id int) ?Response { // patch_git_repo sends a PATCH request to the given repo with the params as // payload. pub fn (c &Client) patch_git_repo(id int, params map[string]string) ?Response { - data := c.send_request(Method.patch, '/api/repos/$id', params) ? + data := c.send_request(Method.patch, '/api/repos/$id', params)? return data } diff --git a/src/client/logs.v b/src/client/logs.v index 8c532138..cdacab9b 100644 --- a/src/client/logs.v +++ b/src/client/logs.v @@ -7,7 +7,7 @@ import time // get_build_logs returns all build logs. pub fn (c &Client) get_build_logs() ?Response<[]BuildLog> { - data := c.send_request<[]BuildLog>(Method.get, '/api/logs', {}) ? + data := c.send_request<[]BuildLog>(Method.get, '/api/logs', {})? return data } @@ -18,21 +18,21 @@ pub fn (c &Client) get_build_logs_for_repo(repo_id int) ?Response<[]BuildLog> { 'repo': repo_id.str() } - data := c.send_request<[]BuildLog>(Method.get, '/api/logs', params) ? + data := c.send_request<[]BuildLog>(Method.get, '/api/logs', params)? return data } // get_build_log returns a specific build log. pub fn (c &Client) get_build_log(id int) ?Response { - data := c.send_request(Method.get, '/api/logs/$id', {}) ? + data := c.send_request(Method.get, '/api/logs/$id', {})? return data } // get_build_log_content returns the contents of the build log file. pub fn (c &Client) get_build_log_content(id int) ?string { - data := c.send_request_raw_response(Method.get, '/api/logs/$id/content', {}, '') ? + data := c.send_request_raw_response(Method.get, '/api/logs/$id/content', {}, '')? return data } @@ -47,7 +47,7 @@ pub fn (c &Client) add_build_log(repo_id int, start_time time.Time, end_time tim 'exitCode': exit_code.str() } - data := c.send_request_with_body(Method.post, '/api/logs', params, content) ? + data := c.send_request_with_body(Method.post, '/api/logs', params, content)? return data } diff --git a/src/console/git/git.v b/src/console/git/git.v index 3bf78d10..83837446 100644 --- a/src/console/git/git.v +++ b/src/console/git/git.v @@ -20,10 +20,10 @@ pub fn cmd() cli.Command { name: 'list' description: 'List the current repos.' execute: fn (cmd cli.Command) ? { - config_file := cmd.flags.get_string('config-file') ? - conf := env.load(config_file) ? + config_file := cmd.flags.get_string('config-file')? + conf := env.load(config_file)? - list(conf) ? + list(conf)? } }, cli.Command{ @@ -32,10 +32,10 @@ pub fn cmd() cli.Command { usage: 'url branch repo' description: 'Add a new repository.' execute: fn (cmd cli.Command) ? { - config_file := cmd.flags.get_string('config-file') ? - conf := env.load(config_file) ? + 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], cmd.args[2])? } }, cli.Command{ @@ -44,10 +44,10 @@ pub fn cmd() cli.Command { 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) ? + config_file := cmd.flags.get_string('config-file')? + conf := env.load(config_file)? - remove(conf, cmd.args[0]) ? + remove(conf, cmd.args[0])? } }, cli.Command{ @@ -56,10 +56,10 @@ pub fn cmd() cli.Command { usage: 'id' description: 'Show detailed information for the repo matching the ID prefix.' execute: fn (cmd cli.Command) ? { - config_file := cmd.flags.get_string('config-file') ? - conf := env.load(config_file) ? + config_file := cmd.flags.get_string('config-file')? + conf := env.load(config_file)? - info(conf, cmd.args[0]) ? + info(conf, cmd.args[0])? } }, cli.Command{ @@ -95,8 +95,8 @@ pub fn cmd() cli.Command { }, ] execute: fn (cmd cli.Command) ? { - config_file := cmd.flags.get_string('config-file') ? - conf := env.load(config_file) ? + config_file := cmd.flags.get_string('config-file')? + conf := env.load(config_file)? found := cmd.flags.get_all_found() @@ -104,11 +104,11 @@ pub fn cmd() cli.Command { for f in found { if f.name != 'config-file' { - params[f.name] = f.get_string() ? + params[f.name] = f.get_string()? } } - patch(conf, cmd.args[0], params) ? + patch(conf, cmd.args[0], params)? } }, ] @@ -121,7 +121,7 @@ pub fn cmd() cli.Command { // list prints out a list of all repositories. fn list(conf Config) ? { c := client.new(conf.address, conf.api_key) - repos := c.get_git_repos() ? + repos := c.get_git_repos()? for repo in repos { println('$repo.id\t$repo.url\t$repo.branch\t$repo.repo') @@ -131,7 +131,7 @@ fn list(conf Config) ? { // add adds a new repository to the server's list. fn add(conf Config, url string, branch string, repo string) ? { c := client.new(conf.address, conf.api_key) - res := c.add_git_repo(url, branch, repo, []) ? + res := c.add_git_repo(url, branch, repo, [])? println(res.message) } @@ -143,7 +143,7 @@ fn remove(conf Config, id string) ? { if id_int != 0 { c := client.new(conf.address, conf.api_key) - res := c.remove_git_repo(id_int) ? + res := c.remove_git_repo(id_int)? println(res.message) } } @@ -161,7 +161,7 @@ fn patch(conf Config, id string, params map[string]string) ? { id_int := id.int() if id_int != 0 { c := client.new(conf.address, conf.api_key) - res := c.patch_git_repo(id_int, params) ? + res := c.patch_git_repo(id_int, params)? println(res.message) } @@ -176,6 +176,6 @@ fn info(conf Config, id string) ? { } c := client.new(conf.address, conf.api_key) - repo := c.get_git_repo(id_int) ? + repo := c.get_git_repo(id_int)? println(repo) } diff --git a/src/console/logs/logs.v b/src/console/logs/logs.v index e3c7d14a..0e965232 100644 --- a/src/console/logs/logs.v +++ b/src/console/logs/logs.v @@ -27,12 +27,12 @@ pub fn cmd() cli.Command { }, ] execute: fn (cmd cli.Command) ? { - config_file := cmd.flags.get_string('config-file') ? - conf := env.load(config_file) ? + config_file := cmd.flags.get_string('config-file')? + conf := env.load(config_file)? - repo_id := cmd.flags.get_int('repo') ? + repo_id := cmd.flags.get_int('repo')? - if repo_id == 0 { list(conf) ? } else { list_for_repo(conf, repo_id) ? } + if repo_id == 0 { list(conf)? } else { list_for_repo(conf, repo_id)? } } }, cli.Command{ @@ -41,11 +41,11 @@ pub fn cmd() cli.Command { usage: 'id' description: 'Show all info for a specific build log.' execute: fn (cmd cli.Command) ? { - config_file := cmd.flags.get_string('config-file') ? - conf := env.load(config_file) ? + config_file := cmd.flags.get_string('config-file')? + conf := env.load(config_file)? id := cmd.args[0].int() - info(conf, id) ? + info(conf, id)? } }, cli.Command{ @@ -54,11 +54,11 @@ pub fn cmd() cli.Command { usage: 'id' description: 'Output the content of a build log to stdout.' execute: fn (cmd cli.Command) ? { - config_file := cmd.flags.get_string('config-file') ? - conf := env.load(config_file) ? + config_file := cmd.flags.get_string('config-file')? + conf := env.load(config_file)? id := cmd.args[0].int() - content(conf, id) ? + content(conf, id)? } }, ] @@ -75,7 +75,7 @@ fn print_log_list(logs []db.BuildLog) { // list prints a list of all build logs. fn list(conf Config) ? { c := client.new(conf.address, conf.api_key) - logs := c.get_build_logs() ?.data + logs := c.get_build_logs()?.data print_log_list(logs) } @@ -83,7 +83,7 @@ fn list(conf Config) ? { // list prints a list of all build logs for a given repo. fn list_for_repo(conf Config, repo_id int) ? { c := client.new(conf.address, conf.api_key) - logs := c.get_build_logs_for_repo(repo_id) ?.data + logs := c.get_build_logs_for_repo(repo_id)?.data print_log_list(logs) } @@ -91,7 +91,7 @@ fn list_for_repo(conf Config, repo_id int) ? { // info print the detailed info for a given build log. fn info(conf Config, id int) ? { c := client.new(conf.address, conf.api_key) - log := c.get_build_log(id) ?.data + log := c.get_build_log(id)?.data print(log) } @@ -100,7 +100,7 @@ fn info(conf Config, id int) ? { // stdout. fn content(conf Config, id int) ? { c := client.new(conf.address, conf.api_key) - content := c.get_build_log_content(id) ? + content := c.get_build_log_content(id)? println(content) } diff --git a/src/cron/cli.v b/src/cron/cli.v index 15bc9867..9703c663 100644 --- a/src/cron/cli.v +++ b/src/cron/cli.v @@ -23,10 +23,10 @@ pub fn cmd() cli.Command { name: 'cron' description: 'Start the cron service that periodically runs builds.' execute: fn (cmd cli.Command) ? { - config_file := cmd.flags.get_string('config-file') ? - conf := env.load(config_file) ? + config_file := cmd.flags.get_string('config-file')? + conf := env.load(config_file)? - cron(conf) ? + cron(conf)? } } } diff --git a/src/cron/cron.v b/src/cron/cron.v index e356faa1..5f128cf0 100644 --- a/src/cron/cron.v +++ b/src/cron/cron.v @@ -27,7 +27,7 @@ pub fn cron(conf Config) ? { } mut d := daemon.init_daemon(logger, conf.address, conf.api_key, conf.base_image, ce, - conf.max_concurrent_builds, conf.api_update_frequency, conf.image_rebuild_frequency) ? + conf.max_concurrent_builds, conf.api_update_frequency, conf.image_rebuild_frequency)? d.run() } diff --git a/src/cron/expression/expression.v b/src/cron/expression/expression.v index 124337f0..5eae332c 100644 --- a/src/cron/expression/expression.v +++ b/src/cron/expression/expression.v @@ -218,7 +218,7 @@ fn parse_part(s string, min int, max int) ?[]int { mut bitv := []bool{len: max - min + 1, init: false} for range in s.split(',') { - parse_range(range, min, max, mut bitv) ? + parse_range(range, min, max, mut bitv)? } return bitv_to_ints(bitv, min) diff --git a/src/cron/expression/expression_parse_test.v b/src/cron/expression/expression_parse_test.v index 18531c0c..4eebc49e 100644 --- a/src/cron/expression/expression_parse_test.v +++ b/src/cron/expression/expression_parse_test.v @@ -13,14 +13,14 @@ fn parse_range_error(s string, min int, max int) string { // =====parse_range===== fn test_range_star_range() ? { mut bitv := []bool{len: 6, init: false} - parse_range('*', 0, 5, mut bitv) ? + parse_range('*', 0, 5, mut bitv)? assert bitv == [true, true, true, true, true, true] } fn test_range_number() ? { mut bitv := []bool{len: 6, init: false} - parse_range('4', 0, 5, mut bitv) ? + parse_range('4', 0, 5, mut bitv)? assert bitv_to_ints(bitv, 0) == [4] } @@ -39,14 +39,14 @@ fn test_range_number_invalid() ? { fn test_range_step_star_1() ? { mut bitv := []bool{len: 21, init: false} - parse_range('*/4', 0, 20, mut bitv) ? + parse_range('*/4', 0, 20, mut bitv)? assert bitv_to_ints(bitv, 0) == [0, 4, 8, 12, 16, 20] } fn test_range_step_star_2() ? { mut bitv := []bool{len: 8, init: false} - parse_range('*/3', 1, 8, mut bitv) ? + parse_range('*/3', 1, 8, mut bitv)? assert bitv_to_ints(bitv, 1) == [1, 4, 7] } @@ -61,7 +61,7 @@ fn test_range_step_zero() ? { fn test_range_step_number() ? { mut bitv := []bool{len: 21, init: false} - parse_range('5/4', 2, 22, mut bitv) ? + parse_range('5/4', 2, 22, mut bitv)? assert bitv_to_ints(bitv, 2) == [5, 9, 13, 17, 21] } @@ -76,23 +76,23 @@ fn test_range_step_number_too_small() ? { fn test_range_dash() ? { mut bitv := []bool{len: 10, init: false} - parse_range('4-8', 0, 9, mut bitv) ? + parse_range('4-8', 0, 9, mut bitv)? assert bitv_to_ints(bitv, 0) == [4, 5, 6, 7, 8] } fn test_range_dash_step() ? { mut bitv := []bool{len: 10, init: false} - parse_range('4-8/2', 0, 9, mut bitv) ? + parse_range('4-8/2', 0, 9, mut bitv)? assert bitv_to_ints(bitv, 0) == [4, 6, 8] } // =====parse_part===== fn test_part_single() ? { - assert parse_part('*', 0, 5) ? == [0, 1, 2, 3, 4, 5] + assert parse_part('*', 0, 5)? == [0, 1, 2, 3, 4, 5] } fn test_part_multiple() ? { - assert parse_part('*/2,2/3', 1, 8) ? == [1, 2, 3, 5, 7, 8] + assert parse_part('*/2,2/3', 1, 8)? == [1, 2, 3, 5, 7, 8] } diff --git a/src/cron/expression/expression_test.v b/src/cron/expression/expression_test.v index ef0283a7..9e25e924 100644 --- a/src/cron/expression/expression_test.v +++ b/src/cron/expression/expression_test.v @@ -3,11 +3,11 @@ module expression import time { parse } fn util_test_time(exp string, t1_str string, t2_str string) ? { - ce := parse_expression(exp) ? - t1 := parse(t1_str) ? - t2 := parse(t2_str) ? + ce := parse_expression(exp)? + t1 := parse(t1_str)? + t2 := parse(t2_str)? - t3 := ce.next(t1) ? + t3 := ce.next(t1)? assert t2.year == t3.year assert t2.month == t3.month @@ -18,17 +18,17 @@ fn util_test_time(exp string, t1_str string, t2_str string) ? { fn test_next_simple() ? { // Very simple - util_test_time('0 3', '2002-01-01 00:00:00', '2002-01-01 03:00:00') ? + util_test_time('0 3', '2002-01-01 00:00:00', '2002-01-01 03:00:00')? // Overlap to next day - util_test_time('0 3', '2002-01-01 03:00:00', '2002-01-02 03:00:00') ? - util_test_time('0 3', '2002-01-01 04:00:00', '2002-01-02 03:00:00') ? + util_test_time('0 3', '2002-01-01 03:00:00', '2002-01-02 03:00:00')? + util_test_time('0 3', '2002-01-01 04:00:00', '2002-01-02 03:00:00')? - util_test_time('0 3/4', '2002-01-01 04:00:00', '2002-01-01 07:00:00') ? + util_test_time('0 3/4', '2002-01-01 04:00:00', '2002-01-01 07:00:00')? // Overlap to next month - util_test_time('0 3', '2002-11-31 04:00:00', '2002-12-01 03:00:00') ? + util_test_time('0 3', '2002-11-31 04:00:00', '2002-12-01 03:00:00')? // Overlap to next year - util_test_time('0 3', '2002-12-31 04:00:00', '2003-01-01 03:00:00') ? + util_test_time('0 3', '2002-12-31 04:00:00', '2003-01-01 03:00:00')? } diff --git a/src/db/db.v b/src/db/db.v index 5ec240de..7c1acf1e 100644 --- a/src/db/db.v +++ b/src/db/db.v @@ -8,7 +8,7 @@ struct VieterDb { // init initializes a database & adds the correct tables. pub fn init(db_path string) ?VieterDb { - conn := sqlite.connect(db_path) ? + conn := sqlite.connect(db_path)? sql conn { create table GitRepo diff --git a/src/docker/containers.v b/src/docker/containers.v index 2258f3bd..14ac12d9 100644 --- a/src/docker/containers.v +++ b/src/docker/containers.v @@ -11,7 +11,7 @@ struct Container { // containers returns a list of all currently running containers pub fn containers() ?[]Container { - res := request('GET', urllib.parse('/v1.41/containers/json') ?) ? + res := request('GET', urllib.parse('/v1.41/containers/json')?)? return json.decode([]Container, res.text) or {} } @@ -32,19 +32,19 @@ struct CreatedContainer { // create_container creates a container defined by the given configuration. If // successful, it returns the ID of the newly created container. pub fn create_container(c &NewContainer) ?string { - res := request_with_json('POST', urllib.parse('/v1.41/containers/create') ?, c) ? + res := request_with_json('POST', urllib.parse('/v1.41/containers/create')?, c)? if res.status_code != 201 { return error('Failed to create container.') } - return json.decode(CreatedContainer, res.text) ?.id + return json.decode(CreatedContainer, res.text)?.id } // start_container starts a container with a given ID. It returns whether the // container was started or not. pub fn start_container(id string) ?bool { - res := request('POST', urllib.parse('/v1.41/containers/$id/start') ?) ? + res := request('POST', urllib.parse('/v1.41/containers/$id/start')?)? return res.status_code == 204 } @@ -70,18 +70,18 @@ pub mut: // inspect_container returns the result of inspecting a container with a given // ID. pub fn inspect_container(id string) ?ContainerInspect { - res := request('GET', urllib.parse('/v1.41/containers/$id/json') ?) ? + res := request('GET', urllib.parse('/v1.41/containers/$id/json')?)? if res.status_code != 200 { return error('Failed to inspect container.') } - mut data := json.decode(ContainerInspect, res.text) ? + mut data := json.decode(ContainerInspect, res.text)? - data.state.start_time = time.parse_rfc3339(data.state.start_time_str) ? + data.state.start_time = time.parse_rfc3339(data.state.start_time_str)? if data.state.status == 'exited' { - data.state.end_time = time.parse_rfc3339(data.state.end_time_str) ? + data.state.end_time = time.parse_rfc3339(data.state.end_time_str)? } return data @@ -89,7 +89,7 @@ pub fn inspect_container(id string) ?ContainerInspect { // remove_container removes a container with a given ID. pub fn remove_container(id string) ?bool { - res := request('DELETE', urllib.parse('/v1.41/containers/$id') ?) ? + res := request('DELETE', urllib.parse('/v1.41/containers/$id')?)? return res.status_code == 204 } @@ -97,7 +97,7 @@ pub fn remove_container(id string) ?bool { // get_container_logs retrieves the logs for a Docker container, both stdout & // stderr. pub fn get_container_logs(id string) ?string { - res := request('GET', urllib.parse('/v1.41/containers/$id/logs?stdout=true&stderr=true') ?) ? + res := request('GET', urllib.parse('/v1.41/containers/$id/logs?stdout=true&stderr=true')?)? mut res_bytes := res.text.bytes() // Docker uses a special "stream" format for their logs, so we have to diff --git a/src/docker/docker.v b/src/docker/docker.v index 5deef830..305e8253 100644 --- a/src/docker/docker.v +++ b/src/docker/docker.v @@ -27,7 +27,7 @@ fn send(req &string) ?http.Response { // Write the request to the socket s.write_string(req) or { return error('Failed to write request to socket ${docker.socket}.') } - s.wait_for_write() ? + s.wait_for_write()? mut c := 0 mut buf := []u8{len: docker.buf_len} @@ -56,7 +56,7 @@ fn send(req &string) ?http.Response { // A chunked HTTP response always ends with '0\r\n\r\n'. for res.len < 5 || res#[-5..] != [u8(`0`), `\r`, `\n`, `\r`, `\n`] { // Wait for the server to respond - s.wait_for_write() ? + s.wait_for_write()? for { c = s.read(mut buf) or { diff --git a/src/docker/images.v b/src/docker/images.v index e94ceca2..2e873fa0 100644 --- a/src/docker/images.v +++ b/src/docker/images.v @@ -11,13 +11,13 @@ pub: // pull_image pulls tries to pull the image for the given image & tag pub fn pull_image(image string, tag string) ?http.Response { - return request('POST', urllib.parse('/v1.41/images/create?fromImage=$image&tag=$tag') ?) + return request('POST', urllib.parse('/v1.41/images/create?fromImage=$image&tag=$tag')?) } // create_image_from_container creates a new image from a container with the // given repo & tag, given the container's ID. pub fn create_image_from_container(id string, repo string, tag string) ?Image { - res := request('POST', urllib.parse('/v1.41/commit?container=$id&repo=$repo&tag=$tag') ?) ? + res := request('POST', urllib.parse('/v1.41/commit?container=$id&repo=$repo&tag=$tag')?)? if res.status_code != 201 { return error('Failed to create image from container.') @@ -28,7 +28,7 @@ pub fn create_image_from_container(id string, repo string, tag string) ?Image { // remove_image removes the image with the given ID. pub fn remove_image(id string) ?bool { - res := request('DELETE', urllib.parse('/v1.41/images/$id') ?) ? + res := request('DELETE', urllib.parse('/v1.41/images/$id')?)? return res.status_code == 200 } diff --git a/src/env/env.v b/src/env/env.v index b2b5f446..d1459311 100644 --- a/src/env/env.v +++ b/src/env/env.v @@ -50,7 +50,7 @@ pub fn load(path string) ?T { if os.exists(path) { // We don't use reflect here because reflect also sets any fields not // in the toml back to their zero value, which we don't want - doc := toml.parse_file(path) ? + doc := toml.parse_file(path)? $for field in T.fields { s := doc.value(field.name) @@ -66,7 +66,7 @@ pub fn load(path string) ?T { } $for field in T.fields { - env_value := get_env_var(field.name) ? + env_value := get_env_var(field.name)? // The value of an env var will always take precedence over the toml // file. diff --git a/src/package/package.v b/src/package/package.v index a1042b54..273322fa 100644 --- a/src/package/package.v +++ b/src/package/package.v @@ -159,7 +159,7 @@ pub fn read_pkg_archive(pkg_path string) ?Pkg { pkg_text := unsafe { buf.vstring_with_len(size).clone() } - pkg_info = parse_pkg_info_string(pkg_text) ? + pkg_info = parse_pkg_info_string(pkg_text)? } else { C.archive_read_data_skip(a) } diff --git a/src/repo/repo.v b/src/repo/repo.v index e27e232b..817ec304 100644 --- a/src/repo/repo.v +++ b/src/repo/repo.v @@ -53,22 +53,22 @@ pub fn (r &RepoGroupManager) add_pkg_from_path(repo string, pkg_path string) ?Re return error('Failed to read package file: $err.msg()') } - added := r.add_pkg_in_repo(repo, pkg) ? + added := r.add_pkg_in_repo(repo, pkg)? // If the add was successful, we move the file to the packages directory for arch in added { repo_pkg_path := os.real_path(os.join_path(r.pkg_dir, repo, arch)) dest_path := os.join_path_single(repo_pkg_path, pkg.filename()) - os.mkdir_all(repo_pkg_path) ? + os.mkdir_all(repo_pkg_path)? // We create hard links so that "any" arch packages aren't stored // multiple times - os.link(pkg_path, dest_path) ? + os.link(pkg_path, dest_path)? } // After linking, we can remove the original file - os.rm(pkg_path) ? + os.rm(pkg_path)? return RepoAddResult{ added: added.len > 0 @@ -87,7 +87,7 @@ fn (r &RepoGroupManager) add_pkg_in_repo(repo string, pkg &package.Pkg) ?[]strin // A package not of arch 'any' can be handled easily by adding it to the // respective repo if pkg.info.arch != 'any' { - if r.add_pkg_in_arch_repo(repo, pkg.info.arch, pkg) ? { + if r.add_pkg_in_arch_repo(repo, pkg.info.arch, pkg)? { return [pkg.info.arch] } else { return [] @@ -104,7 +104,7 @@ fn (r &RepoGroupManager) add_pkg_in_repo(repo string, pkg &package.Pkg) ?[]strin // If this is the first package that's added to the repo, the directory // won't exist yet if os.exists(repo_dir) { - arch_repos = os.ls(repo_dir) ? + arch_repos = os.ls(repo_dir)? } // The default_arch should always be updated when a package with arch 'any' @@ -118,7 +118,7 @@ fn (r &RepoGroupManager) add_pkg_in_repo(repo string, pkg &package.Pkg) ?[]strin // We add the package to each repository. If any of the repositories // return true, the result of the function is also true. for arch in arch_repos { - if r.add_pkg_in_arch_repo(repo, arch, pkg) ? { + if r.add_pkg_in_arch_repo(repo, arch, pkg)? { added << arch } } @@ -135,22 +135,22 @@ fn (r &RepoGroupManager) add_pkg_in_arch_repo(repo string, arch string, pkg &pac pkg_dir := os.join_path(r.repos_dir, repo, arch, '$pkg.info.name-$pkg.info.version') // Remove the previous version of the package, if present - r.remove_pkg_from_arch_repo(repo, arch, pkg.info.name, false) ? + r.remove_pkg_from_arch_repo(repo, arch, pkg.info.name, false)? os.mkdir_all(pkg_dir) or { return error('Failed to create package directory.') } os.write_file(os.join_path_single(pkg_dir, 'desc'), pkg.to_desc()) or { - os.rmdir_all(pkg_dir) ? + os.rmdir_all(pkg_dir)? return error('Failed to write desc file.') } os.write_file(os.join_path_single(pkg_dir, 'files'), pkg.to_files()) or { - os.rmdir_all(pkg_dir) ? + os.rmdir_all(pkg_dir)? return error('Failed to write files file.') } - r.sync(repo, arch) ? + r.sync(repo, arch)? return true } @@ -168,7 +168,7 @@ fn (r &RepoGroupManager) remove_pkg_from_arch_repo(repo string, arch string, pkg // We iterate over every directory in the repo dir // TODO filter so we only check directories - for d in os.ls(repo_dir) ? { + for d in os.ls(repo_dir)? { // Because a repository only allows a single version of each package, // we need only compare whether the name of the package is the same, // not the version. @@ -178,22 +178,22 @@ fn (r &RepoGroupManager) remove_pkg_from_arch_repo(repo string, arch string, pkg // We lock the mutex here to prevent other routines from creating a // new archive while we remove an entry lock r.mutex { - os.rmdir_all(os.join_path_single(repo_dir, d)) ? + os.rmdir_all(os.join_path_single(repo_dir, d))? } // Also remove the package archive repo_pkg_dir := os.join_path(r.pkg_dir, repo, arch) - archives := os.ls(repo_pkg_dir) ?.filter(it.split('-')#[..-3].join('-') == name) + archives := os.ls(repo_pkg_dir)?.filter(it.split('-')#[..-3].join('-') == name) for archive_name in archives { full_path := os.join_path_single(repo_pkg_dir, archive_name) - os.rm(full_path) ? + os.rm(full_path)? } // Sync the db archives if requested if sync { - r.sync(repo, arch) ? + r.sync(repo, arch)? } return true diff --git a/src/repo/sync.v b/src/repo/sync.v index 9c5e7ed2..73d21c88 100644 --- a/src/repo/sync.v +++ b/src/repo/sync.v @@ -54,7 +54,7 @@ fn (r &RepoGroupManager) sync(repo string, arch string) ? { C.archive_write_open_filename(a_files, &char(files_path.str)) // Iterate over each directory - for d in os.ls(subrepo_path) ?.filter(os.is_dir(os.join_path_single(subrepo_path, + for d in os.ls(subrepo_path)?.filter(os.is_dir(os.join_path_single(subrepo_path, it))) { // desc mut inner_path := os.join_path_single(d, 'desc') diff --git a/src/server/cli.v b/src/server/cli.v index 4d396661..556efcfe 100644 --- a/src/server/cli.v +++ b/src/server/cli.v @@ -18,10 +18,10 @@ pub fn cmd() cli.Command { name: 'server' description: 'Start the Vieter server.' execute: fn (cmd cli.Command) ? { - config_file := cmd.flags.get_string('config-file') ? - conf := env.load(config_file) ? + config_file := cmd.flags.get_string('config-file')? + conf := env.load(config_file)? - server(conf) ? + server(conf)? } } } diff --git a/src/server/logs.v b/src/server/logs.v index b048dc4f..21331e5b 100644 --- a/src/server/logs.v +++ b/src/server/logs.v @@ -56,8 +56,8 @@ fn (mut app App) get_log_content(id int) web.Result { // parse_query_time unescapes an HTTP query parameter & tries to parse it as a // time.Time struct. fn parse_query_time(query string) ?time.Time { - unescaped := urllib.query_unescape(query) ? - t := time.parse(unescaped) ? + unescaped := urllib.query_unescape(query)? + t := time.parse(unescaped)? return t } diff --git a/src/util/util.v b/src/util/util.v index c1af30ec..66026215 100644 --- a/src/util/util.v +++ b/src/util/util.v @@ -25,7 +25,7 @@ pub fn exit_with_message(code int, msg string) { // reader_to_file writes the contents of a BufferedReader to a file pub fn reader_to_file(mut reader io.BufferedReader, length int, path string) ? { - mut file := os.create(path) ? + mut file := os.create(path)? defer { file.close() } diff --git a/src/web/web.v b/src/web/web.v index 3e7b0478..f237e246 100644 --- a/src/web/web.v +++ b/src/web/web.v @@ -190,7 +190,7 @@ pub fn (ctx Context) before_request() {} // send_string fn send_string(mut conn net.TcpConn, s string) ? { - conn.write(s.bytes()) ? + conn.write(s.bytes())? } // send_response_to_client sends a response to the client From 5c5c2f87e0e74c940210744b525cb6e9b302c188 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 9 May 2022 16:16:25 +0200 Subject: [PATCH 006/236] feat(cli): lists are now properly formatted in an ascii table --- src/console/console.v | 55 +++++++++++++++++++++++++++++++++++++++++ src/console/git/git.v | 6 ++--- src/console/logs/logs.v | 14 ++++++----- 3 files changed, 66 insertions(+), 9 deletions(-) diff --git a/src/console/console.v b/src/console/console.v index 6f296bdd..dfb2fac8 100644 --- a/src/console/console.v +++ b/src/console/console.v @@ -1 +1,56 @@ module console + +import arrays +import strings + +// pretty_table converts a list of string data into a pretty table. Many thanks +// to @hungrybluedev in the Vlang Discord for providing this code! +// https://ptb.discord.com/channels/592103645835821068/592106336838352923/970278787143045192 +pub fn pretty_table(header []string, data [][]string) ?string { + column_count := header.len + + mut column_widths := []int{len: column_count, init: header[it].len} + + for values in data { + for col, value in values { + if column_widths[col] < value.len { + column_widths[col] = value.len + } + } + } + + single_line_length := arrays.sum(column_widths)? + (column_count + 1) * 3 - 4 + + horizontal_line := '+' + strings.repeat(`-`, single_line_length) + '+' + mut buffer := strings.new_builder(data.len * single_line_length) + + buffer.writeln(horizontal_line) + + buffer.write_string('| ') + for col, head in header { + if col != 0 { + buffer.write_string(' | ') + } + buffer.write_string(head) + buffer.write_string(strings.repeat(` `, column_widths[col] - head.len)) + } + buffer.writeln(' |') + + buffer.writeln(horizontal_line) + + for values in data { + buffer.write_string('| ') + for col, value in values { + if col != 0 { + buffer.write_string(' | ') + } + buffer.write_string(value) + buffer.write_string(strings.repeat(` `, column_widths[col] - value.len)) + } + buffer.writeln(' |') + } + + buffer.writeln(horizontal_line) + + return buffer.str() +} diff --git a/src/console/git/git.v b/src/console/git/git.v index 83837446..db9dec59 100644 --- a/src/console/git/git.v +++ b/src/console/git/git.v @@ -4,6 +4,7 @@ import cli import env import cron.expression { parse_expression } import client +import console struct Config { address string [required] @@ -122,10 +123,9 @@ pub fn cmd() cli.Command { fn list(conf Config) ? { c := client.new(conf.address, conf.api_key) repos := c.get_git_repos()? + data := repos.map([it.id.str(), it.url, it.branch, it.repo]) - for repo in repos { - println('$repo.id\t$repo.url\t$repo.branch\t$repo.repo') - } + println(console.pretty_table(['id', 'url', 'branch', 'repo'], data)?) } // add adds a new repository to the server's list. diff --git a/src/console/logs/logs.v b/src/console/logs/logs.v index 0e965232..6400e801 100644 --- a/src/console/logs/logs.v +++ b/src/console/logs/logs.v @@ -4,6 +4,7 @@ import cli import env import client import db +import console struct Config { address string [required] @@ -66,10 +67,11 @@ pub fn cmd() cli.Command { } // print_log_list prints a list of logs. -fn print_log_list(logs []db.BuildLog) { - for log in logs { - println('$log.id\t$log.start_time\t$log.exit_code') - } +fn print_log_list(logs []db.BuildLog) ? { + data := logs.map([it.id.str(), it.repo_id.str(), it.start_time.str(), + it.exit_code.str()]) + + println(console.pretty_table(['id', 'repo', 'start time', 'exit code'], data)?) } // list prints a list of all build logs. @@ -77,7 +79,7 @@ fn list(conf Config) ? { c := client.new(conf.address, conf.api_key) logs := c.get_build_logs()?.data - print_log_list(logs) + print_log_list(logs)? } // list prints a list of all build logs for a given repo. @@ -85,7 +87,7 @@ fn list_for_repo(conf Config, repo_id int) ? { c := client.new(conf.address, conf.api_key) logs := c.get_build_logs_for_repo(repo_id)?.data - print_log_list(logs) + print_log_list(logs)? } // info print the detailed info for a given build log. From 473c7ec06b10735b82959a50df7bb470d12ded7a Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Wed, 11 May 2022 10:03:43 +0200 Subject: [PATCH 007/236] feat(docker): start of new socket implementation --- src/docker/docker.v | 4 ---- src/docker/socket.v | 47 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 src/docker/socket.v diff --git a/src/docker/docker.v b/src/docker/docker.v index 305e8253..ce01e7ef 100644 --- a/src/docker/docker.v +++ b/src/docker/docker.v @@ -5,10 +5,6 @@ import net.urllib import net.http import json -const socket = '/var/run/docker.sock' - -const buf_len = 1024 - // send writes a request to the Docker socket, waits for a response & returns // it. fn send(req &string) ?http.Response { diff --git a/src/docker/socket.v b/src/docker/socket.v new file mode 100644 index 00000000..3a72f144 --- /dev/null +++ b/src/docker/socket.v @@ -0,0 +1,47 @@ +module docker + +import net.unix +import io +import net.http + +const socket = '/var/run/docker.sock' + +const buf_len = 10 * 1024 + +pub struct DockerDaemon { +mut: + socket &unix.StreamConn + reader &io.BufferedReader +} + +pub fn new_conn() ?DockerDaemon { + s := unix.connect_stream(socket) ? + + d := DockerDaemon{socket: s, reader: io.new_buffered_reader(reader: s)} + + return d +} + +fn (mut d DockerDaemon) send_request(req &string) ? { + d.socket.write_string(req) ? + d.socket.wait_for_write() ? +} + +// read_response_head consumes the socket's contents until it encounters +// '\n\n', after which it parses the response as an HTTP response. +fn (mut d DockerDaemon) read_response_head() ?http.Response { + mut c := 0 + mut buf := [buf_len]u8{len: docker.buf_len} + mut res := []u8{} + + for { + c = d.socket.read(mut buf) ? + res << buf[..c] + + if res#[-2..] == [u8(`\n`), `\n`] { + break + } + } + + return http.parse_response(res.bytestr()) +} From dd9958ea28982d39c4dba26036cb9f03c304e27d Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Fri, 13 May 2022 21:28:24 +0200 Subject: [PATCH 008/236] refactor: ran vfmt with new defaults feat(docker): started work on new implementation --- src/docker/containers.v | 13 +++++-- src/docker/docker.v | 20 +++++----- src/docker/socket.v | 83 ++++++++++++++++++++++++++++++++++------- src/main.v | 2 +- 4 files changed, 88 insertions(+), 30 deletions(-) diff --git a/src/docker/containers.v b/src/docker/containers.v index 14ac12d9..3b674e69 100644 --- a/src/docker/containers.v +++ b/src/docker/containers.v @@ -3,17 +3,22 @@ module docker import json import net.urllib import time +import net.http struct Container { id string [json: Id] names []string [json: Names] } -// containers returns a list of all currently running containers -pub fn containers() ?[]Container { - res := request('GET', urllib.parse('/v1.41/containers/json')?)? +pub fn (mut d DockerDaemon) containers() ?[]Container { + d.send_request('GET', urllib.parse('/v1.41/containers/json')?)? + res_header := d.read_response_head()? + content_length := res_header.header.get(http.CommonHeader.content_length)?.int() + res := d.read_response_body(content_length)? - return json.decode([]Container, res.text) or {} + data := json.decode([]Container, res)? + + return data } pub struct NewContainer { diff --git a/src/docker/docker.v b/src/docker/docker.v index ce01e7ef..fa83d897 100644 --- a/src/docker/docker.v +++ b/src/docker/docker.v @@ -9,8 +9,8 @@ import json // it. fn send(req &string) ?http.Response { // Open a connection to the socket - mut s := unix.connect_stream(docker.socket) or { - return error('Failed to connect to socket ${docker.socket}.') + mut s := unix.connect_stream(socket) or { + return error('Failed to connect to socket ${socket}.') } defer { @@ -21,19 +21,19 @@ fn send(req &string) ?http.Response { } // Write the request to the socket - s.write_string(req) or { return error('Failed to write request to socket ${docker.socket}.') } + s.write_string(req) or { return error('Failed to write request to socket ${socket}.') } s.wait_for_write()? mut c := 0 - mut buf := []u8{len: docker.buf_len} + mut buf := []u8{len: buf_len} mut res := []u8{} for { - c = s.read(mut buf) or { return error('Failed to read data from socket ${docker.socket}.') } + c = s.read(mut buf) or { return error('Failed to read data from socket ${socket}.') } res << buf[..c] - if c < docker.buf_len { + if c < buf_len { break } } @@ -41,7 +41,7 @@ fn send(req &string) ?http.Response { // After reading the first part of the response, we parse it into an HTTP // response. If it isn't chunked, we return early with the data. parsed := http.parse_response(res.bytestr()) or { - return error('Failed to parse HTTP response from socket ${docker.socket}.') + return error('Failed to parse HTTP response from socket ${socket}.') } if parsed.header.get(http.CommonHeader.transfer_encoding) or { '' } != 'chunked' { @@ -55,12 +55,10 @@ fn send(req &string) ?http.Response { s.wait_for_write()? for { - c = s.read(mut buf) or { - return error('Failed to read data from socket ${docker.socket}.') - } + c = s.read(mut buf) or { return error('Failed to read data from socket ${socket}.') } res << buf[..c] - if c < docker.buf_len { + if c < buf_len { break } } diff --git a/src/docker/socket.v b/src/docker/socket.v index 3a72f144..78154352 100644 --- a/src/docker/socket.v +++ b/src/docker/socket.v @@ -3,10 +3,15 @@ module docker import net.unix import io import net.http +import strings +import net.urllib +import json -const socket = '/var/run/docker.sock' - -const buf_len = 10 * 1024 +const ( + socket = '/var/run/docker.sock' + buf_len = 10 * 1024 + http_separator = [u8(`\r`), `\n`, `\r`, `\n`] +) pub struct DockerDaemon { mut: @@ -14,34 +19,84 @@ mut: reader &io.BufferedReader } -pub fn new_conn() ?DockerDaemon { - s := unix.connect_stream(socket) ? +pub fn new_conn() ?&DockerDaemon { + s := unix.connect_stream(docker.socket)? - d := DockerDaemon{socket: s, reader: io.new_buffered_reader(reader: s)} + d := &DockerDaemon{ + socket: s + reader: io.new_buffered_reader(reader: s) + } return d } -fn (mut d DockerDaemon) send_request(req &string) ? { - d.socket.write_string(req) ? - d.socket.wait_for_write() ? +pub fn (mut d DockerDaemon) send_request(method string, url urllib.URL) ? { + req := '$method $url.request_uri() HTTP/1.1\nHost: localhost\n\n' + + d.socket.write_string(req)? +} + +pub fn (mut d DockerDaemon) send_request_with_body(method string, url urllib.URL, content_type string, body string) ? { + req := '$method $url.request_uri() HTTP/1.1\nHost: localhost\nContent-Type: $content_type\nContent-Length: $body.len\n\n$body\n\n' + + d.socket.write_string(req)? +} + +pub fn (mut d DockerDaemon) request_with_json(method string, url urllib.URL, data &T) ? { + body := json.encode(data) + + return request_with_body(method, url, 'application/json', body) } // read_response_head consumes the socket's contents until it encounters -// '\n\n', after which it parses the response as an HTTP response. -fn (mut d DockerDaemon) read_response_head() ?http.Response { +// '\r\n\r\n', after which it parses the response as an HTTP response. +pub fn (mut d DockerDaemon) read_response_head() ?http.Response { mut c := 0 - mut buf := [buf_len]u8{len: docker.buf_len} + mut buf := []u8{len: 4} mut res := []u8{} for { - c = d.socket.read(mut buf) ? + c = d.reader.read(mut buf)? res << buf[..c] - if res#[-2..] == [u8(`\n`), `\n`] { + mut i := 0 + mut match_len := 0 + + for i + match_len < c { + if buf[i + match_len] == docker.http_separator[match_len] { + match_len += 1 + } else { + i += match_len + 1 + match_len = 0 + } + } + + if match_len == 4 { break + } else if match_len > 0 { + mut buf2 := []u8{len: 4 - match_len} + c2 := d.reader.read(mut buf2)? + res << buf2[..c2] + + if buf2 == docker.http_separator[match_len..] { + break + } } } return http.parse_response(res.bytestr()) } + +pub fn (mut d DockerDaemon) read_response_body(length int) ?string { + mut buf := []u8{len: docker.buf_len} + mut c := 0 + mut builder := strings.new_builder(docker.buf_len) + + for builder.len < length { + c = d.reader.read(mut buf) or { break } + + builder.write(buf[..c])? + } + + return builder.str() +} diff --git a/src/main.v b/src/main.v index 41d0d331..db6d5ef9 100644 --- a/src/main.v +++ b/src/main.v @@ -31,7 +31,7 @@ fn main() { logs.cmd(), ] } - app.setup() app.parse(os.args) + return } From 4c97489f8a7f868e2ea58afbe9fe3fd47477ed02 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Fri, 13 May 2022 22:03:06 +0200 Subject: [PATCH 009/236] feat(docker): partially migrate to new code --- src/build/build.v | 11 ++++++---- src/docker/containers.v | 47 ++++++++++++++++++++++++++++++++++++++--- src/docker/socket.v | 12 +++++++++-- src/main.v | 1 + 4 files changed, 62 insertions(+), 9 deletions(-) diff --git a/src/build/build.v b/src/build/build.v index 41f68e28..b4708538 100644 --- a/src/build/build.v +++ b/src/build/build.v @@ -17,6 +17,8 @@ const build_image_repo = 'vieter-build' // makepkg with. The base image should be some Linux distribution that uses // Pacman as its package manager. pub fn create_build_image(base_image string) ?string { + mut dd := docker.new_conn() ? + commands := [ // Update repos & install required packages 'pacman -Syu --needed --noconfirm base-devel git' @@ -48,12 +50,13 @@ pub fn create_build_image(base_image string) ?string { // We pull the provided image docker.pull_image(image_name, image_tag)? - id := docker.create_container(c)? - docker.start_container(id)? + id := dd.create_container(c)?.id + /* id := docker.create_container(c)? */ + dd.start_container(id)? // This loop waits until the container has stopped, so we can remove it after for { - data := docker.inspect_container(id)? + data := dd.inspect_container(id)? if !data.state.running { break @@ -68,7 +71,7 @@ pub fn create_build_image(base_image string) ?string { // conflicts. tag := time.sys_mono_now().str() image := docker.create_image_from_container(id, 'vieter-build', tag)? - docker.remove_container(id)? + dd.remove_container(id)? return image.id } diff --git a/src/docker/containers.v b/src/docker/containers.v index 3b674e69..98116ccc 100644 --- a/src/docker/containers.v +++ b/src/docker/containers.v @@ -12,15 +12,14 @@ struct Container { pub fn (mut d DockerDaemon) containers() ?[]Container { d.send_request('GET', urllib.parse('/v1.41/containers/json')?)? - res_header := d.read_response_head()? - content_length := res_header.header.get(http.CommonHeader.content_length)?.int() - res := d.read_response_body(content_length)? + _, res := d.read_response()? data := json.decode([]Container, res)? return data } +[params] pub struct NewContainer { image string [json: Image] entrypoint []string [json: Entrypoint] @@ -31,7 +30,25 @@ pub struct NewContainer { } struct CreatedContainer { +pub: id string [json: Id] + warnings []string [json: Warnings] +} + +pub fn (mut d DockerDaemon) create_container(c NewContainer) ?CreatedContainer { + d.send_request_with_json('POST', urllib.parse('/v1.41/containers/create')?, c)? + _, res := d.read_response()? + + data := json.decode(CreatedContainer, res)? + + return data +} + +pub fn (mut d DockerDaemon) start_container(id string) ?bool { + d.send_request('POST', urllib.parse('/v1.41/containers/$id/start')?)? + head := d.read_response_head() ? + + return head.status_code == 204 } // create_container creates a container defined by the given configuration. If @@ -72,6 +89,25 @@ pub mut: end_time time.Time [skip] } +pub fn (mut d DockerDaemon) inspect_container(id string) ?ContainerInspect { + d.send_request('GET', urllib.parse('/v1.41/containers/$id/json')?)? + head, body := d.read_response()? + + if head.status_code != 200 { + return error('Failed to inspect container.') + } + + mut data := json.decode(ContainerInspect, body)? + + data.state.start_time = time.parse_rfc3339(data.state.start_time_str)? + + if data.state.status == 'exited' { + data.state.end_time = time.parse_rfc3339(data.state.end_time_str)? + } + + return data +} + // inspect_container returns the result of inspecting a container with a given // ID. pub fn inspect_container(id string) ?ContainerInspect { @@ -92,6 +128,11 @@ pub fn inspect_container(id string) ?ContainerInspect { return data } +pub fn (mut d DockerDaemon) remove_container(id string) ? { + d.send_request('DELETE', urllib.parse('/v1.41/containers/$id')?)? + head := d.read_response_head() ? +} + // remove_container removes a container with a given ID. pub fn remove_container(id string) ?bool { res := request('DELETE', urllib.parse('/v1.41/containers/$id')?)? diff --git a/src/docker/socket.v b/src/docker/socket.v index 78154352..41f9f6dd 100644 --- a/src/docker/socket.v +++ b/src/docker/socket.v @@ -42,10 +42,10 @@ pub fn (mut d DockerDaemon) send_request_with_body(method string, url urllib.URL d.socket.write_string(req)? } -pub fn (mut d DockerDaemon) request_with_json(method string, url urllib.URL, data &T) ? { +pub fn (mut d DockerDaemon) send_request_with_json(method string, url urllib.URL, data &T) ? { body := json.encode(data) - return request_with_body(method, url, 'application/json', body) + return d.send_request_with_body(method, url, 'application/json', body) } // read_response_head consumes the socket's contents until it encounters @@ -100,3 +100,11 @@ pub fn (mut d DockerDaemon) read_response_body(length int) ?string { return builder.str() } + +pub fn (mut d DockerDaemon) read_response() ?(http.Response, string) { + head := d.read_response_head()? + content_length := head.header.get(http.CommonHeader.content_length)?.int() + res := d.read_response_body(content_length)? + + return head, res +} diff --git a/src/main.v b/src/main.v index db6d5ef9..6b1e7bc2 100644 --- a/src/main.v +++ b/src/main.v @@ -7,6 +7,7 @@ import build import console.git import console.logs import cron +import docker fn main() { mut app := cli.Command{ From da46b8b4ae4483fed98b911e0523182ffe2156e8 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Fri, 13 May 2022 22:15:30 +0200 Subject: [PATCH 010/236] feat(docker): error when HTTP requests fail --- src/build/build.v | 1 + src/docker/containers.v | 42 ++++++++++++++++++++++++++++++++++------- src/docker/socket.v | 6 ++++++ src/docker/stream.v | 9 +++++++++ 4 files changed, 51 insertions(+), 7 deletions(-) create mode 100644 src/docker/stream.v diff --git a/src/build/build.v b/src/build/build.v index b4708538..ae103189 100644 --- a/src/build/build.v +++ b/src/build/build.v @@ -56,6 +56,7 @@ pub fn create_build_image(base_image string) ?string { // This loop waits until the container has stopped, so we can remove it after for { + println('wot') data := dd.inspect_container(id)? if !data.state.running { diff --git a/src/docker/containers.v b/src/docker/containers.v index 98116ccc..e2da938f 100644 --- a/src/docker/containers.v +++ b/src/docker/containers.v @@ -5,6 +5,10 @@ import net.urllib import time import net.http +struct DockerError { + message string +} + struct Container { id string [json: Id] names []string [json: Names] @@ -12,7 +16,13 @@ struct Container { pub fn (mut d DockerDaemon) containers() ?[]Container { d.send_request('GET', urllib.parse('/v1.41/containers/json')?)? - _, res := d.read_response()? + head, res := d.read_response()? + + if head.status_code != 200 { + data := json.decode(DockerError, res)? + + return error(data.message) + } data := json.decode([]Container, res)? @@ -37,18 +47,28 @@ pub: pub fn (mut d DockerDaemon) create_container(c NewContainer) ?CreatedContainer { d.send_request_with_json('POST', urllib.parse('/v1.41/containers/create')?, c)? - _, res := d.read_response()? + head, res := d.read_response()? + + if head.status_code != 201 { + data := json.decode(DockerError, res)? + + return error(data.message) + } data := json.decode(CreatedContainer, res)? return data } -pub fn (mut d DockerDaemon) start_container(id string) ?bool { +pub fn (mut d DockerDaemon) start_container(id string) ? { d.send_request('POST', urllib.parse('/v1.41/containers/$id/start')?)? - head := d.read_response_head() ? + head, body := d.read_response() ? - return head.status_code == 204 + if head.status_code != 204 { + data := json.decode(DockerError, body)? + + return error(data.message) + } } // create_container creates a container defined by the given configuration. If @@ -94,7 +114,9 @@ pub fn (mut d DockerDaemon) inspect_container(id string) ?ContainerInspect { head, body := d.read_response()? if head.status_code != 200 { - return error('Failed to inspect container.') + data := json.decode(DockerError, body)? + + return error(data.message) } mut data := json.decode(ContainerInspect, body)? @@ -130,7 +152,13 @@ pub fn inspect_container(id string) ?ContainerInspect { pub fn (mut d DockerDaemon) remove_container(id string) ? { d.send_request('DELETE', urllib.parse('/v1.41/containers/$id')?)? - head := d.read_response_head() ? + head, body := d.read_response() ? + + if head.status_code != 204 { + data := json.decode(DockerError, body)? + + return error(data.message) + } } // remove_container removes a container with a given ID. diff --git a/src/docker/socket.v b/src/docker/socket.v index 41f9f6dd..749f14e6 100644 --- a/src/docker/socket.v +++ b/src/docker/socket.v @@ -50,6 +50,8 @@ pub fn (mut d DockerDaemon) send_request_with_json(method string, url urllib. // read_response_head consumes the socket's contents until it encounters // '\r\n\r\n', after which it parses the response as an HTTP response. +// Importantly, this function never consumes past the HTTP separator, so the +// body can be read fully later on. pub fn (mut d DockerDaemon) read_response_head() ?http.Response { mut c := 0 mut buf := []u8{len: 4} @@ -88,6 +90,10 @@ pub fn (mut d DockerDaemon) read_response_head() ?http.Response { } pub fn (mut d DockerDaemon) read_response_body(length int) ?string { + if length == 0 { + return '' + } + mut buf := []u8{len: docker.buf_len} mut c := 0 mut builder := strings.new_builder(docker.buf_len) diff --git a/src/docker/stream.v b/src/docker/stream.v new file mode 100644 index 00000000..24c51c12 --- /dev/null +++ b/src/docker/stream.v @@ -0,0 +1,9 @@ +module docker + +import io + +struct ChunkedResponseStream { + reader io.Reader +} + + From 92cbea69d6e4f8929ff5f397b36d7852e7084172 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 14 May 2022 21:55:19 +0200 Subject: [PATCH 011/236] feat(docker): added ChunkedResponseReader implementation --- src/build/build.v | 6 +-- src/docker/containers.v | 6 +-- src/docker/images.v | 26 ++++++++++++ src/docker/socket.v | 35 ++++++++++------ src/docker/stream.v | 92 ++++++++++++++++++++++++++++++++++++++++- src/util/util.v | 16 +++++++ 6 files changed, 161 insertions(+), 20 deletions(-) diff --git a/src/build/build.v b/src/build/build.v index ae103189..dd72c174 100644 --- a/src/build/build.v +++ b/src/build/build.v @@ -17,7 +17,7 @@ const build_image_repo = 'vieter-build' // makepkg with. The base image should be some Linux distribution that uses // Pacman as its package manager. pub fn create_build_image(base_image string) ?string { - mut dd := docker.new_conn() ? + mut dd := docker.new_conn()? commands := [ // Update repos & install required packages @@ -51,12 +51,12 @@ pub fn create_build_image(base_image string) ?string { docker.pull_image(image_name, image_tag)? id := dd.create_container(c)?.id - /* id := docker.create_container(c)? */ + // id := docker.create_container(c)? dd.start_container(id)? // This loop waits until the container has stopped, so we can remove it after for { - println('wot') + println('wot') data := dd.inspect_container(id)? if !data.state.running { diff --git a/src/docker/containers.v b/src/docker/containers.v index e2da938f..0027747d 100644 --- a/src/docker/containers.v +++ b/src/docker/containers.v @@ -41,7 +41,7 @@ pub struct NewContainer { struct CreatedContainer { pub: - id string [json: Id] + id string [json: Id] warnings []string [json: Warnings] } @@ -62,7 +62,7 @@ pub fn (mut d DockerDaemon) create_container(c NewContainer) ?CreatedContainer { pub fn (mut d DockerDaemon) start_container(id string) ? { d.send_request('POST', urllib.parse('/v1.41/containers/$id/start')?)? - head, body := d.read_response() ? + head, body := d.read_response()? if head.status_code != 204 { data := json.decode(DockerError, body)? @@ -152,7 +152,7 @@ pub fn inspect_container(id string) ?ContainerInspect { pub fn (mut d DockerDaemon) remove_container(id string) ? { d.send_request('DELETE', urllib.parse('/v1.41/containers/$id')?)? - head, body := d.read_response() ? + head, body := d.read_response()? if head.status_code != 204 { data := json.decode(DockerError, body)? diff --git a/src/docker/images.v b/src/docker/images.v index 2e873fa0..22491139 100644 --- a/src/docker/images.v +++ b/src/docker/images.v @@ -9,6 +9,32 @@ pub: id string [json: Id] } +pub fn (mut d DockerDaemon) pull_image(image string, tag string) ? { + d.send_request('POST', urllib.parse('/v1.41/images/create?fromImage=$image&tag=$tag')?)? + head := d.read_response_head()? + + if head.status_code != 200 { + content_length := head.header.get(http.CommonHeader.content_length)?.int() + body := d.read_response_body(content_length)? + data := json.decode(DockerError, body)? + + return error(data.message) + } + + mut body := d.get_chunked_response_reader() + + mut buf := []u8{len: 1024} + + for { + c := body.read(mut buf)? + + if c == 0 { + break + } + print(buf[..c].bytestr()) + } +} + // pull_image pulls tries to pull the image for the given image & tag pub fn pull_image(image string, tag string) ?http.Response { return request('POST', urllib.parse('/v1.41/images/create?fromImage=$image&tag=$tag')?) diff --git a/src/docker/socket.v b/src/docker/socket.v index 749f14e6..444d9a96 100644 --- a/src/docker/socket.v +++ b/src/docker/socket.v @@ -6,11 +6,13 @@ import net.http import strings import net.urllib import json +import util const ( - socket = '/var/run/docker.sock' - buf_len = 10 * 1024 - http_separator = [u8(`\r`), `\n`, `\r`, `\n`] + socket = '/var/run/docker.sock' + buf_len = 10 * 1024 + http_separator = [u8(`\r`), `\n`, `\r`, `\n`] + http_chunk_separator = [u8(`\r`), `\n`] ) pub struct DockerDaemon { @@ -61,17 +63,18 @@ pub fn (mut d DockerDaemon) read_response_head() ?http.Response { c = d.reader.read(mut buf)? res << buf[..c] - mut i := 0 - mut match_len := 0 + match_len := util.match_array_in_array(buf[..c], docker.http_separator) + // mut i := 0 + // mut match_len := 0 - for i + match_len < c { - if buf[i + match_len] == docker.http_separator[match_len] { - match_len += 1 - } else { - i += match_len + 1 - match_len = 0 - } - } + // for i + match_len < c { + // if buf[i + match_len] == docker.http_separator[match_len] { + // match_len += 1 + // } else { + // i += match_len + 1 + // match_len = 0 + // } + //} if match_len == 4 { break @@ -114,3 +117,9 @@ pub fn (mut d DockerDaemon) read_response() ?(http.Response, string) { return head, res } + +pub fn (mut d DockerDaemon) get_chunked_response_reader() &ChunkedResponseReader { + r := new_chunked_response_reader(d.reader) + + return r +} diff --git a/src/docker/stream.v b/src/docker/stream.v index 24c51c12..fbc48bad 100644 --- a/src/docker/stream.v +++ b/src/docker/stream.v @@ -1,9 +1,99 @@ module docker import io +import util +import encoding.binary +import encoding.hex -struct ChunkedResponseStream { +struct ChunkedResponseReader { +mut: reader io.Reader + // buf []u8 + // offset int + // len int + bytes_left_in_chunk u64 + end_of_stream bool + started bool } +pub fn new_chunked_response_reader(reader io.Reader) &ChunkedResponseReader { + r := &ChunkedResponseReader{ + reader: reader + } + return r +} + +// We satisfy the io.Reader interface +pub fn (mut r ChunkedResponseReader) read(mut buf []u8) ?int { + if r.end_of_stream { + return none + } + + if r.bytes_left_in_chunk == 0 { + r.bytes_left_in_chunk = r.read_chunk_size()? + + if r.end_of_stream { + return none + } + } + + mut c := 0 + + if buf.len > r.bytes_left_in_chunk { + c = r.reader.read(mut buf[..r.bytes_left_in_chunk])? + } else { + c = r.reader.read(mut buf)? + } + + r.bytes_left_in_chunk -= u64(c) + + return c +} + +fn (mut r ChunkedResponseReader) read_chunk_size() ?u64 { + mut buf := []u8{len: 2} + mut res := []u8{} + + if r.started { + // Each chunk ends with a `\r\n` which we want to skip first + r.reader.read(mut buf) ? + } + + r.started = true + + for { + c := r.reader.read(mut buf)? + res << buf[..c] + + match_len := util.match_array_in_array(buf[..c], http_chunk_separator) + + if match_len == http_chunk_separator.len { + break + } + + if match_len > 0 { + mut buf2 := []u8{len: 2 - match_len} + c2 := r.reader.read(mut buf2)? + res << buf2[..c2] + + if buf2 == http_chunk_separator[match_len..] { + break + } + } + } + + mut num_data := hex.decode(res#[..-2].bytestr())? + + for num_data.len < 8 { + num_data.insert(0, 0) + } + + num := binary.big_endian_u64(num_data) + + if num == 0 { + r.end_of_stream = true + } + + return num +} diff --git a/src/util/util.v b/src/util/util.v index 66026215..f9f58edd 100644 --- a/src/util/util.v +++ b/src/util/util.v @@ -92,3 +92,19 @@ pub fn pretty_bytes(bytes int) string { return '${n:.2}${util.prefixes[i]}' } + +pub fn match_array_in_array(a1 []T, a2 []T) int { + mut i := 0 + mut match_len := 0 + + for i + match_len < a1.len { + if a1[i + match_len] == a2[match_len] { + match_len += 1 + } else { + i += match_len + 1 + match_len = 0 + } + } + + return match_len +} From f22ed29631a3e5d09b9496bb6bba255d4b5dc400 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 14 May 2022 22:39:52 +0200 Subject: [PATCH 012/236] feat(docker): added StreamFormatReader --- src/docker/containers.v | 15 +++++++++ src/docker/socket.v | 7 ++++ src/docker/stream.v | 73 ++++++++++++++++++++++++++++++++++------- 3 files changed, 84 insertions(+), 11 deletions(-) diff --git a/src/docker/containers.v b/src/docker/containers.v index 0027747d..ff48c698 100644 --- a/src/docker/containers.v +++ b/src/docker/containers.v @@ -168,6 +168,21 @@ pub fn remove_container(id string) ?bool { return res.status_code == 204 } +pub fn (mut d DockerDaemon) get_container_logs(id string) ?&StreamFormatReader { + d.send_request('GET', urllib.parse('/v1.41/containers/$id/logs?stdout=true&stderr=true')?)? + head := d.read_response_head()? + + if head.status_code != 200 { + content_length := head.header.get(http.CommonHeader.content_length)?.int() + body := d.read_response_body(content_length)? + data := json.decode(DockerError, body)? + + return error(data.message) + } + + return d.get_stream_format_reader() +} + // get_container_logs retrieves the logs for a Docker container, both stdout & // stderr. pub fn get_container_logs(id string) ?string { diff --git a/src/docker/socket.v b/src/docker/socket.v index 444d9a96..f6dfeb1c 100644 --- a/src/docker/socket.v +++ b/src/docker/socket.v @@ -123,3 +123,10 @@ pub fn (mut d DockerDaemon) get_chunked_response_reader() &ChunkedResponseReader return r } + +pub fn (mut d DockerDaemon) get_stream_format_reader() &StreamFormatReader { + r := new_chunked_response_reader(d.reader) + r2 := new_stream_format_reader(r) + + return r2 +} diff --git a/src/docker/stream.v b/src/docker/stream.v index fbc48bad..8822fd3a 100644 --- a/src/docker/stream.v +++ b/src/docker/stream.v @@ -7,13 +7,10 @@ import encoding.hex struct ChunkedResponseReader { mut: - reader io.Reader - // buf []u8 - // offset int - // len int + reader io.Reader bytes_left_in_chunk u64 end_of_stream bool - started bool + started bool } pub fn new_chunked_response_reader(reader io.Reader) &ChunkedResponseReader { @@ -39,7 +36,7 @@ pub fn (mut r ChunkedResponseReader) read(mut buf []u8) ?int { } mut c := 0 - + if buf.len > r.bytes_left_in_chunk { c = r.reader.read(mut buf[..r.bytes_left_in_chunk])? } else { @@ -56,8 +53,8 @@ fn (mut r ChunkedResponseReader) read_chunk_size() ?u64 { mut res := []u8{} if r.started { - // Each chunk ends with a `\r\n` which we want to skip first - r.reader.read(mut buf) ? + // Each chunk ends with a `\r\n` which we want to skip first + r.reader.read(mut buf)? } r.started = true @@ -85,9 +82,9 @@ fn (mut r ChunkedResponseReader) read_chunk_size() ?u64 { mut num_data := hex.decode(res#[..-2].bytestr())? - for num_data.len < 8 { - num_data.insert(0, 0) - } + for num_data.len < 8 { + num_data.insert(0, 0) + } num := binary.big_endian_u64(num_data) @@ -97,3 +94,57 @@ fn (mut r ChunkedResponseReader) read_chunk_size() ?u64 { return num } + +struct StreamFormatReader { + stdout bool + stderr bool + stdin bool +mut: + reader io.Reader + bytes_left_in_chunk u32 + end_of_stream bool +} + +pub fn new_stream_format_reader(reader io.Reader) &StreamFormatReader { + r := &StreamFormatReader{ + reader: reader + } + + return r +} + +pub fn (mut r StreamFormatReader) read(mut buf []u8) ?int { + if r.end_of_stream { + return none + } + + if r.bytes_left_in_chunk == 0 { + r.bytes_left_in_chunk = r.read_chunk_size()? + + if r.end_of_stream { + return none + } + } + + mut c := 0 + + if buf.len > r.bytes_left_in_chunk { + c = r.reader.read(mut buf[..r.bytes_left_in_chunk])? + } else { + c = r.reader.read(mut buf)? + } + + r.bytes_left_in_chunk -= u32(c) + + return c +} + +fn (mut r StreamFormatReader) read_chunk_size() ?u32 { + mut buf := []u8{len: 8} + + r.reader.read(mut buf)? + + num := binary.big_endian_u32(buf[4..]) + + return num +} From 1811ebbe3f3549b1034274a6a74675af4ef8469f Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 14 May 2022 23:42:20 +0200 Subject: [PATCH 013/236] doc: documented new Docker code --- src/docker/containers.v | 7 +++++++ src/docker/images.v | 1 + src/docker/socket.v | 33 +++++++++++++++++++-------------- src/docker/stream.v | 24 ++++++++++++++++++++---- src/util/util.v | 3 +++ 5 files changed, 50 insertions(+), 18 deletions(-) diff --git a/src/docker/containers.v b/src/docker/containers.v index ff48c698..05c9cc7f 100644 --- a/src/docker/containers.v +++ b/src/docker/containers.v @@ -14,6 +14,7 @@ struct Container { names []string [json: Names] } +// containers returns a list of all containers. pub fn (mut d DockerDaemon) containers() ?[]Container { d.send_request('GET', urllib.parse('/v1.41/containers/json')?)? head, res := d.read_response()? @@ -45,6 +46,7 @@ pub: warnings []string [json: Warnings] } +// create_container creates a new container with the given config. pub fn (mut d DockerDaemon) create_container(c NewContainer) ?CreatedContainer { d.send_request_with_json('POST', urllib.parse('/v1.41/containers/create')?, c)? head, res := d.read_response()? @@ -60,6 +62,7 @@ pub fn (mut d DockerDaemon) create_container(c NewContainer) ?CreatedContainer { return data } +// start_container starts the container with the given id. pub fn (mut d DockerDaemon) start_container(id string) ? { d.send_request('POST', urllib.parse('/v1.41/containers/$id/start')?)? head, body := d.read_response()? @@ -109,6 +112,7 @@ pub mut: end_time time.Time [skip] } +// inspect_container returns detailed information for a given container. pub fn (mut d DockerDaemon) inspect_container(id string) ?ContainerInspect { d.send_request('GET', urllib.parse('/v1.41/containers/$id/json')?)? head, body := d.read_response()? @@ -150,6 +154,7 @@ pub fn inspect_container(id string) ?ContainerInspect { return data } +// remove_container removes the container with the given id. pub fn (mut d DockerDaemon) remove_container(id string) ? { d.send_request('DELETE', urllib.parse('/v1.41/containers/$id')?)? head, body := d.read_response()? @@ -168,6 +173,8 @@ pub fn remove_container(id string) ?bool { return res.status_code == 204 } +// get_container_logs returns a reader object allowing access to the +// container's logs. pub fn (mut d DockerDaemon) get_container_logs(id string) ?&StreamFormatReader { d.send_request('GET', urllib.parse('/v1.41/containers/$id/logs?stdout=true&stderr=true')?)? head := d.read_response_head()? diff --git a/src/docker/images.v b/src/docker/images.v index 22491139..974b4083 100644 --- a/src/docker/images.v +++ b/src/docker/images.v @@ -9,6 +9,7 @@ pub: id string [json: Id] } +// pull_image pulls the given image:tag. pub fn (mut d DockerDaemon) pull_image(image string, tag string) ? { d.send_request('POST', urllib.parse('/v1.41/images/create?fromImage=$image&tag=$tag')?)? head := d.read_response_head()? diff --git a/src/docker/socket.v b/src/docker/socket.v index f6dfeb1c..b1e30800 100644 --- a/src/docker/socket.v +++ b/src/docker/socket.v @@ -21,6 +21,7 @@ mut: reader &io.BufferedReader } +// new_conn creates a new connection to the Docker daemon. pub fn new_conn() ?&DockerDaemon { s := unix.connect_stream(docker.socket)? @@ -32,18 +33,22 @@ pub fn new_conn() ?&DockerDaemon { return d } +// send_request sends an HTTP request without body. pub fn (mut d DockerDaemon) send_request(method string, url urllib.URL) ? { req := '$method $url.request_uri() HTTP/1.1\nHost: localhost\n\n' d.socket.write_string(req)? } +// send_request_with_body sends an HTTP request with the given body. pub fn (mut d DockerDaemon) send_request_with_body(method string, url urllib.URL, content_type string, body string) ? { req := '$method $url.request_uri() HTTP/1.1\nHost: localhost\nContent-Type: $content_type\nContent-Length: $body.len\n\n$body\n\n' d.socket.write_string(req)? } +// send_request_with_json is a convenience wrapper around +// send_request_with_body that encodes the input as JSON. pub fn (mut d DockerDaemon) send_request_with_json(method string, url urllib.URL, data &T) ? { body := json.encode(data) @@ -52,8 +57,8 @@ pub fn (mut d DockerDaemon) send_request_with_json(method string, url urllib. // read_response_head consumes the socket's contents until it encounters // '\r\n\r\n', after which it parses the response as an HTTP response. -// Importantly, this function never consumes past the HTTP separator, so the -// body can be read fully later on. +// Importantly, this function never consumes the reader past the HTTP +// separator, so the body can be read fully later on. pub fn (mut d DockerDaemon) read_response_head() ?http.Response { mut c := 0 mut buf := []u8{len: 4} @@ -64,21 +69,12 @@ pub fn (mut d DockerDaemon) read_response_head() ?http.Response { res << buf[..c] match_len := util.match_array_in_array(buf[..c], docker.http_separator) - // mut i := 0 - // mut match_len := 0 - - // for i + match_len < c { - // if buf[i + match_len] == docker.http_separator[match_len] { - // match_len += 1 - // } else { - // i += match_len + 1 - // match_len = 0 - // } - //} if match_len == 4 { break - } else if match_len > 0 { + } + + if match_len > 0 { mut buf2 := []u8{len: 4 - match_len} c2 := d.reader.read(mut buf2)? res << buf2[..c2] @@ -92,6 +88,8 @@ pub fn (mut d DockerDaemon) read_response_head() ?http.Response { return http.parse_response(res.bytestr()) } +// read_response_body reads `length` bytes from the stream. It can be used when +// the response encoding isn't chunked to fully read it. pub fn (mut d DockerDaemon) read_response_body(length int) ?string { if length == 0 { return '' @@ -110,6 +108,9 @@ pub fn (mut d DockerDaemon) read_response_body(length int) ?string { return builder.str() } +// read_response is a convenience function combining read_response_head & +// read_response_body. It can be used when you know for certain the response +// won't be chunked. pub fn (mut d DockerDaemon) read_response() ?(http.Response, string) { head := d.read_response_head()? content_length := head.header.get(http.CommonHeader.content_length)?.int() @@ -118,12 +119,16 @@ pub fn (mut d DockerDaemon) read_response() ?(http.Response, string) { return head, res } +// get_chunked_response_reader returns a ChunkedResponseReader using the socket +// as reader. pub fn (mut d DockerDaemon) get_chunked_response_reader() &ChunkedResponseReader { r := new_chunked_response_reader(d.reader) return r } +// get_stream_format_reader returns a StreamFormatReader using the socket as +// reader. pub fn (mut d DockerDaemon) get_stream_format_reader() &StreamFormatReader { r := new_chunked_response_reader(d.reader) r2 := new_stream_format_reader(r) diff --git a/src/docker/stream.v b/src/docker/stream.v index 8822fd3a..f20fe185 100644 --- a/src/docker/stream.v +++ b/src/docker/stream.v @@ -5,6 +5,8 @@ import util import encoding.binary import encoding.hex +// ChunkedResponseReader parses an underlying HTTP chunked response, exposing +// it as if it was a continuous stream of data. struct ChunkedResponseReader { mut: reader io.Reader @@ -13,6 +15,8 @@ mut: started bool } +// new_chunked_response_reader creates a new ChunkedResponseReader on the heap +// with the provided reader. pub fn new_chunked_response_reader(reader io.Reader) &ChunkedResponseReader { r := &ChunkedResponseReader{ reader: reader @@ -21,7 +25,7 @@ pub fn new_chunked_response_reader(reader io.Reader) &ChunkedResponseReader { return r } -// We satisfy the io.Reader interface +// read satisfies the io.Reader interface. pub fn (mut r ChunkedResponseReader) read(mut buf []u8) ?int { if r.end_of_stream { return none @@ -37,6 +41,8 @@ pub fn (mut r ChunkedResponseReader) read(mut buf []u8) ?int { mut c := 0 + // Make sure we don't read more than we can safely read. This is to avoid + // the underlying reader from becoming out of sync with our parsing: if buf.len > r.bytes_left_in_chunk { c = r.reader.read(mut buf[..r.bytes_left_in_chunk])? } else { @@ -48,6 +54,9 @@ pub fn (mut r ChunkedResponseReader) read(mut buf []u8) ?int { return c } +// read_chunk_size advances the reader & reads the size of the next HTTP chunk. +// This function should only be called if the previous chunk has been +// completely consumed. fn (mut r ChunkedResponseReader) read_chunk_size() ?u64 { mut buf := []u8{len: 2} mut res := []u8{} @@ -80,6 +89,7 @@ fn (mut r ChunkedResponseReader) read_chunk_size() ?u64 { } } + // The length of the next chunk is provided as a hexadecimal mut num_data := hex.decode(res#[..-2].bytestr())? for num_data.len < 8 { @@ -88,6 +98,8 @@ fn (mut r ChunkedResponseReader) read_chunk_size() ?u64 { num := binary.big_endian_u64(num_data) + // This only occurs for the very last chunk, which always reports a size of + // 0. if num == 0 { r.end_of_stream = true } @@ -95,16 +107,17 @@ fn (mut r ChunkedResponseReader) read_chunk_size() ?u64 { return num } +// StreamFormatReader parses an underlying stream of Docker logs, removing the +// header bytes. struct StreamFormatReader { - stdout bool - stderr bool - stdin bool mut: reader io.Reader bytes_left_in_chunk u32 end_of_stream bool } +// new_stream_format_reader creates a new StreamFormatReader using the given +// reader. pub fn new_stream_format_reader(reader io.Reader) &StreamFormatReader { r := &StreamFormatReader{ reader: reader @@ -113,6 +126,7 @@ pub fn new_stream_format_reader(reader io.Reader) &StreamFormatReader { return r } +// read satisfies the io.Reader interface. pub fn (mut r StreamFormatReader) read(mut buf []u8) ?int { if r.end_of_stream { return none @@ -139,6 +153,8 @@ pub fn (mut r StreamFormatReader) read(mut buf []u8) ?int { return c } +// read_chunk_size advances the reader & reads the header bytes for the length +// of the next chunk. fn (mut r StreamFormatReader) read_chunk_size() ?u32 { mut buf := []u8{len: 8} diff --git a/src/util/util.v b/src/util/util.v index f9f58edd..f805e6d8 100644 --- a/src/util/util.v +++ b/src/util/util.v @@ -93,6 +93,9 @@ pub fn pretty_bytes(bytes int) string { return '${n:.2}${util.prefixes[i]}' } +// match_array_in_array returns how many elements of a2 overlap with a1. For +// example, if a1 = "abcd" & a2 = "cd", the result will be 2. If the match is +// not at the end of a1, the result is 0. pub fn match_array_in_array(a1 []T, a2 []T) int { mut i := 0 mut match_len := 0 From 79fd9c1f8741f15cda9cb03200fa37576b047e91 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sun, 15 May 2022 00:23:52 +0200 Subject: [PATCH 014/236] fix(docker): read_response now handles chunked data --- src/build/build.v | 1 - src/docker/images.v | 5 +---- src/docker/socket.v | 22 +++++++++++++++++++--- src/util/util.v | 11 +++++++++++ 4 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/build/build.v b/src/build/build.v index dd72c174..98c56f5f 100644 --- a/src/build/build.v +++ b/src/build/build.v @@ -56,7 +56,6 @@ pub fn create_build_image(base_image string) ?string { // This loop waits until the container has stopped, so we can remove it after for { - println('wot') data := dd.inspect_container(id)? if !data.state.running { diff --git a/src/docker/images.v b/src/docker/images.v index 974b4083..ab735f24 100644 --- a/src/docker/images.v +++ b/src/docker/images.v @@ -27,11 +27,8 @@ pub fn (mut d DockerDaemon) pull_image(image string, tag string) ? { mut buf := []u8{len: 1024} for { - c := body.read(mut buf)? + c := body.read(mut buf) or { break } - if c == 0 { - break - } print(buf[..c].bytestr()) } } diff --git a/src/docker/socket.v b/src/docker/socket.v index b1e30800..dfa7ea7e 100644 --- a/src/docker/socket.v +++ b/src/docker/socket.v @@ -38,6 +38,9 @@ pub fn (mut d DockerDaemon) send_request(method string, url urllib.URL) ? { req := '$method $url.request_uri() HTTP/1.1\nHost: localhost\n\n' d.socket.write_string(req)? + + // When starting a new request, the reader needs to be reset. + d.reader = io.new_buffered_reader(reader: d.socket) } // send_request_with_body sends an HTTP request with the given body. @@ -45,6 +48,9 @@ pub fn (mut d DockerDaemon) send_request_with_body(method string, url urllib.URL req := '$method $url.request_uri() HTTP/1.1\nHost: localhost\nContent-Type: $content_type\nContent-Length: $body.len\n\n$body\n\n' d.socket.write_string(req)? + + // When starting a new request, the reader needs to be reset. + d.reader = io.new_buffered_reader(reader: d.socket) } // send_request_with_json is a convenience wrapper around @@ -108,11 +114,21 @@ pub fn (mut d DockerDaemon) read_response_body(length int) ?string { return builder.str() } -// read_response is a convenience function combining read_response_head & -// read_response_body. It can be used when you know for certain the response -// won't be chunked. +// read_response is a convenience function which always consumes the entire +// response & returns it. It should only be used when we're certain that the +// result isn't too large. pub fn (mut d DockerDaemon) read_response() ?(http.Response, string) { head := d.read_response_head()? + + if head.header.get(http.CommonHeader.transfer_encoding) or { '' } == 'chunked' { + mut builder := strings.new_builder(1024) + mut body := d.get_chunked_response_reader() + + util.reader_to_writer(mut body, mut builder) ? + + return head, builder.str() + } + content_length := head.header.get(http.CommonHeader.content_length)?.int() res := d.read_response_body(content_length)? diff --git a/src/util/util.v b/src/util/util.v index f805e6d8..7aabc1b9 100644 --- a/src/util/util.v +++ b/src/util/util.v @@ -23,6 +23,17 @@ pub fn exit_with_message(code int, msg string) { exit(code) } +// reader_to_writer tries to consume the entire reader & write it to the writer. +pub fn reader_to_writer(mut reader io.Reader, mut writer io.Writer) ? { + mut buf := []u8{len: 10 * 1024} + + for { + c := reader.read(mut buf) or { break } + + writer.write(buf) or { break } + } +} + // reader_to_file writes the contents of a BufferedReader to a file pub fn reader_to_file(mut reader io.BufferedReader, length int, path string) ? { mut file := os.create(path)? From e041682feae9d058d20e44dba58e185ba0310443 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sun, 15 May 2022 09:56:23 +0200 Subject: [PATCH 015/236] feat(docker): fully migrate build commands to new code --- src/build/build.v | 43 +++++++++++++++++++++++++++++++++---------- src/docker/images.v | 33 ++++++++++++++++++++++++++++++--- src/docker/socket.v | 7 ++++++- src/main.v | 1 - src/util/util.v | 2 +- 5 files changed, 70 insertions(+), 16 deletions(-) diff --git a/src/build/build.v b/src/build/build.v index 98c56f5f..0de91a64 100644 --- a/src/build/build.v +++ b/src/build/build.v @@ -6,6 +6,8 @@ import time import os import db import client +import strings +import util const container_build_dir = '/build' @@ -19,6 +21,10 @@ const build_image_repo = 'vieter-build' pub fn create_build_image(base_image string) ?string { mut dd := docker.new_conn()? + defer { + dd.close() or {} + } + commands := [ // Update repos & install required packages 'pacman -Syu --needed --noconfirm base-devel git' @@ -48,7 +54,7 @@ pub fn create_build_image(base_image string) ?string { image_tag := if image_parts.len > 1 { image_parts[1] } else { 'latest' } // We pull the provided image - docker.pull_image(image_name, image_tag)? + dd.pull_image(image_name, image_tag)? id := dd.create_container(c)?.id // id := docker.create_container(c)? @@ -70,7 +76,7 @@ pub fn create_build_image(base_image string) ?string { // TODO also add the base image's name into the image name to prevent // conflicts. tag := time.sys_mono_now().str() - image := docker.create_image_from_container(id, 'vieter-build', tag)? + image := dd.create_image_from_container(id, 'vieter-build', tag)? dd.remove_container(id)? return image.id @@ -88,6 +94,12 @@ pub: // provided GitRepo. The base image ID should be of an image previously created // by create_build_image. It returns the logs of the container. pub fn build_repo(address string, api_key string, base_image_id string, repo &db.GitRepo) ?BuildResult { + mut dd := docker.new_conn()? + + defer { + dd.close() or {} + } + build_arch := os.uname().machine // TODO what to do with PKGBUILDs that build multiple packages? @@ -115,27 +127,31 @@ pub fn build_repo(address string, api_key string, base_image_id string, repo &db user: 'builder:builder' } - id := docker.create_container(c)? - docker.start_container(id)? + id := dd.create_container(c)?.id + dd.start_container(id)? - mut data := docker.inspect_container(id)? + mut data := dd.inspect_container(id)? // This loop waits until the container has stopped, so we can remove it after for data.state.running { time.sleep(1 * time.second) - data = docker.inspect_container(id)? + data = dd.inspect_container(id)? } - logs := docker.get_container_logs(id)? + mut logs_stream := dd.get_container_logs(id)? - docker.remove_container(id)? + // Read in the entire stream + mut logs_builder := strings.new_builder(10 * 1024) + util.reader_to_writer(mut logs_stream, mut logs_builder)? + + dd.remove_container(id)? return BuildResult{ start_time: data.state.start_time end_time: data.state.end_time exit_code: data.state.exit_code - logs: logs + logs: logs_builder.str() } } @@ -153,7 +169,14 @@ fn build(conf Config, repo_id int) ? { res := build_repo(conf.address, conf.api_key, image_id, repo)? println('Removing build image...') - docker.remove_image(image_id)? + + mut dd := docker.new_conn()? + + defer { + dd.close() or {} + } + + dd.remove_image(image_id)? println('Uploading logs to Vieter...') c.add_build_log(repo.id, res.start_time, res.end_time, build_arch, res.exit_code, diff --git a/src/docker/images.v b/src/docker/images.v index ab735f24..51620afd 100644 --- a/src/docker/images.v +++ b/src/docker/images.v @@ -22,14 +22,13 @@ pub fn (mut d DockerDaemon) pull_image(image string, tag string) ? { return error(data.message) } + // Keep reading the body until the pull has completed mut body := d.get_chunked_response_reader() mut buf := []u8{len: 1024} for { - c := body.read(mut buf) or { break } - - print(buf[..c].bytestr()) + body.read(mut buf) or { break } } } @@ -38,6 +37,22 @@ pub fn pull_image(image string, tag string) ?http.Response { return request('POST', urllib.parse('/v1.41/images/create?fromImage=$image&tag=$tag')?) } +// create_image_from_container creates a new image from a container. +pub fn (mut d DockerDaemon) create_image_from_container(id string, repo string, tag string) ?Image { + d.send_request('POST', urllib.parse('/v1.41/commit?container=$id&repo=$repo&tag=$tag')?)? + head, body := d.read_response()? + + if head.status_code != 201 { + data := json.decode(DockerError, body)? + + return error(data.message) + } + + data := json.decode(Image, body)? + + return data +} + // create_image_from_container creates a new image from a container with the // given repo & tag, given the container's ID. pub fn create_image_from_container(id string, repo string, tag string) ?Image { @@ -56,3 +71,15 @@ pub fn remove_image(id string) ?bool { return res.status_code == 200 } + +// remove_image removes the image with the given id. +pub fn (mut d DockerDaemon) remove_image(id string) ? { + d.send_request('DELETE', urllib.parse('/v1.41/images/$id')?)? + head, body := d.read_response()? + + if head.status_code != 200 { + data := json.decode(DockerError, body)? + + return error(data.message) + } +} diff --git a/src/docker/socket.v b/src/docker/socket.v index dfa7ea7e..2ee9bff7 100644 --- a/src/docker/socket.v +++ b/src/docker/socket.v @@ -33,6 +33,11 @@ pub fn new_conn() ?&DockerDaemon { return d } +// close closes the underlying socket connection. +pub fn (mut d DockerDaemon) close() ? { + d.socket.close()? +} + // send_request sends an HTTP request without body. pub fn (mut d DockerDaemon) send_request(method string, url urllib.URL) ? { req := '$method $url.request_uri() HTTP/1.1\nHost: localhost\n\n' @@ -124,7 +129,7 @@ pub fn (mut d DockerDaemon) read_response() ?(http.Response, string) { mut builder := strings.new_builder(1024) mut body := d.get_chunked_response_reader() - util.reader_to_writer(mut body, mut builder) ? + util.reader_to_writer(mut body, mut builder)? return head, builder.str() } diff --git a/src/main.v b/src/main.v index 6b1e7bc2..db6d5ef9 100644 --- a/src/main.v +++ b/src/main.v @@ -7,7 +7,6 @@ import build import console.git import console.logs import cron -import docker fn main() { mut app := cli.Command{ diff --git a/src/util/util.v b/src/util/util.v index 7aabc1b9..324fb3d1 100644 --- a/src/util/util.v +++ b/src/util/util.v @@ -28,7 +28,7 @@ pub fn reader_to_writer(mut reader io.Reader, mut writer io.Writer) ? { mut buf := []u8{len: 10 * 1024} for { - c := reader.read(mut buf) or { break } + reader.read(mut buf) or { break } writer.write(buf) or { break } } From ce67208fbdda00d226c26358d5c42a1833c232f6 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sun, 15 May 2022 10:01:12 +0200 Subject: [PATCH 016/236] refactor(docker): remove old code --- src/cron/daemon/daemon.v | 13 ++- src/docker/containers.v | 70 ---------------- src/docker/docker.v | 175 +++++++++++++++++++++++++++------------ src/docker/images.v | 24 ------ src/docker/socket.v | 158 ----------------------------------- 5 files changed, 131 insertions(+), 309 deletions(-) delete mode 100644 src/docker/socket.v diff --git a/src/cron/daemon/daemon.v b/src/cron/daemon/daemon.v index ade8fcbf..da3b46e2 100644 --- a/src/cron/daemon/daemon.v +++ b/src/cron/daemon/daemon.v @@ -253,14 +253,21 @@ fn (mut d Daemon) rebuild_base_image() bool { fn (mut d Daemon) clean_old_base_images() { mut i := 0 + mut dd := docker.new_conn() or { + d.lerror('Failed to connect to Docker socket.') + return + } + + defer { + dd.close() or {} + } + for i < d.builder_images.len - 1 { // For each builder image, we try to remove it by calling the Docker // API. If the function returns an error or false, that means the image // wasn't deleted. Therefore, we move the index over. If the function // returns true, the array's length has decreased by one so we don't // move the index. - if !docker.remove_image(d.builder_images[i]) or { false } { - i += 1 - } + dd.remove_image(d.builder_images[i]) or { i += 1 } } } diff --git a/src/docker/containers.v b/src/docker/containers.v index 05c9cc7f..76054526 100644 --- a/src/docker/containers.v +++ b/src/docker/containers.v @@ -30,7 +30,6 @@ pub fn (mut d DockerDaemon) containers() ?[]Container { return data } -[params] pub struct NewContainer { image string [json: Image] entrypoint []string [json: Entrypoint] @@ -74,26 +73,6 @@ pub fn (mut d DockerDaemon) start_container(id string) ? { } } -// create_container creates a container defined by the given configuration. If -// successful, it returns the ID of the newly created container. -pub fn create_container(c &NewContainer) ?string { - res := request_with_json('POST', urllib.parse('/v1.41/containers/create')?, c)? - - if res.status_code != 201 { - return error('Failed to create container.') - } - - return json.decode(CreatedContainer, res.text)?.id -} - -// start_container starts a container with a given ID. It returns whether the -// container was started or not. -pub fn start_container(id string) ?bool { - res := request('POST', urllib.parse('/v1.41/containers/$id/start')?)? - - return res.status_code == 204 -} - struct ContainerInspect { pub mut: state ContainerState [json: State] @@ -134,26 +113,6 @@ pub fn (mut d DockerDaemon) inspect_container(id string) ?ContainerInspect { return data } -// inspect_container returns the result of inspecting a container with a given -// ID. -pub fn inspect_container(id string) ?ContainerInspect { - res := request('GET', urllib.parse('/v1.41/containers/$id/json')?)? - - if res.status_code != 200 { - return error('Failed to inspect container.') - } - - mut data := json.decode(ContainerInspect, res.text)? - - data.state.start_time = time.parse_rfc3339(data.state.start_time_str)? - - if data.state.status == 'exited' { - data.state.end_time = time.parse_rfc3339(data.state.end_time_str)? - } - - return data -} - // remove_container removes the container with the given id. pub fn (mut d DockerDaemon) remove_container(id string) ? { d.send_request('DELETE', urllib.parse('/v1.41/containers/$id')?)? @@ -166,13 +125,6 @@ pub fn (mut d DockerDaemon) remove_container(id string) ? { } } -// remove_container removes a container with a given ID. -pub fn remove_container(id string) ?bool { - res := request('DELETE', urllib.parse('/v1.41/containers/$id')?)? - - return res.status_code == 204 -} - // get_container_logs returns a reader object allowing access to the // container's logs. pub fn (mut d DockerDaemon) get_container_logs(id string) ?&StreamFormatReader { @@ -189,25 +141,3 @@ pub fn (mut d DockerDaemon) get_container_logs(id string) ?&StreamFormatReader { return d.get_stream_format_reader() } - -// get_container_logs retrieves the logs for a Docker container, both stdout & -// stderr. -pub fn get_container_logs(id string) ?string { - res := request('GET', urllib.parse('/v1.41/containers/$id/logs?stdout=true&stderr=true')?)? - mut res_bytes := res.text.bytes() - - // Docker uses a special "stream" format for their logs, so we have to - // clean up the data. - mut index := 0 - - for index < res_bytes.len { - // The reverse is required because V reads in the bytes differently - t := res_bytes[index + 4..index + 8].reverse() - len_length := unsafe { *(&u32(&t[0])) } - - res_bytes.delete_many(index, 8) - index += int(len_length) - } - - return res_bytes.bytestr() -} diff --git a/src/docker/docker.v b/src/docker/docker.v index fa83d897..2ee9bff7 100644 --- a/src/docker/docker.v +++ b/src/docker/docker.v @@ -1,91 +1,158 @@ module docker import net.unix -import net.urllib +import io import net.http +import strings +import net.urllib import json +import util -// send writes a request to the Docker socket, waits for a response & returns -// it. -fn send(req &string) ?http.Response { - // Open a connection to the socket - mut s := unix.connect_stream(socket) or { - return error('Failed to connect to socket ${socket}.') +const ( + socket = '/var/run/docker.sock' + buf_len = 10 * 1024 + http_separator = [u8(`\r`), `\n`, `\r`, `\n`] + http_chunk_separator = [u8(`\r`), `\n`] +) + +pub struct DockerDaemon { +mut: + socket &unix.StreamConn + reader &io.BufferedReader +} + +// new_conn creates a new connection to the Docker daemon. +pub fn new_conn() ?&DockerDaemon { + s := unix.connect_stream(docker.socket)? + + d := &DockerDaemon{ + socket: s + reader: io.new_buffered_reader(reader: s) } - defer { - // This or is required because otherwise, the V compiler segfaults for - // some reason - // https://github.com/vlang/v/issues/13534 - s.close() or {} - } + return d +} - // Write the request to the socket - s.write_string(req) or { return error('Failed to write request to socket ${socket}.') } +// close closes the underlying socket connection. +pub fn (mut d DockerDaemon) close() ? { + d.socket.close()? +} - s.wait_for_write()? +// send_request sends an HTTP request without body. +pub fn (mut d DockerDaemon) send_request(method string, url urllib.URL) ? { + req := '$method $url.request_uri() HTTP/1.1\nHost: localhost\n\n' + d.socket.write_string(req)? + + // When starting a new request, the reader needs to be reset. + d.reader = io.new_buffered_reader(reader: d.socket) +} + +// send_request_with_body sends an HTTP request with the given body. +pub fn (mut d DockerDaemon) send_request_with_body(method string, url urllib.URL, content_type string, body string) ? { + req := '$method $url.request_uri() HTTP/1.1\nHost: localhost\nContent-Type: $content_type\nContent-Length: $body.len\n\n$body\n\n' + + d.socket.write_string(req)? + + // When starting a new request, the reader needs to be reset. + d.reader = io.new_buffered_reader(reader: d.socket) +} + +// send_request_with_json is a convenience wrapper around +// send_request_with_body that encodes the input as JSON. +pub fn (mut d DockerDaemon) send_request_with_json(method string, url urllib.URL, data &T) ? { + body := json.encode(data) + + return d.send_request_with_body(method, url, 'application/json', body) +} + +// read_response_head consumes the socket's contents until it encounters +// '\r\n\r\n', after which it parses the response as an HTTP response. +// Importantly, this function never consumes the reader past the HTTP +// separator, so the body can be read fully later on. +pub fn (mut d DockerDaemon) read_response_head() ?http.Response { mut c := 0 - mut buf := []u8{len: buf_len} + mut buf := []u8{len: 4} mut res := []u8{} for { - c = s.read(mut buf) or { return error('Failed to read data from socket ${socket}.') } + c = d.reader.read(mut buf)? res << buf[..c] - if c < buf_len { + match_len := util.match_array_in_array(buf[..c], docker.http_separator) + + if match_len == 4 { break } - } - // After reading the first part of the response, we parse it into an HTTP - // response. If it isn't chunked, we return early with the data. - parsed := http.parse_response(res.bytestr()) or { - return error('Failed to parse HTTP response from socket ${socket}.') - } + if match_len > 0 { + mut buf2 := []u8{len: 4 - match_len} + c2 := d.reader.read(mut buf2)? + res << buf2[..c2] - if parsed.header.get(http.CommonHeader.transfer_encoding) or { '' } != 'chunked' { - return parsed - } - - // We loop until we've encountered the end of the chunked response - // A chunked HTTP response always ends with '0\r\n\r\n'. - for res.len < 5 || res#[-5..] != [u8(`0`), `\r`, `\n`, `\r`, `\n`] { - // Wait for the server to respond - s.wait_for_write()? - - for { - c = s.read(mut buf) or { return error('Failed to read data from socket ${socket}.') } - res << buf[..c] - - if c < buf_len { + if buf2 == docker.http_separator[match_len..] { break } } } - // Decode chunked response return http.parse_response(res.bytestr()) } -// request_with_body sends a request to the Docker socket with the given body. -fn request_with_body(method string, url urllib.URL, content_type string, body string) ?http.Response { - req := '$method $url.request_uri() HTTP/1.1\nHost: localhost\nContent-Type: $content_type\nContent-Length: $body.len\n\n$body\n\n' +// read_response_body reads `length` bytes from the stream. It can be used when +// the response encoding isn't chunked to fully read it. +pub fn (mut d DockerDaemon) read_response_body(length int) ?string { + if length == 0 { + return '' + } - return send(req) + mut buf := []u8{len: docker.buf_len} + mut c := 0 + mut builder := strings.new_builder(docker.buf_len) + + for builder.len < length { + c = d.reader.read(mut buf) or { break } + + builder.write(buf[..c])? + } + + return builder.str() } -// request sends a request to the Docker socket with an empty body. -fn request(method string, url urllib.URL) ?http.Response { - req := '$method $url.request_uri() HTTP/1.1\nHost: localhost\n\n' +// read_response is a convenience function which always consumes the entire +// response & returns it. It should only be used when we're certain that the +// result isn't too large. +pub fn (mut d DockerDaemon) read_response() ?(http.Response, string) { + head := d.read_response_head()? - return send(req) + if head.header.get(http.CommonHeader.transfer_encoding) or { '' } == 'chunked' { + mut builder := strings.new_builder(1024) + mut body := d.get_chunked_response_reader() + + util.reader_to_writer(mut body, mut builder)? + + return head, builder.str() + } + + content_length := head.header.get(http.CommonHeader.content_length)?.int() + res := d.read_response_body(content_length)? + + return head, res } -// request_with_json sends a request to the Docker socket with a given JSON -// payload -pub fn request_with_json(method string, url urllib.URL, data &T) ?http.Response { - body := json.encode(data) +// get_chunked_response_reader returns a ChunkedResponseReader using the socket +// as reader. +pub fn (mut d DockerDaemon) get_chunked_response_reader() &ChunkedResponseReader { + r := new_chunked_response_reader(d.reader) - return request_with_body(method, url, 'application/json', body) + return r +} + +// get_stream_format_reader returns a StreamFormatReader using the socket as +// reader. +pub fn (mut d DockerDaemon) get_stream_format_reader() &StreamFormatReader { + r := new_chunked_response_reader(d.reader) + r2 := new_stream_format_reader(r) + + return r2 } diff --git a/src/docker/images.v b/src/docker/images.v index 51620afd..58669058 100644 --- a/src/docker/images.v +++ b/src/docker/images.v @@ -32,11 +32,6 @@ pub fn (mut d DockerDaemon) pull_image(image string, tag string) ? { } } -// pull_image pulls tries to pull the image for the given image & tag -pub fn pull_image(image string, tag string) ?http.Response { - return request('POST', urllib.parse('/v1.41/images/create?fromImage=$image&tag=$tag')?) -} - // create_image_from_container creates a new image from a container. pub fn (mut d DockerDaemon) create_image_from_container(id string, repo string, tag string) ?Image { d.send_request('POST', urllib.parse('/v1.41/commit?container=$id&repo=$repo&tag=$tag')?)? @@ -53,25 +48,6 @@ pub fn (mut d DockerDaemon) create_image_from_container(id string, repo string, return data } -// create_image_from_container creates a new image from a container with the -// given repo & tag, given the container's ID. -pub fn create_image_from_container(id string, repo string, tag string) ?Image { - res := request('POST', urllib.parse('/v1.41/commit?container=$id&repo=$repo&tag=$tag')?)? - - if res.status_code != 201 { - return error('Failed to create image from container.') - } - - return json.decode(Image, res.text) or {} -} - -// remove_image removes the image with the given ID. -pub fn remove_image(id string) ?bool { - res := request('DELETE', urllib.parse('/v1.41/images/$id')?)? - - return res.status_code == 200 -} - // remove_image removes the image with the given id. pub fn (mut d DockerDaemon) remove_image(id string) ? { d.send_request('DELETE', urllib.parse('/v1.41/images/$id')?)? diff --git a/src/docker/socket.v b/src/docker/socket.v deleted file mode 100644 index 2ee9bff7..00000000 --- a/src/docker/socket.v +++ /dev/null @@ -1,158 +0,0 @@ -module docker - -import net.unix -import io -import net.http -import strings -import net.urllib -import json -import util - -const ( - socket = '/var/run/docker.sock' - buf_len = 10 * 1024 - http_separator = [u8(`\r`), `\n`, `\r`, `\n`] - http_chunk_separator = [u8(`\r`), `\n`] -) - -pub struct DockerDaemon { -mut: - socket &unix.StreamConn - reader &io.BufferedReader -} - -// new_conn creates a new connection to the Docker daemon. -pub fn new_conn() ?&DockerDaemon { - s := unix.connect_stream(docker.socket)? - - d := &DockerDaemon{ - socket: s - reader: io.new_buffered_reader(reader: s) - } - - return d -} - -// close closes the underlying socket connection. -pub fn (mut d DockerDaemon) close() ? { - d.socket.close()? -} - -// send_request sends an HTTP request without body. -pub fn (mut d DockerDaemon) send_request(method string, url urllib.URL) ? { - req := '$method $url.request_uri() HTTP/1.1\nHost: localhost\n\n' - - d.socket.write_string(req)? - - // When starting a new request, the reader needs to be reset. - d.reader = io.new_buffered_reader(reader: d.socket) -} - -// send_request_with_body sends an HTTP request with the given body. -pub fn (mut d DockerDaemon) send_request_with_body(method string, url urllib.URL, content_type string, body string) ? { - req := '$method $url.request_uri() HTTP/1.1\nHost: localhost\nContent-Type: $content_type\nContent-Length: $body.len\n\n$body\n\n' - - d.socket.write_string(req)? - - // When starting a new request, the reader needs to be reset. - d.reader = io.new_buffered_reader(reader: d.socket) -} - -// send_request_with_json is a convenience wrapper around -// send_request_with_body that encodes the input as JSON. -pub fn (mut d DockerDaemon) send_request_with_json(method string, url urllib.URL, data &T) ? { - body := json.encode(data) - - return d.send_request_with_body(method, url, 'application/json', body) -} - -// read_response_head consumes the socket's contents until it encounters -// '\r\n\r\n', after which it parses the response as an HTTP response. -// Importantly, this function never consumes the reader past the HTTP -// separator, so the body can be read fully later on. -pub fn (mut d DockerDaemon) read_response_head() ?http.Response { - mut c := 0 - mut buf := []u8{len: 4} - mut res := []u8{} - - for { - c = d.reader.read(mut buf)? - res << buf[..c] - - match_len := util.match_array_in_array(buf[..c], docker.http_separator) - - if match_len == 4 { - break - } - - if match_len > 0 { - mut buf2 := []u8{len: 4 - match_len} - c2 := d.reader.read(mut buf2)? - res << buf2[..c2] - - if buf2 == docker.http_separator[match_len..] { - break - } - } - } - - return http.parse_response(res.bytestr()) -} - -// read_response_body reads `length` bytes from the stream. It can be used when -// the response encoding isn't chunked to fully read it. -pub fn (mut d DockerDaemon) read_response_body(length int) ?string { - if length == 0 { - return '' - } - - mut buf := []u8{len: docker.buf_len} - mut c := 0 - mut builder := strings.new_builder(docker.buf_len) - - for builder.len < length { - c = d.reader.read(mut buf) or { break } - - builder.write(buf[..c])? - } - - return builder.str() -} - -// read_response is a convenience function which always consumes the entire -// response & returns it. It should only be used when we're certain that the -// result isn't too large. -pub fn (mut d DockerDaemon) read_response() ?(http.Response, string) { - head := d.read_response_head()? - - if head.header.get(http.CommonHeader.transfer_encoding) or { '' } == 'chunked' { - mut builder := strings.new_builder(1024) - mut body := d.get_chunked_response_reader() - - util.reader_to_writer(mut body, mut builder)? - - return head, builder.str() - } - - content_length := head.header.get(http.CommonHeader.content_length)?.int() - res := d.read_response_body(content_length)? - - return head, res -} - -// get_chunked_response_reader returns a ChunkedResponseReader using the socket -// as reader. -pub fn (mut d DockerDaemon) get_chunked_response_reader() &ChunkedResponseReader { - r := new_chunked_response_reader(d.reader) - - return r -} - -// get_stream_format_reader returns a StreamFormatReader using the socket as -// reader. -pub fn (mut d DockerDaemon) get_stream_format_reader() &StreamFormatReader { - r := new_chunked_response_reader(d.reader) - r2 := new_stream_format_reader(r) - - return r2 -} From 97cdaa18e1bb435ca512e81b70cb4fa3bc13f414 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 16 May 2022 13:03:44 +0200 Subject: [PATCH 017/236] refactor(docker): split stream separator code into own function --- CHANGELOG.md | 2 ++ src/docker/docker.v | 23 +---------------------- src/docker/stream.v | 23 ++--------------------- src/util/util.v | 36 ++++++++++++++++++++++++++++++++++-- 4 files changed, 39 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e17cd60..c86761c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Web API for adding & querying build logs * CLI commands to access build logs API * Cron build logs are uploaded to above API +* Proper ASCII table output in CLI ### Changed @@ -20,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Official Arch packages are now split between `vieter` & `vieter-git` * `vieter` is the latest release * `vieter-git` is the latest commit on the dev branch +* Full refactor of Docker socket code ## [0.3.0-alpha.1](https://git.rustybever.be/vieter/vieter/src/tag/0.3.0-alpha.1) diff --git a/src/docker/docker.v b/src/docker/docker.v index 2ee9bff7..f612d1f1 100644 --- a/src/docker/docker.v +++ b/src/docker/docker.v @@ -71,30 +71,9 @@ pub fn (mut d DockerDaemon) send_request_with_json(method string, url urllib. // Importantly, this function never consumes the reader past the HTTP // separator, so the body can be read fully later on. pub fn (mut d DockerDaemon) read_response_head() ?http.Response { - mut c := 0 - mut buf := []u8{len: 4} mut res := []u8{} - for { - c = d.reader.read(mut buf)? - res << buf[..c] - - match_len := util.match_array_in_array(buf[..c], docker.http_separator) - - if match_len == 4 { - break - } - - if match_len > 0 { - mut buf2 := []u8{len: 4 - match_len} - c2 := d.reader.read(mut buf2)? - res << buf2[..c2] - - if buf2 == docker.http_separator[match_len..] { - break - } - } - } + util.read_until_separator(mut d.reader, mut res, http_separator) ? return http.parse_response(res.bytestr()) } diff --git a/src/docker/stream.v b/src/docker/stream.v index f20fe185..ed73098f 100644 --- a/src/docker/stream.v +++ b/src/docker/stream.v @@ -59,7 +59,6 @@ pub fn (mut r ChunkedResponseReader) read(mut buf []u8) ?int { // completely consumed. fn (mut r ChunkedResponseReader) read_chunk_size() ?u64 { mut buf := []u8{len: 2} - mut res := []u8{} if r.started { // Each chunk ends with a `\r\n` which we want to skip first @@ -68,26 +67,8 @@ fn (mut r ChunkedResponseReader) read_chunk_size() ?u64 { r.started = true - for { - c := r.reader.read(mut buf)? - res << buf[..c] - - match_len := util.match_array_in_array(buf[..c], http_chunk_separator) - - if match_len == http_chunk_separator.len { - break - } - - if match_len > 0 { - mut buf2 := []u8{len: 2 - match_len} - c2 := r.reader.read(mut buf2)? - res << buf2[..c2] - - if buf2 == http_chunk_separator[match_len..] { - break - } - } - } + mut res := []u8{} + util.read_until_separator(mut r.reader, mut res, http_chunk_separator) ? // The length of the next chunk is provided as a hexadecimal mut num_data := hex.decode(res#[..-2].bytestr())? diff --git a/src/util/util.v b/src/util/util.v index 324fb3d1..9cf3011c 100644 --- a/src/util/util.v +++ b/src/util/util.v @@ -28,9 +28,14 @@ pub fn reader_to_writer(mut reader io.Reader, mut writer io.Writer) ? { mut buf := []u8{len: 10 * 1024} for { - reader.read(mut buf) or { break } + bytes_read := reader.read(mut buf) or { break } + mut bytes_written := 0 - writer.write(buf) or { break } + for bytes_written < bytes_read { + c := writer.write(buf[bytes_written..bytes_read]) or { break } + + bytes_written += c + } } } @@ -122,3 +127,30 @@ pub fn match_array_in_array(a1 []T, a2 []T) int { return match_len } + +// read_until_separator consumes an io.Reader until it encounters some +// separator array. The data read is stored inside the provided res array. +pub fn read_until_separator(mut reader io.Reader, mut res []u8, sep []u8) ? { + mut buf := []u8{len: sep.len} + + for { + c := reader.read(mut buf)? + res << buf[..c] + + match_len := match_array_in_array(buf[..c], sep) + + if match_len == sep.len { + break + } + + if match_len > 0 { + match_left := sep.len - match_len + c2 := reader.read(mut buf[..match_left])? + res << buf[..c2] + + if buf[..c2] == sep[match_len..] { + break + } + } + } +} From 1d3c7a1651c238c6818a3434f230a4a54ab7840c Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 16 May 2022 14:09:21 +0200 Subject: [PATCH 018/236] refactor(docker): renamed DockerDaemon to DockerConn --- src/docker/README.md | 5 +++++ src/docker/containers.v | 12 ++++++------ src/docker/docker.v | 26 +++++++++++++------------- src/docker/images.v | 6 +++--- src/docker/stream.v | 2 +- 5 files changed, 28 insertions(+), 23 deletions(-) create mode 100644 src/docker/README.md diff --git a/src/docker/README.md b/src/docker/README.md new file mode 100644 index 00000000..5236277e --- /dev/null +++ b/src/docker/README.md @@ -0,0 +1,5 @@ +# docker + +This module implements part of the Docker Engine API v1.41 +([documentation](https://docs.docker.com/engine/api/v1.41/)) using socket-based +HTTP communication. diff --git a/src/docker/containers.v b/src/docker/containers.v index 76054526..62c8031c 100644 --- a/src/docker/containers.v +++ b/src/docker/containers.v @@ -15,7 +15,7 @@ struct Container { } // containers returns a list of all containers. -pub fn (mut d DockerDaemon) containers() ?[]Container { +pub fn (mut d DockerConn) containers() ?[]Container { d.send_request('GET', urllib.parse('/v1.41/containers/json')?)? head, res := d.read_response()? @@ -46,7 +46,7 @@ pub: } // create_container creates a new container with the given config. -pub fn (mut d DockerDaemon) create_container(c NewContainer) ?CreatedContainer { +pub fn (mut d DockerConn) create_container(c NewContainer) ?CreatedContainer { d.send_request_with_json('POST', urllib.parse('/v1.41/containers/create')?, c)? head, res := d.read_response()? @@ -62,7 +62,7 @@ pub fn (mut d DockerDaemon) create_container(c NewContainer) ?CreatedContainer { } // start_container starts the container with the given id. -pub fn (mut d DockerDaemon) start_container(id string) ? { +pub fn (mut d DockerConn) start_container(id string) ? { d.send_request('POST', urllib.parse('/v1.41/containers/$id/start')?)? head, body := d.read_response()? @@ -92,7 +92,7 @@ pub mut: } // inspect_container returns detailed information for a given container. -pub fn (mut d DockerDaemon) inspect_container(id string) ?ContainerInspect { +pub fn (mut d DockerConn) inspect_container(id string) ?ContainerInspect { d.send_request('GET', urllib.parse('/v1.41/containers/$id/json')?)? head, body := d.read_response()? @@ -114,7 +114,7 @@ pub fn (mut d DockerDaemon) inspect_container(id string) ?ContainerInspect { } // remove_container removes the container with the given id. -pub fn (mut d DockerDaemon) remove_container(id string) ? { +pub fn (mut d DockerConn) remove_container(id string) ? { d.send_request('DELETE', urllib.parse('/v1.41/containers/$id')?)? head, body := d.read_response()? @@ -127,7 +127,7 @@ pub fn (mut d DockerDaemon) remove_container(id string) ? { // get_container_logs returns a reader object allowing access to the // container's logs. -pub fn (mut d DockerDaemon) get_container_logs(id string) ?&StreamFormatReader { +pub fn (mut d DockerConn) get_container_logs(id string) ?&StreamFormatReader { d.send_request('GET', urllib.parse('/v1.41/containers/$id/logs?stdout=true&stderr=true')?)? head := d.read_response_head()? diff --git a/src/docker/docker.v b/src/docker/docker.v index f612d1f1..4ce1ea6a 100644 --- a/src/docker/docker.v +++ b/src/docker/docker.v @@ -15,17 +15,17 @@ const ( http_chunk_separator = [u8(`\r`), `\n`] ) -pub struct DockerDaemon { +pub struct DockerConn { mut: socket &unix.StreamConn reader &io.BufferedReader } // new_conn creates a new connection to the Docker daemon. -pub fn new_conn() ?&DockerDaemon { +pub fn new_conn() ?&DockerConn { s := unix.connect_stream(docker.socket)? - d := &DockerDaemon{ + d := &DockerConn{ socket: s reader: io.new_buffered_reader(reader: s) } @@ -34,12 +34,12 @@ pub fn new_conn() ?&DockerDaemon { } // close closes the underlying socket connection. -pub fn (mut d DockerDaemon) close() ? { +pub fn (mut d DockerConn) close() ? { d.socket.close()? } // send_request sends an HTTP request without body. -pub fn (mut d DockerDaemon) send_request(method string, url urllib.URL) ? { +pub fn (mut d DockerConn) send_request(method string, url urllib.URL) ? { req := '$method $url.request_uri() HTTP/1.1\nHost: localhost\n\n' d.socket.write_string(req)? @@ -49,7 +49,7 @@ pub fn (mut d DockerDaemon) send_request(method string, url urllib.URL) ? { } // send_request_with_body sends an HTTP request with the given body. -pub fn (mut d DockerDaemon) send_request_with_body(method string, url urllib.URL, content_type string, body string) ? { +pub fn (mut d DockerConn) send_request_with_body(method string, url urllib.URL, content_type string, body string) ? { req := '$method $url.request_uri() HTTP/1.1\nHost: localhost\nContent-Type: $content_type\nContent-Length: $body.len\n\n$body\n\n' d.socket.write_string(req)? @@ -60,7 +60,7 @@ pub fn (mut d DockerDaemon) send_request_with_body(method string, url urllib.URL // send_request_with_json is a convenience wrapper around // send_request_with_body that encodes the input as JSON. -pub fn (mut d DockerDaemon) send_request_with_json(method string, url urllib.URL, data &T) ? { +pub fn (mut d DockerConn) send_request_with_json(method string, url urllib.URL, data &T) ? { body := json.encode(data) return d.send_request_with_body(method, url, 'application/json', body) @@ -70,17 +70,17 @@ pub fn (mut d DockerDaemon) send_request_with_json(method string, url urllib. // '\r\n\r\n', after which it parses the response as an HTTP response. // Importantly, this function never consumes the reader past the HTTP // separator, so the body can be read fully later on. -pub fn (mut d DockerDaemon) read_response_head() ?http.Response { +pub fn (mut d DockerConn) read_response_head() ?http.Response { mut res := []u8{} - util.read_until_separator(mut d.reader, mut res, http_separator) ? + util.read_until_separator(mut d.reader, mut res, docker.http_separator)? return http.parse_response(res.bytestr()) } // read_response_body reads `length` bytes from the stream. It can be used when // the response encoding isn't chunked to fully read it. -pub fn (mut d DockerDaemon) read_response_body(length int) ?string { +pub fn (mut d DockerConn) read_response_body(length int) ?string { if length == 0 { return '' } @@ -101,7 +101,7 @@ pub fn (mut d DockerDaemon) read_response_body(length int) ?string { // read_response is a convenience function which always consumes the entire // response & returns it. It should only be used when we're certain that the // result isn't too large. -pub fn (mut d DockerDaemon) read_response() ?(http.Response, string) { +pub fn (mut d DockerConn) read_response() ?(http.Response, string) { head := d.read_response_head()? if head.header.get(http.CommonHeader.transfer_encoding) or { '' } == 'chunked' { @@ -121,7 +121,7 @@ pub fn (mut d DockerDaemon) read_response() ?(http.Response, string) { // get_chunked_response_reader returns a ChunkedResponseReader using the socket // as reader. -pub fn (mut d DockerDaemon) get_chunked_response_reader() &ChunkedResponseReader { +pub fn (mut d DockerConn) get_chunked_response_reader() &ChunkedResponseReader { r := new_chunked_response_reader(d.reader) return r @@ -129,7 +129,7 @@ pub fn (mut d DockerDaemon) get_chunked_response_reader() &ChunkedResponseReader // get_stream_format_reader returns a StreamFormatReader using the socket as // reader. -pub fn (mut d DockerDaemon) get_stream_format_reader() &StreamFormatReader { +pub fn (mut d DockerConn) get_stream_format_reader() &StreamFormatReader { r := new_chunked_response_reader(d.reader) r2 := new_stream_format_reader(r) diff --git a/src/docker/images.v b/src/docker/images.v index 58669058..cab7f341 100644 --- a/src/docker/images.v +++ b/src/docker/images.v @@ -10,7 +10,7 @@ pub: } // pull_image pulls the given image:tag. -pub fn (mut d DockerDaemon) pull_image(image string, tag string) ? { +pub fn (mut d DockerConn) pull_image(image string, tag string) ? { d.send_request('POST', urllib.parse('/v1.41/images/create?fromImage=$image&tag=$tag')?)? head := d.read_response_head()? @@ -33,7 +33,7 @@ pub fn (mut d DockerDaemon) pull_image(image string, tag string) ? { } // create_image_from_container creates a new image from a container. -pub fn (mut d DockerDaemon) create_image_from_container(id string, repo string, tag string) ?Image { +pub fn (mut d DockerConn) create_image_from_container(id string, repo string, tag string) ?Image { d.send_request('POST', urllib.parse('/v1.41/commit?container=$id&repo=$repo&tag=$tag')?)? head, body := d.read_response()? @@ -49,7 +49,7 @@ pub fn (mut d DockerDaemon) create_image_from_container(id string, repo string, } // remove_image removes the image with the given id. -pub fn (mut d DockerDaemon) remove_image(id string) ? { +pub fn (mut d DockerConn) remove_image(id string) ? { d.send_request('DELETE', urllib.parse('/v1.41/images/$id')?)? head, body := d.read_response()? diff --git a/src/docker/stream.v b/src/docker/stream.v index ed73098f..dff17846 100644 --- a/src/docker/stream.v +++ b/src/docker/stream.v @@ -68,7 +68,7 @@ fn (mut r ChunkedResponseReader) read_chunk_size() ?u64 { r.started = true mut res := []u8{} - util.read_until_separator(mut r.reader, mut res, http_chunk_separator) ? + util.read_until_separator(mut r.reader, mut res, http_chunk_separator)? // The length of the next chunk is provided as a hexadecimal mut num_data := hex.decode(res#[..-2].bytestr())? From 055b168ff12d1db28ad0dc918a3790cd523597c5 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 16 May 2022 14:22:53 +0200 Subject: [PATCH 019/236] refactor(util): split into two files --- src/build/build.v | 7 ++-- src/docker/README.md | 2 - src/docker/stream.v | 4 +- src/env/env.v | 13 +++--- src/util/README.md | 2 + src/util/stream.v | 95 ++++++++++++++++++++++++++++++++++++++++++ src/util/util.v | 98 ++------------------------------------------ 7 files changed, 114 insertions(+), 107 deletions(-) create mode 100644 src/util/README.md create mode 100644 src/util/stream.v diff --git a/src/build/build.v b/src/build/build.v index 0de91a64..2784c260 100644 --- a/src/build/build.v +++ b/src/build/build.v @@ -9,9 +9,10 @@ import client import strings import util -const container_build_dir = '/build' - -const build_image_repo = 'vieter-build' +const ( + container_build_dir = '/build' + build_image_repo = 'vieter-build' +) // create_build_image creates a builder image given some base image which can // then be used to build & package Arch images. It mostly just updates the diff --git a/src/docker/README.md b/src/docker/README.md index 5236277e..4cc8971a 100644 --- a/src/docker/README.md +++ b/src/docker/README.md @@ -1,5 +1,3 @@ -# docker - This module implements part of the Docker Engine API v1.41 ([documentation](https://docs.docker.com/engine/api/v1.41/)) using socket-based HTTP communication. diff --git a/src/docker/stream.v b/src/docker/stream.v index dff17846..02fb9720 100644 --- a/src/docker/stream.v +++ b/src/docker/stream.v @@ -58,9 +58,9 @@ pub fn (mut r ChunkedResponseReader) read(mut buf []u8) ?int { // This function should only be called if the previous chunk has been // completely consumed. fn (mut r ChunkedResponseReader) read_chunk_size() ?u64 { - mut buf := []u8{len: 2} - if r.started { + mut buf := []u8{len: 2} + // Each chunk ends with a `\r\n` which we want to skip first r.reader.read(mut buf)? } diff --git a/src/env/env.v b/src/env/env.v index d1459311..3d07d235 100644 --- a/src/env/env.v +++ b/src/env/env.v @@ -3,12 +3,13 @@ module env import os import toml -// The prefix that every environment variable should have -const prefix = 'VIETER_' - -// The suffix an environment variable in order for it to be loaded from a file -// instead -const file_suffix = '_FILE' +const ( + // The prefix that every environment variable should have + prefix = 'VIETER_' + // The suffix an environment variable in order for it to be loaded from a file + // instead + file_suffix = '_FILE' +) fn get_env_var(field_name string) ?string { env_var_name := '$env.prefix$field_name.to_upper()' diff --git a/src/util/README.md b/src/util/README.md new file mode 100644 index 00000000..529e412e --- /dev/null +++ b/src/util/README.md @@ -0,0 +1,2 @@ +This module defines a few useful functions used throughout the codebase that +don't specifically fit inside a module. diff --git a/src/util/stream.v b/src/util/stream.v new file mode 100644 index 00000000..06397aa8 --- /dev/null +++ b/src/util/stream.v @@ -0,0 +1,95 @@ +// Functions for interacting with `io.Reader` & `io.Writer` objects. +module util + +import io +import os + +// reader_to_writer tries to consume the entire reader & write it to the writer. +pub fn reader_to_writer(mut reader io.Reader, mut writer io.Writer) ? { + mut buf := []u8{len: 10 * 1024} + + for { + bytes_read := reader.read(mut buf) or { break } + mut bytes_written := 0 + + for bytes_written < bytes_read { + c := writer.write(buf[bytes_written..bytes_read]) or { break } + + bytes_written += c + } + } +} + +// reader_to_file writes the contents of a BufferedReader to a file +pub fn reader_to_file(mut reader io.BufferedReader, length int, path string) ? { + mut file := os.create(path)? + defer { + file.close() + } + + mut buf := []u8{len: 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 } + // file.flush() + + to_write = to_write - bytes_written + } + } +} + +// match_array_in_array returns how many elements of a2 overlap with a1. For +// example, if a1 = "abcd" & a2 = "cd", the result will be 2. If the match is +// not at the end of a1, the result is 0. +pub fn match_array_in_array(a1 []T, a2 []T) int { + mut i := 0 + mut match_len := 0 + + for i + match_len < a1.len { + if a1[i + match_len] == a2[match_len] { + match_len += 1 + } else { + i += match_len + 1 + match_len = 0 + } + } + + return match_len +} + +// read_until_separator consumes an io.Reader until it encounters some +// separator array. The data read is stored inside the provided res array. +pub fn read_until_separator(mut reader io.Reader, mut res []u8, sep []u8) ? { + mut buf := []u8{len: sep.len} + + for { + c := reader.read(mut buf)? + res << buf[..c] + + match_len := match_array_in_array(buf[..c], sep) + + if match_len == sep.len { + break + } + + if match_len > 0 { + match_left := sep.len - match_len + c2 := reader.read(mut buf[..match_left])? + res << buf[..c2] + + if buf[..c2] == sep[match_len..] { + break + } + } + } +} diff --git a/src/util/util.v b/src/util/util.v index 9cf3011c..266bcb5b 100644 --- a/src/util/util.v +++ b/src/util/util.v @@ -1,13 +1,13 @@ 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'] +const ( + reader_buf_size = 1_000_000 + prefixes = ['B', 'KB', 'MB', 'GB'] +) // Dummy struct to work around the fact that you can only share structs, maps & // arrays @@ -23,50 +23,6 @@ pub fn exit_with_message(code int, msg string) { exit(code) } -// reader_to_writer tries to consume the entire reader & write it to the writer. -pub fn reader_to_writer(mut reader io.Reader, mut writer io.Writer) ? { - mut buf := []u8{len: 10 * 1024} - - for { - bytes_read := reader.read(mut buf) or { break } - mut bytes_written := 0 - - for bytes_written < bytes_read { - c := writer.write(buf[bytes_written..bytes_read]) or { break } - - bytes_written += c - } - } -} - -// reader_to_file writes the contents of a BufferedReader to a file -pub fn reader_to_file(mut reader io.BufferedReader, length int, path string) ? { - mut file := os.create(path)? - defer { - file.close() - } - - mut buf := []u8{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 } - // file.flush() - - 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) { @@ -108,49 +64,3 @@ pub fn pretty_bytes(bytes int) string { return '${n:.2}${util.prefixes[i]}' } - -// match_array_in_array returns how many elements of a2 overlap with a1. For -// example, if a1 = "abcd" & a2 = "cd", the result will be 2. If the match is -// not at the end of a1, the result is 0. -pub fn match_array_in_array(a1 []T, a2 []T) int { - mut i := 0 - mut match_len := 0 - - for i + match_len < a1.len { - if a1[i + match_len] == a2[match_len] { - match_len += 1 - } else { - i += match_len + 1 - match_len = 0 - } - } - - return match_len -} - -// read_until_separator consumes an io.Reader until it encounters some -// separator array. The data read is stored inside the provided res array. -pub fn read_until_separator(mut reader io.Reader, mut res []u8, sep []u8) ? { - mut buf := []u8{len: sep.len} - - for { - c := reader.read(mut buf)? - res << buf[..c] - - match_len := match_array_in_array(buf[..c], sep) - - if match_len == sep.len { - break - } - - if match_len > 0 { - match_left := sep.len - match_len - c2 := reader.read(mut buf[..match_left])? - res << buf[..c2] - - if buf[..c2] == sep[match_len..] { - break - } - } - } -} From d4c803c41c7d1322cd3a3e7975bc10c6adb69a59 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 16 May 2022 14:53:48 +0200 Subject: [PATCH 020/236] doc(env): added missing docstring & README --- src/env/README.md | 7 +++++++ src/env/env.v | 5 +++++ 2 files changed, 12 insertions(+) create mode 100644 src/env/README.md diff --git a/src/env/README.md b/src/env/README.md new file mode 100644 index 00000000..135e8fa9 --- /dev/null +++ b/src/env/README.md @@ -0,0 +1,7 @@ +This module provides a framework for parsing a configuration, defined as a +struct, from both a TOML configuration file & environment variables. Some +notable features are: + +* Overwrite values in config file using environment variables +* Allow default values in config struct +* Read environment variable value from file diff --git a/src/env/env.v b/src/env/env.v index 3d07d235..5ed19552 100644 --- a/src/env/env.v +++ b/src/env/env.v @@ -11,6 +11,11 @@ const ( file_suffix = '_FILE' ) +// get_env_var tries to read the contents of the given environment variable. It +// looks for either `${env.prefix}${field_name.to_upper()}` or +// `${env.prefix}${field_name.to_upper()}${env.file_suffix}`, returning the +// contents of the file instead if the latter. If both or neither exist, the +// function returns an error. fn get_env_var(field_name string) ?string { env_var_name := '$env.prefix$field_name.to_upper()' env_file_name := '$env.prefix$field_name.to_upper()$env.file_suffix' From 850cba6ab9b81b8bf2c28a9348c5ba7ed2f9181f Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 16 May 2022 15:02:57 +0200 Subject: [PATCH 021/236] refactor(docker): use http.Method instead of strings --- src/docker/containers.v | 14 +++++++------- src/docker/docker.v | 6 +++--- src/docker/images.v | 8 ++++---- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/docker/containers.v b/src/docker/containers.v index 62c8031c..a790243d 100644 --- a/src/docker/containers.v +++ b/src/docker/containers.v @@ -3,7 +3,7 @@ module docker import json import net.urllib import time -import net.http +import net.http { Method } struct DockerError { message string @@ -16,7 +16,7 @@ struct Container { // containers returns a list of all containers. pub fn (mut d DockerConn) containers() ?[]Container { - d.send_request('GET', urllib.parse('/v1.41/containers/json')?)? + d.send_request(Method.get, urllib.parse('/v1.41/containers/json')?)? head, res := d.read_response()? if head.status_code != 200 { @@ -47,7 +47,7 @@ pub: // create_container creates a new container with the given config. pub fn (mut d DockerConn) create_container(c NewContainer) ?CreatedContainer { - d.send_request_with_json('POST', urllib.parse('/v1.41/containers/create')?, c)? + d.send_request_with_json(Method.post, urllib.parse('/v1.41/containers/create')?, c)? head, res := d.read_response()? if head.status_code != 201 { @@ -63,7 +63,7 @@ pub fn (mut d DockerConn) create_container(c NewContainer) ?CreatedContainer { // start_container starts the container with the given id. pub fn (mut d DockerConn) start_container(id string) ? { - d.send_request('POST', urllib.parse('/v1.41/containers/$id/start')?)? + d.send_request(Method.post, urllib.parse('/v1.41/containers/$id/start')?)? head, body := d.read_response()? if head.status_code != 204 { @@ -93,7 +93,7 @@ pub mut: // inspect_container returns detailed information for a given container. pub fn (mut d DockerConn) inspect_container(id string) ?ContainerInspect { - d.send_request('GET', urllib.parse('/v1.41/containers/$id/json')?)? + d.send_request(Method.get, urllib.parse('/v1.41/containers/$id/json')?)? head, body := d.read_response()? if head.status_code != 200 { @@ -115,7 +115,7 @@ pub fn (mut d DockerConn) inspect_container(id string) ?ContainerInspect { // remove_container removes the container with the given id. pub fn (mut d DockerConn) remove_container(id string) ? { - d.send_request('DELETE', urllib.parse('/v1.41/containers/$id')?)? + d.send_request(Method.delete, urllib.parse('/v1.41/containers/$id')?)? head, body := d.read_response()? if head.status_code != 204 { @@ -128,7 +128,7 @@ pub fn (mut d DockerConn) remove_container(id string) ? { // get_container_logs returns a reader object allowing access to the // container's logs. pub fn (mut d DockerConn) get_container_logs(id string) ?&StreamFormatReader { - d.send_request('GET', urllib.parse('/v1.41/containers/$id/logs?stdout=true&stderr=true')?)? + d.send_request(Method.get, urllib.parse('/v1.41/containers/$id/logs?stdout=true&stderr=true')?)? head := d.read_response_head()? if head.status_code != 200 { diff --git a/src/docker/docker.v b/src/docker/docker.v index 4ce1ea6a..ccc6bedd 100644 --- a/src/docker/docker.v +++ b/src/docker/docker.v @@ -39,7 +39,7 @@ pub fn (mut d DockerConn) close() ? { } // send_request sends an HTTP request without body. -pub fn (mut d DockerConn) send_request(method string, url urllib.URL) ? { +pub fn (mut d DockerConn) send_request(method http.Method, url urllib.URL) ? { req := '$method $url.request_uri() HTTP/1.1\nHost: localhost\n\n' d.socket.write_string(req)? @@ -49,7 +49,7 @@ pub fn (mut d DockerConn) send_request(method string, url urllib.URL) ? { } // send_request_with_body sends an HTTP request with the given body. -pub fn (mut d DockerConn) send_request_with_body(method string, url urllib.URL, content_type string, body string) ? { +pub fn (mut d DockerConn) send_request_with_body(method http.Method, url urllib.URL, content_type string, body string) ? { req := '$method $url.request_uri() HTTP/1.1\nHost: localhost\nContent-Type: $content_type\nContent-Length: $body.len\n\n$body\n\n' d.socket.write_string(req)? @@ -60,7 +60,7 @@ pub fn (mut d DockerConn) send_request_with_body(method string, url urllib.URL, // send_request_with_json is a convenience wrapper around // send_request_with_body that encodes the input as JSON. -pub fn (mut d DockerConn) send_request_with_json(method string, url urllib.URL, data &T) ? { +pub fn (mut d DockerConn) send_request_with_json(method http.Method, url urllib.URL, data &T) ? { body := json.encode(data) return d.send_request_with_body(method, url, 'application/json', body) diff --git a/src/docker/images.v b/src/docker/images.v index cab7f341..6161565a 100644 --- a/src/docker/images.v +++ b/src/docker/images.v @@ -1,6 +1,6 @@ module docker -import net.http +import net.http { Method } import net.urllib import json @@ -11,7 +11,7 @@ pub: // pull_image pulls the given image:tag. pub fn (mut d DockerConn) pull_image(image string, tag string) ? { - d.send_request('POST', urllib.parse('/v1.41/images/create?fromImage=$image&tag=$tag')?)? + d.send_request(Method.post, urllib.parse('/v1.41/images/create?fromImage=$image&tag=$tag')?)? head := d.read_response_head()? if head.status_code != 200 { @@ -34,7 +34,7 @@ pub fn (mut d DockerConn) pull_image(image string, tag string) ? { // create_image_from_container creates a new image from a container. pub fn (mut d DockerConn) create_image_from_container(id string, repo string, tag string) ?Image { - d.send_request('POST', urllib.parse('/v1.41/commit?container=$id&repo=$repo&tag=$tag')?)? + d.send_request(Method.post, urllib.parse('/v1.41/commit?container=$id&repo=$repo&tag=$tag')?)? head, body := d.read_response()? if head.status_code != 201 { @@ -50,7 +50,7 @@ pub fn (mut d DockerConn) create_image_from_container(id string, repo string, ta // remove_image removes the image with the given id. pub fn (mut d DockerConn) remove_image(id string) ? { - d.send_request('DELETE', urllib.parse('/v1.41/images/$id')?)? + d.send_request(Method.delete, urllib.parse('/v1.41/images/$id')?)? head, body := d.read_response()? if head.status_code != 200 { From 3c87e60293810b37a1c574976261b1d7fc5a9df0 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 16 May 2022 15:36:21 +0200 Subject: [PATCH 022/236] refactor(docker): more tightly integrate streams --- src/docker/stream.v | 34 +++++++++++----------------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/src/docker/stream.v b/src/docker/stream.v index 02fb9720..001f4b3e 100644 --- a/src/docker/stream.v +++ b/src/docker/stream.v @@ -9,15 +9,14 @@ import encoding.hex // it as if it was a continuous stream of data. struct ChunkedResponseReader { mut: - reader io.Reader + reader io.BufferedReader bytes_left_in_chunk u64 - end_of_stream bool started bool } // new_chunked_response_reader creates a new ChunkedResponseReader on the heap // with the provided reader. -pub fn new_chunked_response_reader(reader io.Reader) &ChunkedResponseReader { +pub fn new_chunked_response_reader(reader io.BufferedReader) &ChunkedResponseReader { r := &ChunkedResponseReader{ reader: reader } @@ -27,16 +26,10 @@ pub fn new_chunked_response_reader(reader io.Reader) &ChunkedResponseReader { // read satisfies the io.Reader interface. pub fn (mut r ChunkedResponseReader) read(mut buf []u8) ?int { - if r.end_of_stream { - return none - } - if r.bytes_left_in_chunk == 0 { + // An io.BufferedReader always returns none if its stream has + // ended. r.bytes_left_in_chunk = r.read_chunk_size()? - - if r.end_of_stream { - return none - } } mut c := 0 @@ -82,7 +75,7 @@ fn (mut r ChunkedResponseReader) read_chunk_size() ?u64 { // This only occurs for the very last chunk, which always reports a size of // 0. if num == 0 { - r.end_of_stream = true + return none } return num @@ -92,14 +85,13 @@ fn (mut r ChunkedResponseReader) read_chunk_size() ?u64 { // header bytes. struct StreamFormatReader { mut: - reader io.Reader + reader ChunkedResponseReader bytes_left_in_chunk u32 - end_of_stream bool } // new_stream_format_reader creates a new StreamFormatReader using the given // reader. -pub fn new_stream_format_reader(reader io.Reader) &StreamFormatReader { +pub fn new_stream_format_reader(reader ChunkedResponseReader) &StreamFormatReader { r := &StreamFormatReader{ reader: reader } @@ -109,16 +101,8 @@ pub fn new_stream_format_reader(reader io.Reader) &StreamFormatReader { // read satisfies the io.Reader interface. pub fn (mut r StreamFormatReader) read(mut buf []u8) ?int { - if r.end_of_stream { - return none - } - if r.bytes_left_in_chunk == 0 { r.bytes_left_in_chunk = r.read_chunk_size()? - - if r.end_of_stream { - return none - } } mut c := 0 @@ -143,5 +127,9 @@ fn (mut r StreamFormatReader) read_chunk_size() ?u32 { num := binary.big_endian_u32(buf[4..]) + if num == 0 { + return none + } + return num } From 889d5a08849a6ab08b41e6ff94b78f7a9624c460 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 16 May 2022 15:39:23 +0200 Subject: [PATCH 023/236] refactor(docker): removed unused function --- src/docker/containers.v | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/src/docker/containers.v b/src/docker/containers.v index a790243d..0bc59bb3 100644 --- a/src/docker/containers.v +++ b/src/docker/containers.v @@ -9,27 +9,6 @@ struct DockerError { message string } -struct Container { - id string [json: Id] - names []string [json: Names] -} - -// containers returns a list of all containers. -pub fn (mut d DockerConn) containers() ?[]Container { - d.send_request(Method.get, urllib.parse('/v1.41/containers/json')?)? - head, res := d.read_response()? - - if head.status_code != 200 { - data := json.decode(DockerError, res)? - - return error(data.message) - } - - data := json.decode([]Container, res)? - - return data -} - pub struct NewContainer { image string [json: Image] entrypoint []string [json: Entrypoint] From 73d2d4b08f8af1b7c315520ced96256617b8852f Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 16 May 2022 17:12:37 +0200 Subject: [PATCH 024/236] feat(console): replaced `vieter build` with `vieter repos build` --- CHANGELOG.md | 7 +++++++ src/build/build.v | 29 ----------------------------- src/build/cli.v | 29 ----------------------------- src/console/git/build.v | 34 ++++++++++++++++++++++++++++++++++ src/console/git/git.v | 17 +++++++++++++++-- src/main.v | 2 -- 6 files changed, 56 insertions(+), 62 deletions(-) delete mode 100644 src/build/cli.v create mode 100644 src/console/git/build.v diff --git a/CHANGELOG.md b/CHANGELOG.md index c86761c4..7c203938 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * CLI commands to access build logs API * Cron build logs are uploaded to above API * Proper ASCII table output in CLI +* `vieter repos build id` command to run builds locally + +### Removed + +* `vieter build` command + * This command was used alongside cron for periodic builds, but this has + been replaced by `vieter cron` ### Changed diff --git a/src/build/build.v b/src/build/build.v index 2784c260..fab6c35b 100644 --- a/src/build/build.v +++ b/src/build/build.v @@ -5,7 +5,6 @@ import encoding.base64 import time import os import db -import client import strings import util @@ -155,31 +154,3 @@ pub fn build_repo(address string, api_key string, base_image_id string, repo &db logs: logs_builder.str() } } - -// build builds every Git repo in the server's list. -fn build(conf Config, repo_id int) ? { - c := client.new(conf.address, conf.api_key) - repo := c.get_git_repo(repo_id)? - - build_arch := os.uname().machine - - println('Creating base image...') - image_id := create_build_image(conf.base_image)? - - println('Running build...') - res := build_repo(conf.address, conf.api_key, image_id, repo)? - - println('Removing build image...') - - mut dd := docker.new_conn()? - - defer { - dd.close() or {} - } - - dd.remove_image(image_id)? - - println('Uploading logs to Vieter...') - c.add_build_log(repo.id, res.start_time, res.end_time, build_arch, res.exit_code, - res.logs)? -} diff --git a/src/build/cli.v b/src/build/cli.v deleted file mode 100644 index 64814cb2..00000000 --- a/src/build/cli.v +++ /dev/null @@ -1,29 +0,0 @@ -module build - -import cli -import env - -pub struct Config { -pub: - api_key string - address string - base_image string = 'archlinux:base-devel' -} - -// cmd returns the cli submodule that handles the build process -pub fn cmd() cli.Command { - return cli.Command{ - name: 'build' - required_args: 1 - usage: 'id' - description: 'Build the repository with the given ID.' - execute: fn (cmd cli.Command) ? { - config_file := cmd.flags.get_string('config-file')? - conf := env.load(config_file)? - - id := cmd.args[0].int() - - build(conf, id)? - } - } -} diff --git a/src/console/git/build.v b/src/console/git/build.v new file mode 100644 index 00000000..fac760df --- /dev/null +++ b/src/console/git/build.v @@ -0,0 +1,34 @@ +module git + +import client +import docker +import os +import build + +// build builds every Git repo in the server's list. +fn build(conf Config, repo_id int) ? { + c := client.new(conf.address, conf.api_key) + repo := c.get_git_repo(repo_id)? + + build_arch := os.uname().machine + + println('Creating base image...') + image_id := build.create_build_image(conf.base_image)? + + println('Running build...') + res := build.build_repo(conf.address, conf.api_key, image_id, repo)? + + println('Removing build image...') + + mut dd := docker.new_conn()? + + defer { + dd.close() or {} + } + + dd.remove_image(image_id)? + + println('Uploading logs to Vieter...') + c.add_build_log(repo.id, res.start_time, res.end_time, build_arch, res.exit_code, + res.logs)? +} diff --git a/src/console/git/git.v b/src/console/git/git.v index db9dec59..06d5f80c 100644 --- a/src/console/git/git.v +++ b/src/console/git/git.v @@ -7,8 +7,9 @@ import client import console struct Config { - address string [required] - api_key string [required] + address string [required] + api_key string [required] + base_image string = 'archlinux:base-devel' } // cmd returns the cli submodule that handles the repos API interaction @@ -112,6 +113,18 @@ pub fn cmd() cli.Command { patch(conf, cmd.args[0], params)? } }, + cli.Command{ + name: 'build' + required_args: 1 + usage: 'id' + description: 'Build the repo with the given id & publish it.' + execute: fn (cmd cli.Command) ? { + config_file := cmd.flags.get_string('config-file')? + conf := env.load(config_file)? + + build(conf, cmd.args[0].int())? + } + }, ] } } diff --git a/src/main.v b/src/main.v index db6d5ef9..dbfac09e 100644 --- a/src/main.v +++ b/src/main.v @@ -3,7 +3,6 @@ module main import os import server import cli -import build import console.git import console.logs import cron @@ -25,7 +24,6 @@ fn main() { ] commands: [ server.cmd(), - build.cmd(), git.cmd(), cron.cmd(), logs.cmd(), From 67c4d199218b3403825fa956b37ecaba5065e560 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 16 May 2022 17:17:42 +0200 Subject: [PATCH 025/236] chore: removed healthcheck & unused cron stuff from Dockerfile --- Dockerfile | 9 +-------- docs/content/api.md | 5 +++++ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2ba61817..5997adca 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,15 +36,8 @@ ENV PATH=/bin \ COPY --from=builder /app/dumb-init /app/vieter /bin/ -HEALTHCHECK --interval=30s \ - --timeout=3s \ - --start-period=5s \ - CMD /bin/wget --spider http://localhost:8000/health || exit 1 - RUN mkdir /data && \ - chown -R www-data:www-data /data && \ - mkdir -p '/var/spool/cron/crontabs' && \ - echo '0 3 * * * /bin/vieter build' | crontab - + chown -R www-data:www-data /data WORKDIR /data diff --git a/docs/content/api.md b/docs/content/api.md index 7c395eb2..0fbb694f 100644 --- a/docs/content/api.md +++ b/docs/content/api.md @@ -56,6 +56,11 @@ Vieter only supports uploading archives compressed using either gzip, zstd or xz at the moment. {{< /hint >}} +### `GET /health` + +This endpoint's only use is to be used with healthchecks. It returns a JSON +response with the message "Healthy.". + ## API All API routes require the API key to provided using the `X-Api-Key` header. From 0de5ffb45d2734b95c3c11dd25d2bfdf6dd2bf0e Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 16 May 2022 17:34:51 +0200 Subject: [PATCH 026/236] chore: bumped versions --- CHANGELOG.md | 2 ++ PKGBUILD | 2 +- src/main.v | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c203938..7dc0a009 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://git.rustybever.be/vieter/vieter/src/branch/dev) +## [0.3.0-alpha.2](https://git.rustybever.be/vieter/vieter/src/tag/0.3.0-alpha.2) + ### Added * Web API for adding & querying build logs diff --git a/PKGBUILD b/PKGBUILD index 83ab8961..49fcf548 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -3,7 +3,7 @@ pkgbase='vieter' pkgname='vieter' -pkgver='0.3.0_alpha.1' +pkgver='0.3.0_alpha.2' pkgrel=1 depends=('glibc' 'openssl' 'libarchive' 'sqlite') makedepends=('git' 'vieter-v') diff --git a/src/main.v b/src/main.v index dbfac09e..6df45dc3 100644 --- a/src/main.v +++ b/src/main.v @@ -11,7 +11,7 @@ fn main() { mut app := cli.Command{ name: 'vieter' description: 'Vieter is a lightweight implementation of an Arch repository server.' - version: '0.3.0-alpha.1' + version: '0.3.0-alpha.2' flags: [ cli.Flag{ flag: cli.FlagType.string From 1e079143cd29673ff2a20e8513dfc741493f0e9b Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Tue, 17 May 2022 13:50:46 +0200 Subject: [PATCH 027/236] feat(server): added better query params to GitRepo API --- src/db/filter.v | 25 +++++++++++++++++++++++++ src/db/git.v | 14 ++++++++++++-- src/server/git.v | 5 ++++- 3 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 src/db/filter.v diff --git a/src/db/filter.v b/src/db/filter.v new file mode 100644 index 00000000..79e281b3 --- /dev/null +++ b/src/db/filter.v @@ -0,0 +1,25 @@ +module db + +pub struct GitRepoFilter { +pub mut: + limit u64 = 25 + offset u64 + repo string +} + +pub fn filter_from_params(params map[string]string) ?T { + mut o := GitRepoFilter{} + + $for field in T.fields { + if field.name in params { + val := params[field.name] + + $if field.typ is string { + o.$(field.name) = val + } $else $if field.typ is u64 { + o.$(field.name) = val.u64() + } + } + } + return o +} diff --git a/src/db/git.v b/src/db/git.v index 9a475a54..cb31bf04 100644 --- a/src/db/git.v +++ b/src/db/git.v @@ -77,9 +77,19 @@ pub fn git_repo_from_params(params map[string]string) ?GitRepo { } // get_git_repos returns all GitRepo's in the database. -pub fn (db &VieterDb) get_git_repos() []GitRepo { +pub fn (db &VieterDb) get_git_repos(filter GitRepoFilter) []GitRepo { + // This seems to currently be blocked by a bug in the ORM, I'll have to ask + // around. + if filter.repo != '' { + res := sql db.conn { + select from GitRepo where repo == filter.repo order by id limit filter.limit offset filter.offset + } + + return res + } + res := sql db.conn { - select from GitRepo order by id + select from GitRepo order by id limit filter.limit offset filter.offset } return res diff --git a/src/server/git.v b/src/server/git.v index c5cbc0a6..921251f5 100644 --- a/src/server/git.v +++ b/src/server/git.v @@ -12,7 +12,10 @@ fn (mut app App) get_repos() web.Result { return app.json(http.Status.unauthorized, new_response('Unauthorized.')) } - repos := app.db.get_git_repos() + filter := db.filter_from_params(app.query) or { + return app.json(http.Status.bad_request, new_response('Invalid query parameters.')) + } + repos := app.db.get_git_repos(filter) return app.json(http.Status.ok, new_data_response(repos)) } From 5e81dadce3bfd9a4a43c979507f0bd9be553a55c Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Wed, 18 May 2022 08:22:13 +0200 Subject: [PATCH 028/236] feat: partially added filters to GitRepo CLI --- src/client/git.v | 8 +++++--- src/console/git/git.v | 46 ++++++++++++++++++++++++++++++++++++++++--- src/db/filter.v | 1 + src/db/git.v | 1 + src/util/util.v | 10 ++++++++++ 5 files changed, 60 insertions(+), 6 deletions(-) diff --git a/src/client/git.v b/src/client/git.v index 280caabe..c21565a9 100644 --- a/src/client/git.v +++ b/src/client/git.v @@ -1,12 +1,14 @@ module client -import db { GitRepo } +import db { GitRepo, GitRepoFilter } import net.http { Method } import response { Response } +import util // get_git_repos returns the current list of repos. -pub fn (c &Client) get_git_repos() ?[]GitRepo { - data := c.send_request<[]GitRepo>(Method.get, '/api/repos', {})? +pub fn (c &Client) get_git_repos(filter GitRepoFilter) ?[]GitRepo { + params := util.struct_to_map(filter) + data := c.send_request<[]GitRepo>(Method.get, '/api/repos', params)? return data.data } diff --git a/src/console/git/git.v b/src/console/git/git.v index 06d5f80c..861bfb35 100644 --- a/src/console/git/git.v +++ b/src/console/git/git.v @@ -5,6 +5,7 @@ import env import cron.expression { parse_expression } import client import console +import db { GitRepoFilter } struct Config { address string [required] @@ -21,11 +22,50 @@ pub fn cmd() cli.Command { cli.Command{ name: 'list' description: 'List the current repos.' + flags: [ + cli.Flag{ + name: 'limit' + description: 'How many results to return.' + flag: cli.FlagType.int + }, + cli.Flag{ + name: 'offset' + description: 'Minimum index to return.' + flag: cli.FlagType.int + }, + cli.Flag{ + name: 'repo' + description: 'Only return Git repos that publish to this repo.' + flag: cli.FlagType.string + }, + cli.Flag{ + name: 'arch' + description: 'Only return repos enabled for this architecture.' + flag: cli.FlagType.string + }, + ] execute: fn (cmd cli.Command) ? { config_file := cmd.flags.get_string('config-file')? conf := env.load(config_file)? - list(conf)? + mut filter := GitRepoFilter{} + + if limit := cmd.flags.get_int('limit') { + println('limit = $limit') + filter.limit = u64(limit) + } + + if offset := cmd.flags.get_int('offset') { + filter.limit = u64(offset) + } + + if repo := cmd.flags.get_string('repo') { + filter.repo = repo + } + + dump(filter) + + list(conf, filter)? } }, cli.Command{ @@ -133,9 +173,9 @@ pub fn cmd() cli.Command { // ID. If multiple or none are found, an error is raised. // list prints out a list of all repositories. -fn list(conf Config) ? { +fn list(conf Config, filter GitRepoFilter) ? { c := client.new(conf.address, conf.api_key) - repos := c.get_git_repos()? + repos := c.get_git_repos(filter)? data := repos.map([it.id.str(), it.url, it.branch, it.repo]) println(console.pretty_table(['id', 'url', 'branch', 'repo'], data)?) diff --git a/src/db/filter.v b/src/db/filter.v index 79e281b3..07c890a3 100644 --- a/src/db/filter.v +++ b/src/db/filter.v @@ -1,5 +1,6 @@ module db +[params] pub struct GitRepoFilter { pub mut: limit u64 = 25 diff --git a/src/db/git.v b/src/db/git.v index cb31bf04..73083463 100644 --- a/src/db/git.v +++ b/src/db/git.v @@ -78,6 +78,7 @@ pub fn git_repo_from_params(params map[string]string) ?GitRepo { // get_git_repos returns all GitRepo's in the database. pub fn (db &VieterDb) get_git_repos(filter GitRepoFilter) []GitRepo { + println(filter) // This seems to currently be blocked by a bug in the ORM, I'll have to ask // around. if filter.repo != '' { diff --git a/src/util/util.v b/src/util/util.v index 266bcb5b..cbb40725 100644 --- a/src/util/util.v +++ b/src/util/util.v @@ -64,3 +64,13 @@ pub fn pretty_bytes(bytes int) string { return '${n:.2}${util.prefixes[i]}' } + +pub fn struct_to_map(o T) map[string]string { + mut m := map[string]string{} + + $for field in T.fields { + m[field.name] = o.$(field.name).str() + } + + return m +} From 6bd5b7cb48eb38ebccda393a886badd36e4c4346 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Thu, 19 May 2022 07:54:33 +0200 Subject: [PATCH 029/236] refactor: separated GitRepo types into own module feat: added more query params for GitRepo API --- src/build/build.v | 3 +- src/client/client.v | 5 ++- src/client/git.v | 5 ++- src/console/git/git.v | 16 ++++----- src/cron/daemon/daemon.v | 7 ++-- src/db/db.v | 1 + src/db/filter.v | 26 -------------- src/db/git.v | 77 +--------------------------------------- src/models/git.v | 52 +++++++++++++++++++++++++++ src/models/models.v | 36 +++++++++++++++++++ src/server/git.v | 7 ++-- src/util/util.v | 1 - 12 files changed, 114 insertions(+), 122 deletions(-) delete mode 100644 src/db/filter.v create mode 100644 src/models/git.v create mode 100644 src/models/models.v diff --git a/src/build/build.v b/src/build/build.v index fab6c35b..1f26d03e 100644 --- a/src/build/build.v +++ b/src/build/build.v @@ -7,6 +7,7 @@ import os import db import strings import util +import models { GitRepo } const ( container_build_dir = '/build' @@ -93,7 +94,7 @@ pub: // build_repo builds, packages & publishes a given Arch package based on the // provided GitRepo. The base image ID should be of an image previously created // by create_build_image. It returns the logs of the container. -pub fn build_repo(address string, api_key string, base_image_id string, repo &db.GitRepo) ?BuildResult { +pub fn build_repo(address string, api_key string, base_image_id string, repo &GitRepo) ?BuildResult { mut dd := docker.new_conn()? defer { diff --git a/src/client/client.v b/src/client/client.v index 3b280736..7cb6be5d 100644 --- a/src/client/client.v +++ b/src/client/client.v @@ -29,7 +29,10 @@ fn (c &Client) send_request_raw(method Method, url string, params map[string]str // Escape each query param for k, v in params { - params_escaped[k] = urllib.query_escape(v) + // An empty parameter should be the same as not providing it at all + if v != '' { + params_escaped[k] = urllib.query_escape(v) + } } params_str := params_escaped.keys().map('$it=${params[it]}').join('&') diff --git a/src/client/git.v b/src/client/git.v index c21565a9..fd14718d 100644 --- a/src/client/git.v +++ b/src/client/git.v @@ -1,13 +1,12 @@ module client -import db { GitRepo, GitRepoFilter } +import models { GitRepo, GitRepoFilter } import net.http { Method } import response { Response } -import util // get_git_repos returns the current list of repos. pub fn (c &Client) get_git_repos(filter GitRepoFilter) ?[]GitRepo { - params := util.struct_to_map(filter) + params := models.params_from(filter) data := c.send_request<[]GitRepo>(Method.get, '/api/repos', params)? return data.data diff --git a/src/console/git/git.v b/src/console/git/git.v index 861bfb35..b01f3dd7 100644 --- a/src/console/git/git.v +++ b/src/console/git/git.v @@ -5,7 +5,7 @@ import env import cron.expression { parse_expression } import client import console -import db { GitRepoFilter } +import models { GitRepoFilter } struct Config { address string [required] @@ -50,21 +50,21 @@ pub fn cmd() cli.Command { mut filter := GitRepoFilter{} - if limit := cmd.flags.get_int('limit') { - println('limit = $limit') + limit := cmd.flags.get_int('limit')? + if limit != 0 { filter.limit = u64(limit) } - if offset := cmd.flags.get_int('offset') { - filter.limit = u64(offset) + offset := cmd.flags.get_int('offset')? + if offset != 0 { + filter.offset = u64(offset) } - if repo := cmd.flags.get_string('repo') { + repo := cmd.flags.get_string('repo')? + if repo != '' { filter.repo = repo } - dump(filter) - list(conf, filter)? } }, diff --git a/src/cron/daemon/daemon.v b/src/cron/daemon/daemon.v index da3b46e2..e92dd68d 100644 --- a/src/cron/daemon/daemon.v +++ b/src/cron/daemon/daemon.v @@ -10,6 +10,7 @@ import docker import db import os import client +import models { GitRepo } const ( // How many seconds to wait before retrying to update API if failed @@ -20,7 +21,7 @@ const ( struct ScheduledBuild { pub: - repo db.GitRepo + repo GitRepo timestamp time.Time } @@ -38,7 +39,7 @@ mut: api_update_frequency int image_rebuild_frequency int // Repos currently loaded from API. - repos []db.GitRepo + repos []GitRepo // At what point to update the list of repositories. api_update_timestamp time.Time image_build_timestamp time.Time @@ -149,7 +150,7 @@ pub fn (mut d Daemon) run() { } // schedule_build adds the next occurence of the given repo build to the queue. -fn (mut d Daemon) schedule_build(repo db.GitRepo) { +fn (mut d Daemon) schedule_build(repo GitRepo) { ce := if repo.schedule != '' { parse_expression(repo.schedule) or { // TODO This shouldn't return an error if the expression is empty. diff --git a/src/db/db.v b/src/db/db.v index 7c1acf1e..a5756e6c 100644 --- a/src/db/db.v +++ b/src/db/db.v @@ -1,6 +1,7 @@ module db import sqlite +import models struct VieterDb { conn sqlite.DB diff --git a/src/db/filter.v b/src/db/filter.v deleted file mode 100644 index 07c890a3..00000000 --- a/src/db/filter.v +++ /dev/null @@ -1,26 +0,0 @@ -module db - -[params] -pub struct GitRepoFilter { -pub mut: - limit u64 = 25 - offset u64 - repo string -} - -pub fn filter_from_params(params map[string]string) ?T { - mut o := GitRepoFilter{} - - $for field in T.fields { - if field.name in params { - val := params[field.name] - - $if field.typ is string { - o.$(field.name) = val - } $else $if field.typ is u64 { - o.$(field.name) = val.u64() - } - } - } - return o -} diff --git a/src/db/git.v b/src/db/git.v index 73083463..d8887ffe 100644 --- a/src/db/git.v +++ b/src/db/git.v @@ -1,84 +1,9 @@ module db -pub struct GitRepoArch { -pub: - id int [primary; sql: serial] - repo_id int [nonull] - value string [nonull] -} - -// str returns a string representation. -pub fn (gra &GitRepoArch) str() string { - return gra.value -} - -pub struct GitRepo { -pub mut: - id int [optional; primary; sql: serial] - // URL of the Git repository - url string [nonull] - // Branch of the Git repository to use - branch string [nonull] - // Which repo the builder should publish packages to - repo string [nonull] - // Cron schedule describing how frequently to build the repo. - schedule string [optional] - // On which architectures the package is allowed to be built. In reality, - // this controls which builders will periodically build the image. - arch []GitRepoArch [fkey: 'repo_id'] -} - -// str returns a string representation. -pub fn (gr &GitRepo) str() string { - mut parts := [ - 'id: $gr.id', - 'url: $gr.url', - 'branch: $gr.branch', - 'repo: $gr.repo', - 'schedule: $gr.schedule', - 'arch: ${gr.arch.map(it.value).join(', ')}', - ] - str := parts.join('\n') - - return str -} - -// 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 []GitRepoArch { - r.$(field.name) = params[field.name].split(',').map(GitRepoArch{ value: it }) - } - } - } -} - -// git_repo_from_params creates a GitRepo from a map[string]string, usually -// provided from a web.App's params -pub fn git_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 && !field.attrs.contains('optional') { - return error('Missing parameter: ${field.name}.') - } - } - repo.patch_from_params(params) - - return repo -} +import models { GitRepo, GitRepoArch, GitRepoFilter } // get_git_repos returns all GitRepo's in the database. pub fn (db &VieterDb) get_git_repos(filter GitRepoFilter) []GitRepo { - println(filter) // This seems to currently be blocked by a bug in the ORM, I'll have to ask // around. if filter.repo != '' { diff --git a/src/models/git.v b/src/models/git.v new file mode 100644 index 00000000..5dcc13a7 --- /dev/null +++ b/src/models/git.v @@ -0,0 +1,52 @@ +module models + +pub struct GitRepoArch { +pub: + id int [primary; sql: serial] + repo_id int [nonull] + value string [nonull] +} + +// str returns a string representation. +pub fn (gra &GitRepoArch) str() string { + return gra.value +} + +pub struct GitRepo { +pub mut: + id int [primary; sql: serial] + // URL of the Git repository + url string [nonull] + // Branch of the Git repository to use + branch string [nonull] + // Which repo the builder should publish packages to + repo string [nonull] + // Cron schedule describing how frequently to build the repo. + schedule string + // On which architectures the package is allowed to be built. In reality, + // this controls which builders will periodically build the image. + arch []GitRepoArch [fkey: 'repo_id'] +} + +// str returns a string representation. +pub fn (gr &GitRepo) str() string { + mut parts := [ + 'id: $gr.id', + 'url: $gr.url', + 'branch: $gr.branch', + 'repo: $gr.repo', + 'schedule: $gr.schedule', + 'arch: ${gr.arch.map(it.value).join(', ')}', + ] + str := parts.join('\n') + + return str +} + +[params] +pub struct GitRepoFilter { +pub mut: + limit u64 = 25 + offset u64 + repo string +} diff --git a/src/models/models.v b/src/models/models.v new file mode 100644 index 00000000..a32ae394 --- /dev/null +++ b/src/models/models.v @@ -0,0 +1,36 @@ +module models + +pub fn from_params(params map[string]string) ?T { + mut o := T{} + + patch_from_params(mut o, params)? + + return o +} + +pub fn patch_from_params(mut o T, params map[string]string) ? { + $for field in T.fields { + if field.name in params && params[field.name] != '' { + $if field.typ is string { + o.$(field.name) = params[field.name] + } $else $if field.typ is int { + o.$(field.name) = params[field.name].int() + } $else $if field.typ is u64 { + o.$(field.name) = params[field.name].u64() + } $else $if field.typ is []GitRepoArch { + o.$(field.name) = params[field.name].split(',').map(GitRepoArch{ value: it }) + } + } else if field.attrs.contains('nonull') { + return error('Missing parameter: ${field.name}.') + } + } +} + +pub fn params_from(o &T) map[string]string { + mut out := map[string]string{} + + $for field in T.fields { + out[field.name] = o.$(field.name).str() + } + return out +} diff --git a/src/server/git.v b/src/server/git.v index 921251f5..ec433c76 100644 --- a/src/server/git.v +++ b/src/server/git.v @@ -4,6 +4,7 @@ import web import net.http import response { new_data_response, new_response } import db +import models { GitRepoArch } // get_repos returns the current list of repos. ['/api/repos'; get] @@ -12,7 +13,7 @@ fn (mut app App) get_repos() web.Result { return app.json(http.Status.unauthorized, new_response('Unauthorized.')) } - filter := db.filter_from_params(app.query) or { + filter := models.from_params(app.query) or { return app.json(http.Status.bad_request, new_response('Invalid query parameters.')) } repos := app.db.get_git_repos(filter) @@ -47,7 +48,7 @@ fn (mut app App) post_repo() web.Result { params['arch'] = app.conf.default_arch } - new_repo := db.git_repo_from_params(params) or { + new_repo := models.from_params(params) or { return app.json(http.Status.bad_request, new_response(err.msg())) } @@ -78,7 +79,7 @@ fn (mut app App) patch_repo(id int) web.Result { app.db.update_git_repo(id, app.query) if 'arch' in app.query { - arch_objs := app.query['arch'].split(',').map(db.GitRepoArch{ value: it }) + arch_objs := app.query['arch'].split(',').map(GitRepoArch{ value: it }) app.db.update_git_repo_archs(id, arch_objs) } diff --git a/src/util/util.v b/src/util/util.v index cbb40725..f9d47a82 100644 --- a/src/util/util.v +++ b/src/util/util.v @@ -71,6 +71,5 @@ pub fn struct_to_map(o T) map[string]string { $for field in T.fields { m[field.name] = o.$(field.name).str() } - return m } From 2fc25f1afefe34d4e196aab7b84f2a122045d154 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Thu, 19 May 2022 08:20:11 +0200 Subject: [PATCH 030/236] refactor: moved BuildLog to models --- src/client/logs.v | 2 +- src/console/logs/logs.v | 4 ++-- src/db/db.v | 2 +- src/db/git.v | 1 + src/db/logs.v | 27 +-------------------------- src/models/logs.v | 28 ++++++++++++++++++++++++++++ src/server/git.v | 2 +- src/server/logs.v | 3 ++- 8 files changed, 37 insertions(+), 32 deletions(-) create mode 100644 src/models/logs.v diff --git a/src/client/logs.v b/src/client/logs.v index cdacab9b..3484f036 100644 --- a/src/client/logs.v +++ b/src/client/logs.v @@ -1,6 +1,6 @@ module client -import db { BuildLog } +import models { BuildLog } import net.http { Method } import response { Response } import time diff --git a/src/console/logs/logs.v b/src/console/logs/logs.v index 6400e801..1d0bb556 100644 --- a/src/console/logs/logs.v +++ b/src/console/logs/logs.v @@ -3,8 +3,8 @@ module logs import cli import env import client -import db import console +import models { BuildLog } struct Config { address string [required] @@ -67,7 +67,7 @@ pub fn cmd() cli.Command { } // print_log_list prints a list of logs. -fn print_log_list(logs []db.BuildLog) ? { +fn print_log_list(logs []BuildLog) ? { data := logs.map([it.id.str(), it.repo_id.str(), it.start_time.str(), it.exit_code.str()]) diff --git a/src/db/db.v b/src/db/db.v index a5756e6c..57b437e6 100644 --- a/src/db/db.v +++ b/src/db/db.v @@ -1,7 +1,7 @@ module db import sqlite -import models +import models { BuildLog, GitRepo } struct VieterDb { conn sqlite.DB diff --git a/src/db/git.v b/src/db/git.v index d8887ffe..8cc493f3 100644 --- a/src/db/git.v +++ b/src/db/git.v @@ -66,6 +66,7 @@ pub fn (db &VieterDb) update_git_repo(repo_id int, params map[string]string) { } } values_str := values.join(', ') + // I think this is actual SQL & not the ORM language query := 'update GitRepo set $values_str where id == $repo_id' db.conn.exec_none(query) diff --git a/src/db/logs.v b/src/db/logs.v index 817db78b..129ec4e1 100644 --- a/src/db/logs.v +++ b/src/db/logs.v @@ -1,31 +1,6 @@ module db -import time - -pub struct BuildLog { -pub: - id int [primary; sql: serial] - repo_id int [nonull] - start_time time.Time [nonull] - end_time time.Time [nonull] - arch string [nonull] - exit_code int [nonull] -} - -// str returns a string representation. -pub fn (bl &BuildLog) str() string { - mut parts := [ - 'id: $bl.id', - 'repo id: $bl.repo_id', - 'start time: $bl.start_time', - 'end time: $bl.end_time', - 'arch: $bl.arch', - 'exit code: $bl.exit_code', - ] - str := parts.join('\n') - - return str -} +import models { BuildLog } // get_build_logs returns all BuildLog's in the database. pub fn (db &VieterDb) get_build_logs() []BuildLog { diff --git a/src/models/logs.v b/src/models/logs.v new file mode 100644 index 00000000..173336f4 --- /dev/null +++ b/src/models/logs.v @@ -0,0 +1,28 @@ +module models + +import time + +pub struct BuildLog { +pub: + id int [primary; sql: serial] + repo_id int [nonull] + start_time time.Time [nonull] + end_time time.Time [nonull] + arch string [nonull] + exit_code int [nonull] +} + +// str returns a string representation. +pub fn (bl &BuildLog) str() string { + mut parts := [ + 'id: $bl.id', + 'repo id: $bl.repo_id', + 'start time: $bl.start_time', + 'end time: $bl.end_time', + 'arch: $bl.arch', + 'exit code: $bl.exit_code', + ] + str := parts.join('\n') + + return str +} diff --git a/src/server/git.v b/src/server/git.v index ec433c76..c1bc6f34 100644 --- a/src/server/git.v +++ b/src/server/git.v @@ -4,7 +4,7 @@ import web import net.http import response { new_data_response, new_response } import db -import models { GitRepoArch } +import models { GitRepo, GitRepoArch, GitRepoFilter } // get_repos returns the current list of repos. ['/api/repos'; get] diff --git a/src/server/logs.v b/src/server/logs.v index 21331e5b..af0b0812 100644 --- a/src/server/logs.v +++ b/src/server/logs.v @@ -8,6 +8,7 @@ import db import time import os import util +import models { BuildLog } // get_logs returns all build logs in the database. A 'repo' query param can // optionally be added to limit the list of build logs to that repository. @@ -97,7 +98,7 @@ fn (mut app App) post_log() web.Result { } // Store log in db - log := db.BuildLog{ + log := BuildLog{ repo_id: repo_id start_time: start_time end_time: end_time From 0233b8559de41513153101a34311aaaa1147a211 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Thu, 19 May 2022 22:14:41 +0200 Subject: [PATCH 031/236] doc: added some missing docstrings --- src/build/build.v | 1 - src/cron/daemon/daemon.v | 1 - src/models/models.v | 5 +++++ src/util/util.v | 9 --------- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/build/build.v b/src/build/build.v index 1f26d03e..16942bd0 100644 --- a/src/build/build.v +++ b/src/build/build.v @@ -4,7 +4,6 @@ import docker import encoding.base64 import time import os -import db import strings import util import models { GitRepo } diff --git a/src/cron/daemon/daemon.v b/src/cron/daemon/daemon.v index e92dd68d..82c219de 100644 --- a/src/cron/daemon/daemon.v +++ b/src/cron/daemon/daemon.v @@ -7,7 +7,6 @@ import cron.expression { CronExpression, parse_expression } import math import build import docker -import db import os import client import models { GitRepo } diff --git a/src/models/models.v b/src/models/models.v index a32ae394..924f45ff 100644 --- a/src/models/models.v +++ b/src/models/models.v @@ -1,5 +1,7 @@ module models +// from_params creates a new instance of T from the given map by parsing all +// of its fields from the map. pub fn from_params(params map[string]string) ?T { mut o := T{} @@ -8,6 +10,8 @@ pub fn from_params(params map[string]string) ?T { return o } +// patch_from_params updates the given T object with the params defined in +// the map. pub fn patch_from_params(mut o T, params map[string]string) ? { $for field in T.fields { if field.name in params && params[field.name] != '' { @@ -26,6 +30,7 @@ pub fn patch_from_params(mut o T, params map[string]string) ? { } } +// params_from converts a given T struct into a map of strings. pub fn params_from(o &T) map[string]string { mut out := map[string]string{} diff --git a/src/util/util.v b/src/util/util.v index f9d47a82..266bcb5b 100644 --- a/src/util/util.v +++ b/src/util/util.v @@ -64,12 +64,3 @@ pub fn pretty_bytes(bytes int) string { return '${n:.2}${util.prefixes[i]}' } - -pub fn struct_to_map(o T) map[string]string { - mut m := map[string]string{} - - $for field in T.fields { - m[field.name] = o.$(field.name).str() - } - return m -} From 7f6e9e636c378978082200e2aa5342c43bba8a51 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Wed, 25 May 2022 09:24:01 +0200 Subject: [PATCH 032/236] fix(cron): retrieve all GitRepo's instead of first 25 --- src/client/git.v | 23 ++++++++++++++++++++++- src/cron/daemon/daemon.v | 2 +- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/client/git.v b/src/client/git.v index fd14718d..4496c08a 100644 --- a/src/client/git.v +++ b/src/client/git.v @@ -4,7 +4,7 @@ import models { GitRepo, GitRepoFilter } import net.http { Method } import response { Response } -// get_git_repos returns the current list of repos. +// get_git_repos returns a list of GitRepo's, given a filter object. pub fn (c &Client) get_git_repos(filter GitRepoFilter) ?[]GitRepo { params := models.params_from(filter) data := c.send_request<[]GitRepo>(Method.get, '/api/repos', params)? @@ -12,6 +12,27 @@ pub fn (c &Client) get_git_repos(filter GitRepoFilter) ?[]GitRepo { return data.data } +// get_all_git_repos retrieves *all* GitRepo's from the API using the default +// limit. +pub fn (c &Client) get_all_git_repos() ?[]GitRepo { + mut repos := []GitRepo{} + mut offset := u64(0) + + for { + sub_repos := c.get_git_repos(offset: offset)? + + if sub_repos.len == 0 { + break + } + + repos << sub_repos + + offset += u64(sub_repos.len) + } + + return repos +} + // get_git_repo returns the repo for a specific ID. pub fn (c &Client) get_git_repo(id int) ?GitRepo { data := c.send_request(Method.get, '/api/repos/$id', {})? diff --git a/src/cron/daemon/daemon.v b/src/cron/daemon/daemon.v index 82c219de..f1206d6d 100644 --- a/src/cron/daemon/daemon.v +++ b/src/cron/daemon/daemon.v @@ -178,7 +178,7 @@ fn (mut d Daemon) schedule_build(repo GitRepo) { fn (mut d Daemon) renew_repos() { d.linfo('Renewing repos...') - mut new_repos := d.client.get_git_repos() or { + mut new_repos := d.client.get_all_git_repos() or { d.lerror('Failed to renew repos. Retrying in ${daemon.api_update_retry_timeout}s...') d.api_update_timestamp = time.now().add_seconds(daemon.api_update_retry_timeout) From bd4bb9a9fbe51451cbfb6f5abd11169809f5d275 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Thu, 26 May 2022 09:15:49 +0200 Subject: [PATCH 033/236] feat: added cli command for previewing cron schedules --- src/console/schedule/schedule.v | 34 +++++++++++++++++++++++++++++++++ src/main.v | 2 ++ 2 files changed, 36 insertions(+) create mode 100644 src/console/schedule/schedule.v diff --git a/src/console/schedule/schedule.v b/src/console/schedule/schedule.v new file mode 100644 index 00000000..b086b532 --- /dev/null +++ b/src/console/schedule/schedule.v @@ -0,0 +1,34 @@ +module schedule + +import cli +import cron.expression { parse_expression } + +// cmd returns the cli submodule for previewing a cron schedule. +pub fn cmd() cli.Command { + return cli.Command{ + name: 'schedule' + description: 'Preview the behavior of a cron schedule.' + flags: [ + cli.Flag{ + name: 'count' + description: 'How many scheduled times to show.' + flag: cli.FlagType.int + default_value: ['5'] + }, + ] + execute: fn (cmd cli.Command) ? { + exp := parse_expression(cmd.args.join(' '))? + + mut t := exp.next_from_now()? + println(t) + + count := cmd.flags.get_int('count')? + + for _ in 1 .. count { + t = exp.next(t)? + + println(t) + } + } + } +} diff --git a/src/main.v b/src/main.v index 6df45dc3..885e0f32 100644 --- a/src/main.v +++ b/src/main.v @@ -5,6 +5,7 @@ import server import cli import console.git import console.logs +import console.schedule import cron fn main() { @@ -27,6 +28,7 @@ fn main() { git.cmd(), cron.cmd(), logs.cmd(), + schedule.cmd(), ] } app.setup() From 768da5b7907e27282744aa25946325d7df2d79f8 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Thu, 26 May 2022 13:34:57 +0200 Subject: [PATCH 034/236] refactor: added CronExpression.next_n function --- src/console/schedule/schedule.v | 12 ++++-------- src/cron/expression/expression.v | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/console/schedule/schedule.v b/src/console/schedule/schedule.v index b086b532..8fceddd9 100644 --- a/src/console/schedule/schedule.v +++ b/src/console/schedule/schedule.v @@ -2,11 +2,13 @@ module schedule import cli import cron.expression { parse_expression } +import time // cmd returns the cli submodule for previewing a cron schedule. pub fn cmd() cli.Command { return cli.Command{ name: 'schedule' + usage: 'schedule' description: 'Preview the behavior of a cron schedule.' flags: [ cli.Flag{ @@ -17,16 +19,10 @@ pub fn cmd() cli.Command { }, ] execute: fn (cmd cli.Command) ? { - exp := parse_expression(cmd.args.join(' '))? - - mut t := exp.next_from_now()? - println(t) - + ce := parse_expression(cmd.args.join(' '))? count := cmd.flags.get_int('count')? - for _ in 1 .. count { - t = exp.next(t)? - + for t in ce.next_n(time.now(), count)? { println(t) } } diff --git a/src/cron/expression/expression.v b/src/cron/expression/expression.v index 5eae332c..17d2dde2 100644 --- a/src/cron/expression/expression.v +++ b/src/cron/expression/expression.v @@ -121,6 +121,20 @@ pub fn (ce &CronExpression) next_from_now() ?time.Time { return ce.next(time.now()) } +// next_n returns the n next occurences of the expression, given a starting +// time. +pub fn (ce &CronExpression) next_n(ref time.Time, n int) ?[]time.Time { + mut times := []time.Time{cap: n} + + times << ce.next(ref)? + + for i in 1 .. n { + times << ce.next(times[i - 1])? + } + + return times +} + // parse_range parses a given string into a range of sorted integers, if // possible. fn parse_range(s string, min int, max int, mut bitv []bool) ? { From 0d5704ba157ecf2f5bce70a7e51e1880fa77f7b5 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Tue, 24 May 2022 22:48:17 +0200 Subject: [PATCH 035/236] feat(server): initial implementation of migrations --- CHANGELOG.md | 4 ++ src/db/db.v | 52 ++++++++++++++++++++++++-- src/db/migrations/001-initial/down.sql | 3 ++ src/db/migrations/001-initial/up.sql | 22 +++++++++++ src/server/server.v | 4 +- 5 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 src/db/migrations/001-initial/down.sql create mode 100644 src/db/migrations/001-initial/up.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index 7dc0a009..8c336fe4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://git.rustybever.be/vieter/vieter/src/branch/dev) +### Added + +* Database migrations + ## [0.3.0-alpha.2](https://git.rustybever.be/vieter/vieter/src/tag/0.3.0-alpha.2) ### Added diff --git a/src/db/db.v b/src/db/db.v index 57b437e6..9a06a038 100644 --- a/src/db/db.v +++ b/src/db/db.v @@ -1,19 +1,65 @@ module db import sqlite -import models { BuildLog, GitRepo } struct VieterDb { conn sqlite.DB } +struct MigrationVersion { + id int [primary] + version int +} + +const ( + migrations_up = [$embed_file('migrations/001-initial/up.sql')] + migrations_down = [$embed_file('migrations/001-initial/down.sql')] +) + // init initializes a database & adds the correct tables. pub fn init(db_path string) ?VieterDb { conn := sqlite.connect(db_path)? sql conn { - create table GitRepo - create table BuildLog + create table MigrationVersion + } + + cur_version := sql conn { + select from MigrationVersion limit 1 + } + + // If there's no row yet, we add it here + if cur_version == MigrationVersion{} { + sql conn { + insert cur_version into MigrationVersion + } + } + + // Apply each migration in order + for i in cur_version.version .. db.migrations_up.len { + migration := db.migrations_up[i].to_string() + + version_num := i + 1 + + // vfmt does not like these dots + println('Applying migration $version_num' + '...') + + // The sqlite library seems to not like it when multiple statements are + // passed in a single exec. Therefore, we split them & run them all + // separately. + for part in migration.split(';').map(it.trim_space()).filter(it != '') { + res := conn.exec_none(part) + + if res != sqlite.sqlite_done { + return error('An error occurred while applying migration $version_num') + } + } + + // The where clause doesn't really matter, as there will always only be + // one entry anyways. + sql conn { + update MigrationVersion set version = version_num where id > 0 + } } return VieterDb{ diff --git a/src/db/migrations/001-initial/down.sql b/src/db/migrations/001-initial/down.sql new file mode 100644 index 00000000..43ad40b3 --- /dev/null +++ b/src/db/migrations/001-initial/down.sql @@ -0,0 +1,3 @@ +DROP TABLE IF EXISTS BuildLog; +DROP TABLE IF EXISTS GitRepoArch; +DROP TABLE IF EXISTS GitRepo; diff --git a/src/db/migrations/001-initial/up.sql b/src/db/migrations/001-initial/up.sql new file mode 100644 index 00000000..ca0aaceb --- /dev/null +++ b/src/db/migrations/001-initial/up.sql @@ -0,0 +1,22 @@ +CREATE TABLE IF NOT EXISTS GitRepo ( + id INTEGER PRIMARY KEY, + url TEXT NOT NULL, + branch TEXT NOT NULL, + repo TEXT NOT NULL, + schedule TEXT +); + +CREATE TABLE IF NOT EXISTS GitRepoArch ( + id INTEGER PRIMARY KEY, + repo_id INTEGER NOT NULL, + value TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS BuildLog ( + id INTEGER PRIMARY KEY, + repo_id INTEGER NOT NULL, + start_time INTEGER NOT NULL, + end_time iNTEGER NOT NULL, + arch TEXT NOT NULL, + exit_code INTEGER NOT NULL +); diff --git a/src/server/server.v b/src/server/server.v index 090aa76e..2309ee7b 100644 --- a/src/server/server.v +++ b/src/server/server.v @@ -68,7 +68,9 @@ pub fn server(conf Config) ? { } db_file := os.join_path_single(conf.data_dir, server.db_file_name) - db := db.init(db_file) or { util.exit_with_message(1, 'Failed to initialize database.') } + db := db.init(db_file) or { + util.exit_with_message(1, 'Failed to initialize database: $err.msg()') + } web.run(&App{ logger: logger From 31e903ebeb167a47fe15ca829f450138ce69483b Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Thu, 19 May 2022 22:43:38 +0200 Subject: [PATCH 036/236] feat(server): partial implementation of BuildLog API filter --- CHANGELOG.md | 6 ++++++ src/db/logs.v | 32 ++++++++++++++++++++++++++++---- src/models/logs.v | 13 +++++++++++++ 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c336fe4..170c97c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added * Database migrations +* Query parameters for GitRepo API to filter responses +* Respective CLI flags for new GitRepo API parameters + +### Changed + +* Refactor of main types into `models` module ## [0.3.0-alpha.2](https://git.rustybever.be/vieter/vieter/src/tag/0.3.0-alpha.2) diff --git a/src/db/logs.v b/src/db/logs.v index 129ec4e1..49f905ba 100644 --- a/src/db/logs.v +++ b/src/db/logs.v @@ -1,13 +1,37 @@ module db -import models { BuildLog } +import models { BuildLog, BuildLogFilter } +import time // get_build_logs returns all BuildLog's in the database. -pub fn (db &VieterDb) get_build_logs() []BuildLog { - res := sql db.conn { - select from BuildLog order by id +pub fn (db &VieterDb) get_build_logs(filter BuildLogFilter) []BuildLog { + mut where_parts := []string{} + + if filter.repo != 0 { + where_parts << 'repo_id == $filter.repo' } + if filter.before != time.Time{} { + where_parts << 'start_time < $filter.before.unix_time()' + } + + if filter.after != time.Time{} { + where_parts << 'start_time < $filter.after.unix_time()' + } + + where_str := if where_parts.len > 0 { + ' where ' + where_parts.map('($it)').join(' and ') + } else { + '' + } + + query := 'select from BuildLog' + where_str + res := db.conn.exec(query) + +/* res := sql db.conn { */ +/* select from BuildLog where filter.repo == 0 || repo_id == filter.repo order by id */ +/* } */ + return res } diff --git a/src/models/logs.v b/src/models/logs.v index 173336f4..1bfc7a69 100644 --- a/src/models/logs.v +++ b/src/models/logs.v @@ -26,3 +26,16 @@ pub fn (bl &BuildLog) str() string { return str } + +[params] +pub struct BuildLogFilter { +pub mut: + limit u64 = 25 + offset u64 + repo int + before time.Time + after time.Time + exit_codes_whitelist []u8 + exit_codes_blacklist []u8 + arch string +} From 4f32dec5b5e689afe78ed3df1fcde15525027e17 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 28 May 2022 22:22:55 +0200 Subject: [PATCH 037/236] feat(db): added function to convert sqlite output to struct --- src/db/db.v | 19 +++++++++++++++++++ src/db/logs.v | 17 +++++++++-------- src/models/logs.v | 2 +- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/db/db.v b/src/db/db.v index 9a06a038..d6cc057f 100644 --- a/src/db/db.v +++ b/src/db/db.v @@ -1,6 +1,7 @@ module db import sqlite +import time struct VieterDb { conn sqlite.DB @@ -66,3 +67,21 @@ pub fn init(db_path string) ?VieterDb { conn: conn } } + +pub fn row_into(row sqlite.Row) T { + mut i := 0 + mut out := T{} + + $for field in T.fields { + $if field.typ is string { + out.$(field.name) = row.vals[i] + } $else $if field.typ is int { + out.$(field.name) = row.vals[i].int() + } $else $if field.typ is time.Time { + out.$(field.name) = time.unix(row.vals[i].int()) + } + + i += 1 + } + return out +} diff --git a/src/db/logs.v b/src/db/logs.v index 49f905ba..4ec48c7b 100644 --- a/src/db/logs.v +++ b/src/db/logs.v @@ -19,18 +19,19 @@ pub fn (db &VieterDb) get_build_logs(filter BuildLogFilter) []BuildLog { where_parts << 'start_time < $filter.after.unix_time()' } - where_str := if where_parts.len > 0 { - ' where ' + where_parts.map('($it)').join(' and ') - } else { - '' + mut where_str := '' + + if where_parts.len > 0 { + where_str = ' where ' + where_parts.map('($it)').join(' and ') } query := 'select from BuildLog' + where_str - res := db.conn.exec(query) + rows, _ := db.conn.exec(query) + res := rows.map(row_into(it)) -/* res := sql db.conn { */ -/* select from BuildLog where filter.repo == 0 || repo_id == filter.repo order by id */ -/* } */ + // res := sql db.conn { + // select from BuildLog where filter.repo == 0 || repo_id == filter.repo order by id + // } return res } diff --git a/src/models/logs.v b/src/models/logs.v index 1bfc7a69..82fc52fe 100644 --- a/src/models/logs.v +++ b/src/models/logs.v @@ -3,7 +3,7 @@ module models import time pub struct BuildLog { -pub: +pub mut: id int [primary; sql: serial] repo_id int [nonull] start_time time.Time [nonull] From a39c1aa5eb4031d3302ab1cf817f3c3903aa9412 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sun, 29 May 2022 20:08:21 +0200 Subject: [PATCH 038/236] feat(server): added proper filtering the BuildLog API --- src/db/logs.v | 35 ++++++++++++++++++++++++++++------- src/models/logs.v | 3 +-- src/models/models.v | 7 +++++++ src/server/logs.v | 9 ++++----- 4 files changed, 40 insertions(+), 14 deletions(-) diff --git a/src/db/logs.v b/src/db/logs.v index 4ec48c7b..806f3d8f 100644 --- a/src/db/logs.v +++ b/src/db/logs.v @@ -16,23 +16,44 @@ pub fn (db &VieterDb) get_build_logs(filter BuildLogFilter) []BuildLog { } if filter.after != time.Time{} { - where_parts << 'start_time < $filter.after.unix_time()' + where_parts << 'start_time > $filter.after.unix_time()' + } + + // NOTE: possible SQL injection + if filter.arch != '' { + where_parts << "arch == '$filter.arch'" + } + + println(filter.exit_codes) + + mut parts := []string{} + + for exp in filter.exit_codes { + if exp[0] == `!` { + code := exp[1..].int() + + parts << 'exit_code != $code' + }else { + code := exp.int() + + parts << 'exit_code == $code' + } + } + + if parts.len > 0 { + where_parts << parts.map('($it)').join(' or ') } mut where_str := '' if where_parts.len > 0 { - where_str = ' where ' + where_parts.map('($it)').join(' and ') + where_str = 'where ' + where_parts.map('($it)').join(' and ') } - query := 'select from BuildLog' + where_str + query := 'select * from BuildLog $where_str limit $filter.limit offset $filter.offset' rows, _ := db.conn.exec(query) res := rows.map(row_into(it)) - // res := sql db.conn { - // select from BuildLog where filter.repo == 0 || repo_id == filter.repo order by id - // } - return res } diff --git a/src/models/logs.v b/src/models/logs.v index 82fc52fe..5ea9dd57 100644 --- a/src/models/logs.v +++ b/src/models/logs.v @@ -35,7 +35,6 @@ pub mut: repo int before time.Time after time.Time - exit_codes_whitelist []u8 - exit_codes_blacklist []u8 arch string + exit_codes []string } diff --git a/src/models/models.v b/src/models/models.v index 924f45ff..0d0395a2 100644 --- a/src/models/models.v +++ b/src/models/models.v @@ -1,5 +1,7 @@ module models +import time + // from_params creates a new instance of T from the given map by parsing all // of its fields from the map. pub fn from_params(params map[string]string) ?T { @@ -23,7 +25,12 @@ pub fn patch_from_params(mut o T, params map[string]string) ? { o.$(field.name) = params[field.name].u64() } $else $if field.typ is []GitRepoArch { o.$(field.name) = params[field.name].split(',').map(GitRepoArch{ value: it }) + } $else $if field.typ is time.Time { + o.$(field.name) = time.unix(params[field.name].int()) + } $else $if field.typ is []string { + o.$(field.name) = params[field.name].split(',') } + } else if field.attrs.contains('nonull') { return error('Missing parameter: ${field.name}.') } diff --git a/src/server/logs.v b/src/server/logs.v index af0b0812..51b364fa 100644 --- a/src/server/logs.v +++ b/src/server/logs.v @@ -8,7 +8,7 @@ import db import time import os import util -import models { BuildLog } +import models { BuildLog, BuildLogFilter } // get_logs returns all build logs in the database. A 'repo' query param can // optionally be added to limit the list of build logs to that repository. @@ -18,11 +18,10 @@ fn (mut app App) get_logs() web.Result { return app.json(http.Status.unauthorized, new_response('Unauthorized.')) } - logs := if 'repo' in app.query { - app.db.get_build_logs_for_repo(app.query['repo'].int()) - } else { - app.db.get_build_logs() + filter := models.from_params(app.query) or { + return app.json(http.Status.bad_request, new_response('Invalid query parameters.')) } + logs := app.db.get_build_logs(filter) return app.json(http.Status.ok, new_data_response(logs)) } From 401e0291e39dd0e3f92c98db993ec63e0ab7cc10 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sun, 29 May 2022 21:07:46 +0200 Subject: [PATCH 039/236] feat(cli): added some filter flags to GitRepo CLI --- src/client/logs.v | 7 +++-- src/console/logs/logs.v | 67 ++++++++++++++++++++++++++++++++++++----- src/db/logs.v | 4 +-- src/models/logs.v | 12 ++++---- src/models/models.v | 9 ++++-- 5 files changed, 77 insertions(+), 22 deletions(-) diff --git a/src/client/logs.v b/src/client/logs.v index 3484f036..d4d373f7 100644 --- a/src/client/logs.v +++ b/src/client/logs.v @@ -1,13 +1,14 @@ module client -import models { BuildLog } +import models { BuildLog, BuildLogFilter } import net.http { Method } import response { Response } import time // get_build_logs returns all build logs. -pub fn (c &Client) get_build_logs() ?Response<[]BuildLog> { - data := c.send_request<[]BuildLog>(Method.get, '/api/logs', {})? +pub fn (c &Client) get_build_logs(filter BuildLogFilter) ?Response<[]BuildLog> { + params := models.params_from(filter) + data := c.send_request<[]BuildLog>(Method.get, '/api/logs', params)? return data } diff --git a/src/console/logs/logs.v b/src/console/logs/logs.v index 1d0bb556..48b9df96 100644 --- a/src/console/logs/logs.v +++ b/src/console/logs/logs.v @@ -4,7 +4,8 @@ import cli import env import client import console -import models { BuildLog } +import time +import models { BuildLog, BuildLogFilter } struct Config { address string [required] @@ -19,21 +20,71 @@ pub fn cmd() cli.Command { commands: [ cli.Command{ name: 'list' - description: 'List the build logs. If a repo ID is provided, only list the build logs for that repo.' + description: 'List build logs.' flags: [ cli.Flag{ - name: 'repo' - description: 'ID of the Git repo to restrict list to.' + name: 'limit' + description: 'How many results to return.' flag: cli.FlagType.int }, + cli.Flag{ + name: 'offset' + description: 'Minimum index to return.' + flag: cli.FlagType.int + }, + cli.Flag{ + name: 'repo' + description: 'Only return logs for this repo id.' + flag: cli.FlagType.int + }, + cli.Flag{ + name: 'today' + description: 'Only list logs started today (UTC time).' + flag: cli.FlagType.bool + }, + cli.Flag{ + name: 'failed' + description: 'Only list logs with non-zero exit codes.' + flag: cli.FlagType.bool + }, ] execute: fn (cmd cli.Command) ? { config_file := cmd.flags.get_string('config-file')? conf := env.load(config_file)? - repo_id := cmd.flags.get_int('repo')? + mut filter := BuildLogFilter{} - if repo_id == 0 { list(conf)? } else { list_for_repo(conf, repo_id)? } + limit := cmd.flags.get_int('limit')? + if limit != 0 { + filter.limit = u64(limit) + } + + offset := cmd.flags.get_int('offset')? + if offset != 0 { + filter.offset = u64(offset) + } + + repo_id := cmd.flags.get_int('repo')? + if repo_id != 0 { + filter.repo = repo_id + } + + if cmd.flags.get_bool('today')? { + today := time.now() + + filter.after = time.new_time(time.Time{ + year: today.year + month: today.month + day: today.day + }) + filter.before = filter.after.add_days(1) + } + + if cmd.flags.get_bool('failed')? { + filter.exit_codes = ['!0'] + } + + list(conf, filter)? } }, cli.Command{ @@ -75,9 +126,9 @@ fn print_log_list(logs []BuildLog) ? { } // list prints a list of all build logs. -fn list(conf Config) ? { +fn list(conf Config, filter BuildLogFilter) ? { c := client.new(conf.address, conf.api_key) - logs := c.get_build_logs()?.data + logs := c.get_build_logs(filter)?.data print_log_list(logs)? } diff --git a/src/db/logs.v b/src/db/logs.v index 806f3d8f..cac08e7f 100644 --- a/src/db/logs.v +++ b/src/db/logs.v @@ -24,8 +24,6 @@ pub fn (db &VieterDb) get_build_logs(filter BuildLogFilter) []BuildLog { where_parts << "arch == '$filter.arch'" } - println(filter.exit_codes) - mut parts := []string{} for exp in filter.exit_codes { @@ -33,7 +31,7 @@ pub fn (db &VieterDb) get_build_logs(filter BuildLogFilter) []BuildLog { code := exp[1..].int() parts << 'exit_code != $code' - }else { + } else { code := exp.int() parts << 'exit_code == $code' diff --git a/src/models/logs.v b/src/models/logs.v index 5ea9dd57..c92dc070 100644 --- a/src/models/logs.v +++ b/src/models/logs.v @@ -30,11 +30,11 @@ pub fn (bl &BuildLog) str() string { [params] pub struct BuildLogFilter { pub mut: - limit u64 = 25 - offset u64 - repo int - before time.Time - after time.Time - arch string + limit u64 = 25 + offset u64 + repo int + before time.Time + after time.Time + arch string exit_codes []string } diff --git a/src/models/models.v b/src/models/models.v index 0d0395a2..3a127bcc 100644 --- a/src/models/models.v +++ b/src/models/models.v @@ -30,7 +30,6 @@ pub fn patch_from_params(mut o T, params map[string]string) ? { } $else $if field.typ is []string { o.$(field.name) = params[field.name].split(',') } - } else if field.attrs.contains('nonull') { return error('Missing parameter: ${field.name}.') } @@ -42,7 +41,13 @@ pub fn params_from(o &T) map[string]string { mut out := map[string]string{} $for field in T.fields { - out[field.name] = o.$(field.name).str() + $if field.typ is time.Time { + out[field.name] = o.$(field.name).unix_time().str() + } $else $if field.typ is []string { + out[field.name] = o.$(field.name).join(',') + } $else { + out[field.name] = o.$(field.name).str() + } } return out } From a4ffc2c0e393464eb3c09d89d2873f018e3a76b4 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sun, 29 May 2022 22:16:39 +0200 Subject: [PATCH 040/236] feat(cli): added more advanced date flags for BuildLog CLI --- CHANGELOG.md | 11 +++++----- src/console/logs/logs.v | 45 ++++++++++++++++++++++++++++++++++++++++- src/db/db.v | 2 ++ 3 files changed, 51 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 170c97c6..f9fee6b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,12 +10,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added * Database migrations -* Query parameters for GitRepo API to filter responses -* Respective CLI flags for new GitRepo API parameters - -### Changed - -* Refactor of main types into `models` module +* Improved GitRepo & BuildLog API + * Pagination using `limit` & `offset` query params + * GitRepo: filter by repo + * BuildLog: filter by start & end date, repo, exit code & arch +* CLI flags to take advantage of above API improvements ## [0.3.0-alpha.2](https://git.rustybever.be/vieter/vieter/src/tag/0.3.0-alpha.2) diff --git a/src/console/logs/logs.v b/src/console/logs/logs.v index 48b9df96..3b1c756b 100644 --- a/src/console/logs/logs.v +++ b/src/console/logs/logs.v @@ -47,6 +47,21 @@ pub fn cmd() cli.Command { description: 'Only list logs with non-zero exit codes.' flag: cli.FlagType.bool }, + cli.Flag{ + name: 'day' + description: 'Only list logs started on this day. Format is YYYY-MM-DD.' + flag: cli.FlagType.string + }, + cli.Flag{ + name: 'before' + description: 'Only list logs started before this timestamp. Accepts any RFC 3339 date.' + flag: cli.FlagType.string + }, + cli.Flag{ + name: 'after' + description: 'Only list logs started after this timestamp. Accepts any RFC 3339 date.' + flag: cli.FlagType.string + }, ] execute: fn (cmd cli.Command) ? { config_file := cmd.flags.get_string('config-file')? @@ -79,9 +94,37 @@ pub fn cmd() cli.Command { }) filter.before = filter.after.add_days(1) } + // The -today flag overwrites any of the other date flags. + else { + day_str := cmd.flags.get_string('day')? + before_str := cmd.flags.get_string('before')? + after_str := cmd.flags.get_string('after')? + + if day_str != '' { + day := time.parse_rfc3339(day_str)? + + filter.after = time.new_time(time.Time{ + year: day.year + month: day.month + day: day.day + }) + + filter.before = filter.after.add_days(1) + } else { + if before_str != '' { + filter.before = time.parse_rfc3339(before_str)? + } + + if after_str != '' { + filter.after = time.parse_rfc3339(after_str)? + } + } + } if cmd.flags.get_bool('failed')? { - filter.exit_codes = ['!0'] + filter.exit_codes = [ + '!0', + ] } list(conf, filter)? diff --git a/src/db/db.v b/src/db/db.v index d6cc057f..fac14581 100644 --- a/src/db/db.v +++ b/src/db/db.v @@ -68,6 +68,8 @@ pub fn init(db_path string) ?VieterDb { } } +// row_into converts an sqlite.Row into a given type T by parsing each field +// from a string according to its type. pub fn row_into(row sqlite.Row) T { mut i := 0 mut out := T{} From edd71b41c2910ac27805c97150d80a9e26184763 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Tue, 31 May 2022 12:29:35 +0200 Subject: [PATCH 041/236] feat(cli): interpet input dates & print dates as local timezone --- src/console/logs/logs.v | 32 +++++++++++++++++++------------- src/docker/containers.v | 1 + 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/console/logs/logs.v b/src/console/logs/logs.v index 3b1c756b..cb6997fc 100644 --- a/src/console/logs/logs.v +++ b/src/console/logs/logs.v @@ -20,7 +20,7 @@ pub fn cmd() cli.Command { commands: [ cli.Command{ name: 'list' - description: 'List build logs.' + description: 'List build logs. All date strings in the output are converted to the local timezone. Any time strings provided as input should be in the local timezone as well.' flags: [ cli.Flag{ name: 'limit' @@ -39,7 +39,7 @@ pub fn cmd() cli.Command { }, cli.Flag{ name: 'today' - description: 'Only list logs started today (UTC time).' + description: 'Only list logs started today.' flag: cli.FlagType.bool }, cli.Flag{ @@ -49,17 +49,17 @@ pub fn cmd() cli.Command { }, cli.Flag{ name: 'day' - description: 'Only list logs started on this day. Format is YYYY-MM-DD.' + description: 'Only list logs started on this day. (format: YYYY-MM-DD)' flag: cli.FlagType.string }, cli.Flag{ name: 'before' - description: 'Only list logs started before this timestamp. Accepts any RFC 3339 date.' + description: 'Only list logs started before this timestamp. (format: YYYY-MM-DD HH:mm:ss)' flag: cli.FlagType.string }, cli.Flag{ name: 'after' - description: 'Only list logs started after this timestamp. Accepts any RFC 3339 date.' + description: 'Only list logs started after this timestamp. (format: YYYY-MM-DD HH:mm:ss)' flag: cli.FlagType.string }, ] @@ -84,6 +84,8 @@ pub fn cmd() cli.Command { filter.repo = repo_id } + tz_offset := time.offset() + if cmd.flags.get_bool('today')? { today := time.now() @@ -91,7 +93,7 @@ pub fn cmd() cli.Command { year: today.year month: today.month day: today.day - }) + }).add_seconds(-tz_offset) filter.before = filter.after.add_days(1) } // The -today flag overwrites any of the other date flags. @@ -102,21 +104,25 @@ pub fn cmd() cli.Command { if day_str != '' { day := time.parse_rfc3339(day_str)? - - filter.after = time.new_time(time.Time{ + day_utc := time.new_time(time.Time{ year: day.year month: day.month day: day.day - }) + }).add_seconds(-tz_offset) - filter.before = filter.after.add_days(1) + // The extra -1 is so we also return logs that + // started at exactly midnight (filter bounds are + // exclusive). therefore, we have to request logs + // started after 23:59:59 the previous day. + filter.after = day_utc.add_seconds(-1) + filter.before = day_utc.add_days(1) } else { if before_str != '' { - filter.before = time.parse_rfc3339(before_str)? + filter.before = time.parse(before_str)?.add_seconds(-tz_offset) } if after_str != '' { - filter.after = time.parse_rfc3339(after_str)? + filter.after = time.parse(after_str)?.add_seconds(-tz_offset) } } } @@ -162,7 +168,7 @@ pub fn cmd() cli.Command { // print_log_list prints a list of logs. fn print_log_list(logs []BuildLog) ? { - data := logs.map([it.id.str(), it.repo_id.str(), it.start_time.str(), + data := logs.map([it.id.str(), it.repo_id.str(), it.start_time.local().str(), it.exit_code.str()]) println(console.pretty_table(['id', 'repo', 'start time', 'exit code'], data)?) diff --git a/src/docker/containers.v b/src/docker/containers.v index 0bc59bb3..8fbf0274 100644 --- a/src/docker/containers.v +++ b/src/docker/containers.v @@ -83,6 +83,7 @@ pub fn (mut d DockerConn) inspect_container(id string) ?ContainerInspect { mut data := json.decode(ContainerInspect, body)? + // The Docker engine API *should* always return UTC time. data.state.start_time = time.parse_rfc3339(data.state.start_time_str)? if data.state.status == 'exited' { From aded6d438a2f2daa1beb36e6866f5601c8538b52 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Tue, 31 May 2022 12:41:50 +0200 Subject: [PATCH 042/236] feat(cli): use correct timezones strings for log info; show build duration --- src/models/logs.v | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/models/logs.v b/src/models/logs.v index c92dc070..7f5a5fe5 100644 --- a/src/models/logs.v +++ b/src/models/logs.v @@ -17,8 +17,9 @@ pub fn (bl &BuildLog) str() string { mut parts := [ 'id: $bl.id', 'repo id: $bl.repo_id', - 'start time: $bl.start_time', - 'end time: $bl.end_time', + 'start time: $bl.start_time.local()', + 'end time: $bl.end_time.local()', + 'duration: ${bl.end_time - bl.start_time}', 'arch: $bl.arch', 'exit code: $bl.exit_code', ] From 9f753f9c93f53b0251e14233f5439e3fc9d6e39e Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Thu, 12 May 2022 09:11:18 +0200 Subject: [PATCH 043/236] feat(build): add target repo to builds; update system for every build --- .woodpecker/.build.yml | 2 +- src/build/build.v | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/.woodpecker/.build.yml b/.woodpecker/.build.yml index 16981293..b0fd267e 100644 --- a/.woodpecker/.build.yml +++ b/.woodpecker/.build.yml @@ -45,7 +45,7 @@ pipeline: - export OBJ_PATH="/vieter/commits/$CI_COMMIT_SHA/vieter-$(echo '${PLATFORM}' | sed 's:/:-:g')" - export SIG_STRING="PUT\n\n$CONTENT_TYPE\n$DATE\n$OBJ_PATH" - - export SIGNATURE=`echo -en $SIG_STRING | openssl sha1 -hmac $S3_PASSWORD -binary | base64` + - export SIGNATURE="$(echo -en $SIG_STRING | openssl sha1 -hmac $S3_PASSWORD -binary | base64)" - > curl --silent diff --git a/src/build/build.v b/src/build/build.v index 16942bd0..3c9cae5d 100644 --- a/src/build/build.v +++ b/src/build/build.v @@ -102,16 +102,26 @@ pub fn build_repo(address string, api_key string, base_image_id string, repo &Gi build_arch := os.uname().machine + repo_url := '$address/$repo.repo' + // TODO what to do with PKGBUILDs that build multiple packages? commands := [ + // This will later be replaced by a proper setting for changing the + // mirrorlist + "echo -e '[$repo.repo]\nServer = $address/\$repo/\$arch\nSigLevel = Optional' >> /etc/pacman.conf" + // We need to update the package list of the repo we just added above. + // This should however not pull in a lot of packages as long as the + // builder image is rebuilt frequently. + 'pacman -Syu --needed --noconfirm', + 'su builder', 'git clone --single-branch --depth 1 --branch $repo.branch $repo.url repo', 'cd repo', 'makepkg --nobuild --syncdeps --needed --noconfirm', 'source PKGBUILD', // The build container checks whether the package is already // present on the server - 'curl -s --head --fail $address/$repo.repo/$build_arch/\$pkgname-\$pkgver-\$pkgrel && exit 0', - 'MAKEFLAGS="-j\$(nproc)" makepkg -s --noconfirm --needed && for pkg in \$(ls -1 *.pkg*); do curl -XPOST -T "\$pkg" -H "X-API-KEY: \$API_KEY" $address/$repo.repo/publish; done', + 'curl -s --head --fail $repo_url/$build_arch/\$pkgname-\$pkgver-\$pkgrel && exit 0', + 'MAKEFLAGS="-j\$(nproc)" makepkg -s --noconfirm --needed && for pkg in \$(ls -1 *.pkg*); do curl -XPOST -T "\$pkg" -H "X-API-KEY: \$API_KEY" $repo_url/publish; done', ] // We convert the list of commands into a base64 string, which then gets @@ -124,7 +134,7 @@ pub fn build_repo(address string, api_key string, base_image_id string, repo &Gi entrypoint: ['/bin/sh', '-c'] cmd: ['echo \$BUILD_SCRIPT | base64 -d | /bin/bash -e'] work_dir: '/build' - user: 'builder:builder' + // user: 'builder:builder' } id := dd.create_container(c)?.id From 48e2ae7645f2de788801185bb096aee11cfbf983 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Wed, 1 Jun 2022 20:34:36 +0200 Subject: [PATCH 044/236] feat(build): show shell commands in build logs --- CHANGELOG.md | 9 +++++++ src/build/build.v | 33 +++++------------------ src/build/build_script.sh | 20 ++++++++++++++ src/build/shell.v | 55 +++++++++++++++++++++++++++++++++++++++ src/build/shell_test.v | 16 ++++++++++++ 5 files changed, 106 insertions(+), 27 deletions(-) create mode 100644 src/build/build_script.sh create mode 100644 src/build/shell.v create mode 100644 src/build/shell_test.v diff --git a/CHANGELOG.md b/CHANGELOG.md index f9fee6b7..affd5c4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * BuildLog: filter by start & end date, repo, exit code & arch * CLI flags to take advantage of above API improvements +### Changed + +* Packages from target repo are available during builds + * This can be used as a basic way to support AUR dependencies, by adding + the dependencies to the same repository +* Every build now updates its packages first instead of solely relying on the + updated builder image +* Build logs now show commands being executed + ## [0.3.0-alpha.2](https://git.rustybever.be/vieter/vieter/src/tag/0.3.0-alpha.2) ### Added diff --git a/src/build/build.v b/src/build/build.v index 3c9cae5d..2e86471f 100644 --- a/src/build/build.v +++ b/src/build/build.v @@ -101,40 +101,19 @@ pub fn build_repo(address string, api_key string, base_image_id string, repo &Gi } build_arch := os.uname().machine + build_script := create_build_script(address, repo, build_arch) - repo_url := '$address/$repo.repo' - - // TODO what to do with PKGBUILDs that build multiple packages? - commands := [ - // This will later be replaced by a proper setting for changing the - // mirrorlist - "echo -e '[$repo.repo]\nServer = $address/\$repo/\$arch\nSigLevel = Optional' >> /etc/pacman.conf" - // We need to update the package list of the repo we just added above. - // This should however not pull in a lot of packages as long as the - // builder image is rebuilt frequently. - 'pacman -Syu --needed --noconfirm', - 'su builder', - 'git clone --single-branch --depth 1 --branch $repo.branch $repo.url repo', - 'cd repo', - 'makepkg --nobuild --syncdeps --needed --noconfirm', - 'source PKGBUILD', - // The build container checks whether the package is already - // present on the server - 'curl -s --head --fail $repo_url/$build_arch/\$pkgname-\$pkgver-\$pkgrel && exit 0', - 'MAKEFLAGS="-j\$(nproc)" makepkg -s --noconfirm --needed && for pkg in \$(ls -1 *.pkg*); do curl -XPOST -T "\$pkg" -H "X-API-KEY: \$API_KEY" $repo_url/publish; done', - ] - - // We convert the list of commands into a base64 string, which then gets - // passed to the container as an env var - cmds_str := base64.encode_str(commands.join('\n')) + // We convert the build script into a base64 string, which then gets passed + // to the container as an env var + base64_script := base64.encode_str(build_script) c := docker.NewContainer{ image: '$base_image_id' - env: ['BUILD_SCRIPT=$cmds_str', 'API_KEY=$api_key'] + env: ['BUILD_SCRIPT=$base64_script', 'API_KEY=$api_key'] entrypoint: ['/bin/sh', '-c'] cmd: ['echo \$BUILD_SCRIPT | base64 -d | /bin/bash -e'] work_dir: '/build' - // user: 'builder:builder' + user: '0:0' } id := dd.create_container(c)?.id diff --git a/src/build/build_script.sh b/src/build/build_script.sh new file mode 100644 index 00000000..29f163ed --- /dev/null +++ b/src/build/build_script.sh @@ -0,0 +1,20 @@ +echo -e '+ echo -e '\''[vieter]\\nServer = https://example.com/$repo/$arch\\nSigLevel = Optional'\'' >> /etc/pacman.conf' +echo -e '[vieter]\nServer = https://example.com/$repo/$arch\nSigLevel = Optional' >> /etc/pacman.conf +echo -e '+ pacman -Syu --needed --noconfirm' +pacman -Syu --needed --noconfirm +echo -e '+ su builder' +su builder +echo -e '+ git clone --single-branch --depth 1 --branch main https://examplerepo.com repo' +git clone --single-branch --depth 1 --branch main https://examplerepo.com repo +echo -e '+ cd repo' +cd repo +echo -e '+ makepkg --nobuild --syncdeps --needed --noconfirm' +makepkg --nobuild --syncdeps --needed --noconfirm +echo -e '+ source PKGBUILD' +source PKGBUILD +echo -e '+ curl -s --head --fail https://example.com/vieter/x86_64/$pkgname-$pkgver-$pkgrel && exit 0' +curl -s --head --fail https://example.com/vieter/x86_64/$pkgname-$pkgver-$pkgrel && exit 0 +echo -e '+ [ "$(id -u)" == 0 ] && exit 0' +[ "$(id -u)" == 0 ] && exit 0 +echo -e '+ MAKEFLAGS="-j$(nproc)" makepkg -s --noconfirm --needed && for pkg in $(ls -1 *.pkg*); do curl -XPOST -T "$pkg" -H "X-API-KEY: $API_KEY" https://example.com/vieter/publish; done' +MAKEFLAGS="-j$(nproc)" makepkg -s --noconfirm --needed && for pkg in $(ls -1 *.pkg*); do curl -XPOST -T "$pkg" -H "X-API-KEY: $API_KEY" https://example.com/vieter/publish; done diff --git a/src/build/shell.v b/src/build/shell.v new file mode 100644 index 00000000..a3121fe8 --- /dev/null +++ b/src/build/shell.v @@ -0,0 +1,55 @@ +module build + +import models { GitRepo } + +// escape_shell_string escapes any characters that could be interpreted +// incorrectly by a shell. The resulting value should be safe to use inside an +// echo statement. +fn escape_shell_string(s string) string { + return s.replace(r'\', r'\\').replace("'", r"'\''") +} + +// echo_commands takes a list of shell commands & prepends each one with +// an echo call displaying said command. +pub fn echo_commands(cmds []string) []string { + mut out := []string{cap: 2 * cmds.len} + + for cmd in cmds { + out << "echo -e '+ ${escape_shell_string(cmd)}'" + out << cmd + } + + return out +} + +// create_build_script generates a shell script that builds a given GitRepo. +fn create_build_script(address string, repo &GitRepo, build_arch string) string { + repo_url := '$address/$repo.repo' + + commands := echo_commands([ + // This will later be replaced by a proper setting for changing the + // mirrorlist + "echo -e '[$repo.repo]\\nServer = $address/\$repo/\$arch\\nSigLevel = Optional' >> /etc/pacman.conf" + // We need to update the package list of the repo we just added above. + // This should however not pull in a lot of packages as long as the + // builder image is rebuilt frequently. + 'pacman -Syu --needed --noconfirm', + // makepkg can't run as root + 'su builder', + 'git clone --single-branch --depth 1 --branch $repo.branch $repo.url repo', + 'cd repo', + 'makepkg --nobuild --syncdeps --needed --noconfirm', + 'source PKGBUILD', + // The build container checks whether the package is already present on + // the server. + 'curl -s --head --fail $repo_url/$build_arch/\$pkgname-\$pkgver-\$pkgrel && exit 0', + // If the above curl command succeeds, we don't need to rebuild the + // package. However, because we're in a su shell, the exit command will + // drop us back into the root shell. Therefore, we must check whether + // we're in root so we don't proceed. + '[ "\$(id -u)" == 0 ] && exit 0', + 'MAKEFLAGS="-j\$(nproc)" makepkg -s --noconfirm --needed && for pkg in \$(ls -1 *.pkg*); do curl -XPOST -T "\$pkg" -H "X-API-KEY: \$API_KEY" $repo_url/publish; done', + ]) + + return commands.join('\n') +} diff --git a/src/build/shell_test.v b/src/build/shell_test.v new file mode 100644 index 00000000..46ab3501 --- /dev/null +++ b/src/build/shell_test.v @@ -0,0 +1,16 @@ +module build + +import models { GitRepo } + +fn test_create_build_script() { + repo := GitRepo{ + id: 1 + url: 'https://examplerepo.com' + branch: 'main' + repo: 'vieter' + } + build_script := create_build_script('https://example.com', repo, 'x86_64') + expected := $embed_file('build_script.sh') + + assert build_script == expected.to_string().trim_space() +} From 329e819e1540de3b9ad22ce5f7a1192e0b4b6527 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Wed, 1 Jun 2022 13:59:52 +0200 Subject: [PATCH 045/236] feat(console): added command to generate man pages --- .gitignore | 1 + CHANGELOG.md | 2 ++ Makefile | 5 +++++ PKGBUILD | 3 +++ PKGBUILD.dev | 3 +++ src/console/console.v | 14 ++++++++++++++ src/console/man/man.v | 21 +++++++++++++++++++++ src/main.v | 2 ++ 8 files changed, 51 insertions(+) create mode 100644 src/console/man/man.v diff --git a/.gitignore b/.gitignore index f27a43f3..4d9f94f2 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ gdb.txt # Generated docs _docs/ +/man/ diff --git a/CHANGELOG.md b/CHANGELOG.md index affd5c4e..91f1ef57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * GitRepo: filter by repo * BuildLog: filter by start & end date, repo, exit code & arch * CLI flags to take advantage of above API improvements +* Added CLI command to generate all man pages +* PKGBUILDs now install man pages ### Changed diff --git a/Makefile b/Makefile index 199b99e6..7eb35472 100644 --- a/Makefile +++ b/Makefile @@ -60,6 +60,11 @@ api-docs: rm -rf '$(SRC_DIR)/_docs' cd '$(SRC_DIR)' && v doc -all -f html -m -readme . +.PHONY: man +man: vieter + rm -rf man + ./vieter man man + # =====OTHER===== .PHONY: lint diff --git a/PKGBUILD b/PKGBUILD index 49fcf548..0296dbd6 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -24,4 +24,7 @@ package() { install -dm755 "$pkgdir/usr/bin" install -Dm755 "$pkgname/pvieter" "$pkgdir/usr/bin/vieter" + + install -dm755 "$pkgdir/usr/share/man/man1" + ./vieter man "$pkgdir/usr/share/man/man1" } diff --git a/PKGBUILD.dev b/PKGBUILD.dev index d0176d86..6d95136a 100644 --- a/PKGBUILD.dev +++ b/PKGBUILD.dev @@ -32,4 +32,7 @@ package() { install -dm755 "$pkgdir/usr/bin" install -Dm755 "$pkgname/pvieter" "$pkgdir/usr/bin/vieter" + + install -dm755 "$pkgdir/usr/share/man/man1" + ./vieter man "$pkgdir/usr/share/man/man1" } diff --git a/src/console/console.v b/src/console/console.v index dfb2fac8..7d782bac 100644 --- a/src/console/console.v +++ b/src/console/console.v @@ -2,6 +2,8 @@ module console import arrays import strings +import cli +import os // pretty_table converts a list of string data into a pretty table. Many thanks // to @hungrybluedev in the Vlang Discord for providing this code! @@ -54,3 +56,15 @@ pub fn pretty_table(header []string, data [][]string) ?string { return buffer.str() } + +// export_man_pages recursively generates all man pages for the given +// cli.Command & writes them to the given directory. +pub fn export_man_pages(cmd cli.Command, path string) ? { + man := cmd.manpage() + os.write_file(os.join_path_single(path, cmd.full_name().replace(' ', '-') + '.1'), + man)? + + for sub_cmd in cmd.commands { + export_man_pages(sub_cmd, path)? + } +} diff --git a/src/console/man/man.v b/src/console/man/man.v new file mode 100644 index 00000000..d91a140f --- /dev/null +++ b/src/console/man/man.v @@ -0,0 +1,21 @@ +module man + +import cli +import console +import os + +// cmd returns the cli submodule that handles generating man pages. +pub fn cmd() cli.Command { + return cli.Command{ + name: 'man' + description: 'Generate all man pages & save them in the given directory.' + usage: 'dir' + required_args: 1 + execute: fn (cmd cli.Command) ? { + root := cmd.root() + os.mkdir_all(cmd.args[0])? + + console.export_man_pages(root, cmd.args[0])? + } + } +} diff --git a/src/main.v b/src/main.v index 885e0f32..80c19556 100644 --- a/src/main.v +++ b/src/main.v @@ -6,6 +6,7 @@ import cli import console.git import console.logs import console.schedule +import console.man import cron fn main() { @@ -29,6 +30,7 @@ fn main() { cron.cmd(), logs.cmd(), schedule.cmd(), + man.cmd(), ] } app.setup() From e5582a2d54aa59a4ea8c0982998c27d559c821d8 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Thu, 2 Jun 2022 17:26:03 +0200 Subject: [PATCH 046/236] ci: deploy man pages to website --- .woodpecker/.man.yml | 36 ++++++++++++++++++++++++++++++++++++ CHANGELOG.md | 1 + 2 files changed, 37 insertions(+) create mode 100644 .woodpecker/.man.yml diff --git a/.woodpecker/.man.yml b/.woodpecker/.man.yml new file mode 100644 index 00000000..0b808866 --- /dev/null +++ b/.woodpecker/.man.yml @@ -0,0 +1,36 @@ +platform: 'linux/amd64' +branches: + exclude: [ main ] + +depends_on: + - build + +skip_clone: true + +pipeline: + generate: + image: 'chewingbever/vlang:latest' + pull: true + commands: + - curl -o vieter -L "https://s3.rustybever.be/vieter/commits/$CI_COMMIT_SHA/vieter-linux-amd64" + - chmod +x vieter + - ./vieter man man + - cd man + + # Generate an HTML page from each man page + - for f in $(ls -1 *.1); do mandoc -Thtml -O style=mandoc.css,man=%N.%S.html $f > "$f.html"; done + + # Download the mandoc.css file from the official site + - curl -o mandoc.css -L https://mandoc.bsd.lv/mandoc.css + + - tar czvf ../man.tar.gz *.html mandoc.css + + deploy: + image: 'curlimages/curl' + secrets: + - 'site_api_key' + commands: + - 'curl -XPOST --fail -s -H "Authorization: Bearer $SITE_API_KEY" -T man.tar.gz https://rustybever.be/api/deploy?dir=man-vieter' + when: + event: push + branch: dev diff --git a/CHANGELOG.md b/CHANGELOG.md index 91f1ef57..93e566a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * CLI flags to take advantage of above API improvements * Added CLI command to generate all man pages * PKGBUILDs now install man pages +* CLI man pages are now hosted on https://rustybever.be ### Changed From f9a6d585dd9d27212684f3682bd0c66fdeb9d096 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Thu, 2 Jun 2022 19:12:38 +0200 Subject: [PATCH 047/236] fix(arch): use correct path for pvieter binary --- PKGBUILD | 2 +- PKGBUILD.dev | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 0296dbd6..c33843f5 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -26,5 +26,5 @@ package() { install -Dm755 "$pkgname/pvieter" "$pkgdir/usr/bin/vieter" install -dm755 "$pkgdir/usr/share/man/man1" - ./vieter man "$pkgdir/usr/share/man/man1" + "$pkgname/pvieter" man "$pkgdir/usr/share/man/man1" } diff --git a/PKGBUILD.dev b/PKGBUILD.dev index 6d95136a..1aa07305 100644 --- a/PKGBUILD.dev +++ b/PKGBUILD.dev @@ -34,5 +34,5 @@ package() { install -Dm755 "$pkgname/pvieter" "$pkgdir/usr/bin/vieter" install -dm755 "$pkgdir/usr/share/man/man1" - ./vieter man "$pkgdir/usr/share/man/man1" + "$pkgname/pvieter" man "$pkgdir/usr/share/man/man1" } From 0e8e7223ab8927516af11301bc64ffbf07a69d4d Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Thu, 2 Jun 2022 20:14:44 +0200 Subject: [PATCH 048/236] fix(arch): properly install man pages --- PKGBUILD | 10 +++++++--- PKGBUILD.dev | 10 +++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index c33843f5..c2c33b27 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -5,6 +5,7 @@ pkgbase='vieter' pkgname='vieter' pkgver='0.3.0_alpha.2' pkgrel=1 +pkgdesc="Vieter is a lightweight implementation of an Arch repository server." depends=('glibc' 'openssl' 'libarchive' 'sqlite') makedepends=('git' 'vieter-v') arch=('x86_64' 'aarch64') @@ -17,14 +18,17 @@ build() { cd "$pkgname" make prod + + # The default CFLAGS for some reason causes vieter to segfault if used + # inside the PKGBUILD. As a workaround, we use tcc to build a debug build + # that does work, so we can generate the manpages. + CFLAGS= make man } package() { - pkgdesc="Vieter is a lightweight implementation of an Arch repository server." - install -dm755 "$pkgdir/usr/bin" install -Dm755 "$pkgname/pvieter" "$pkgdir/usr/bin/vieter" install -dm755 "$pkgdir/usr/share/man/man1" - "$pkgname/pvieter" man "$pkgdir/usr/share/man/man1" + install -Dm644 "$pkgname/man"/*.1 "$pkgdir/usr/share/man/man1" } diff --git a/PKGBUILD.dev b/PKGBUILD.dev index 1aa07305..babb6aad 100644 --- a/PKGBUILD.dev +++ b/PKGBUILD.dev @@ -5,6 +5,7 @@ pkgbase='vieter-git' pkgname='vieter-git' pkgver=0.2.0.r25.g20112b8 pkgrel=1 +pkgdesc="Vieter is a lightweight implementation of an Arch repository server." depends=('glibc' 'openssl' 'libarchive' 'sqlite') makedepends=('git' 'vieter-v') arch=('x86_64' 'aarch64') @@ -25,14 +26,17 @@ build() { cd "$pkgname" make prod + + # The default CFLAGS for some reason causes vieter to segfault if used + # inside the PKGBUILD. As a workaround, we use tcc to build a debug build + # that does work, so we can generate the manpages. + CFLAGS= make man } package() { - pkgdesc="Vieter is a lightweight implementation of an Arch repository server." - install -dm755 "$pkgdir/usr/bin" install -Dm755 "$pkgname/pvieter" "$pkgdir/usr/bin/vieter" install -dm755 "$pkgdir/usr/share/man/man1" - "$pkgname/pvieter" man "$pkgdir/usr/share/man/man1" + install -Dm644 "$pkgname/man"/*.1 "$pkgdir/usr/share/man/man1" } From 2abbc1111870d3735ac3c3a94cc25560926c855e Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Thu, 2 Jun 2022 20:21:20 +0200 Subject: [PATCH 049/236] fix(arch): also blank LDFLAGS for debug build --- PKGBUILD | 2 +- PKGBUILD.dev | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index c2c33b27..656d9d47 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -22,7 +22,7 @@ build() { # The default CFLAGS for some reason causes vieter to segfault if used # inside the PKGBUILD. As a workaround, we use tcc to build a debug build # that does work, so we can generate the manpages. - CFLAGS= make man + CFLAGS= LDFLAGS= make man } package() { diff --git a/PKGBUILD.dev b/PKGBUILD.dev index babb6aad..d25abb34 100644 --- a/PKGBUILD.dev +++ b/PKGBUILD.dev @@ -30,7 +30,7 @@ build() { # The default CFLAGS for some reason causes vieter to segfault if used # inside the PKGBUILD. As a workaround, we use tcc to build a debug build # that does work, so we can generate the manpages. - CFLAGS= make man + CFLAGS= LDFLAGS= make man } package() { From 1d034e670e7f60bfde102d7300c63de2088831ec Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Fri, 3 Jun 2022 17:13:17 +0200 Subject: [PATCH 050/236] docs: started HTTP documentation using slate --- docs/api/.dockerignore | 12 + docs/api/.editorconfig | 18 + docs/api/.gitattributes | 1 + docs/api/.gitignore | 27 + docs/api/Gemfile | 13 + docs/api/Gemfile.lock | 145 + docs/api/config.rb | 63 + docs/api/font-selection.json | 148 + docs/api/lib/monokai_sublime_slate.rb | 95 + docs/api/lib/multilang.rb | 16 + docs/api/lib/nesting_unique_head.rb | 22 + docs/api/lib/toc_data.rb | 31 + docs/api/lib/unique_head.rb | 24 + docs/api/slate.sh | 248 + docs/api/source/fonts/slate.eot | Bin 0 -> 1876 bytes docs/api/source/fonts/slate.svg | 14 + docs/api/source/fonts/slate.ttf | Bin 0 -> 1720 bytes docs/api/source/fonts/slate.woff | Bin 0 -> 1796 bytes docs/api/source/fonts/slate.woff2 | Bin 0 -> 796 bytes docs/api/source/images/logo.png | Bin 0 -> 22317 bytes docs/api/source/images/navbar.png | Bin 0 -> 96 bytes docs/api/source/includes/_errors.md | 22 + docs/api/source/includes/_git.md | 0 docs/api/source/includes/_logs.md | 0 docs/api/source/includes/_repository.md | 95 + docs/api/source/index.html.md | 40 + docs/api/source/javascripts/all.js | 2 + docs/api/source/javascripts/all_nosearch.js | 27 + docs/api/source/javascripts/app/_copy.js | 15 + docs/api/source/javascripts/app/_lang.js | 171 + docs/api/source/javascripts/app/_search.js | 102 + docs/api/source/javascripts/app/_toc.js | 122 + docs/api/source/javascripts/lib/_energize.js | 169 + .../javascripts/lib/_imagesloaded.min.js | 7 + .../javascripts/lib/_jquery.highlight.js | 108 + docs/api/source/javascripts/lib/_jquery.js | 10881 ++++++++++++++++ docs/api/source/javascripts/lib/_lunr.js | 3475 +++++ docs/api/source/layouts/layout.erb | 137 + docs/api/source/stylesheets/_icon-font.scss | 38 + docs/api/source/stylesheets/_normalize.scss | 427 + docs/api/source/stylesheets/_rtl.scss | 140 + docs/api/source/stylesheets/_variables.scss | 103 + docs/api/source/stylesheets/print.css.scss | 153 + docs/api/source/stylesheets/screen.css.scss | 633 + 44 files changed, 17744 insertions(+) create mode 100644 docs/api/.dockerignore create mode 100644 docs/api/.editorconfig create mode 100644 docs/api/.gitattributes create mode 100644 docs/api/.gitignore create mode 100644 docs/api/Gemfile create mode 100644 docs/api/Gemfile.lock create mode 100644 docs/api/config.rb create mode 100755 docs/api/font-selection.json create mode 100644 docs/api/lib/monokai_sublime_slate.rb create mode 100644 docs/api/lib/multilang.rb create mode 100644 docs/api/lib/nesting_unique_head.rb create mode 100644 docs/api/lib/toc_data.rb create mode 100644 docs/api/lib/unique_head.rb create mode 100755 docs/api/slate.sh create mode 100644 docs/api/source/fonts/slate.eot create mode 100644 docs/api/source/fonts/slate.svg create mode 100644 docs/api/source/fonts/slate.ttf create mode 100644 docs/api/source/fonts/slate.woff create mode 100644 docs/api/source/fonts/slate.woff2 create mode 100644 docs/api/source/images/logo.png create mode 100644 docs/api/source/images/navbar.png create mode 100644 docs/api/source/includes/_errors.md create mode 100644 docs/api/source/includes/_git.md create mode 100644 docs/api/source/includes/_logs.md create mode 100644 docs/api/source/includes/_repository.md create mode 100644 docs/api/source/index.html.md create mode 100644 docs/api/source/javascripts/all.js create mode 100644 docs/api/source/javascripts/all_nosearch.js create mode 100644 docs/api/source/javascripts/app/_copy.js create mode 100644 docs/api/source/javascripts/app/_lang.js create mode 100644 docs/api/source/javascripts/app/_search.js create mode 100644 docs/api/source/javascripts/app/_toc.js create mode 100644 docs/api/source/javascripts/lib/_energize.js create mode 100644 docs/api/source/javascripts/lib/_imagesloaded.min.js create mode 100644 docs/api/source/javascripts/lib/_jquery.highlight.js create mode 100644 docs/api/source/javascripts/lib/_jquery.js create mode 100644 docs/api/source/javascripts/lib/_lunr.js create mode 100644 docs/api/source/layouts/layout.erb create mode 100644 docs/api/source/stylesheets/_icon-font.scss create mode 100644 docs/api/source/stylesheets/_normalize.scss create mode 100644 docs/api/source/stylesheets/_rtl.scss create mode 100644 docs/api/source/stylesheets/_variables.scss create mode 100644 docs/api/source/stylesheets/print.css.scss create mode 100644 docs/api/source/stylesheets/screen.css.scss diff --git a/docs/api/.dockerignore b/docs/api/.dockerignore new file mode 100644 index 00000000..f6430175 --- /dev/null +++ b/docs/api/.dockerignore @@ -0,0 +1,12 @@ +.git/ +.github/ +build/ +.editorconfig +.gitattributes +.gitignore +CHANGELOG.md +CODE_OF_CONDUCT.md +deploy.sh +font-selection.json +README.md +Vagrantfile \ No newline at end of file diff --git a/docs/api/.editorconfig b/docs/api/.editorconfig new file mode 100644 index 00000000..1692977c --- /dev/null +++ b/docs/api/.editorconfig @@ -0,0 +1,18 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# Top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true + +[*.rb] +charset = utf-8 + +[*.md] +trim_trailing_whitespace = false diff --git a/docs/api/.gitattributes b/docs/api/.gitattributes new file mode 100644 index 00000000..3069c432 --- /dev/null +++ b/docs/api/.gitattributes @@ -0,0 +1 @@ +source/javascripts/lib/* linguist-vendored diff --git a/docs/api/.gitignore b/docs/api/.gitignore new file mode 100644 index 00000000..1d5d08dd --- /dev/null +++ b/docs/api/.gitignore @@ -0,0 +1,27 @@ +*.gem +*.rbc +.bundle +.config +coverage +InstalledFiles +lib/bundler/man +pkg +rdoc +spec/reports +test/tmp +test/version_tmp +tmp +*.DS_STORE +build/ +.cache +.vagrant +.sass-cache + +# YARD artifacts +.yardoc +_yardoc +doc/ +.idea/ + +# Vagrant artifacts +ubuntu-*-console.log diff --git a/docs/api/Gemfile b/docs/api/Gemfile new file mode 100644 index 00000000..7604fd42 --- /dev/null +++ b/docs/api/Gemfile @@ -0,0 +1,13 @@ +ruby '>= 2.6' +source 'https://rubygems.org' + +# Middleman +gem 'middleman', '~> 4.4' +gem 'middleman-syntax', '~> 3.2' +gem 'middleman-autoprefixer', '~> 3.0' +gem 'middleman-sprockets', '~> 4.1' +gem 'rouge', '~> 3.21' +gem 'redcarpet', '~> 3.5.0' +gem 'nokogiri', '~> 1.13.3' +gem 'sass' +gem 'webrick' diff --git a/docs/api/Gemfile.lock b/docs/api/Gemfile.lock new file mode 100644 index 00000000..bd60f7f6 --- /dev/null +++ b/docs/api/Gemfile.lock @@ -0,0 +1,145 @@ +GEM + remote: https://rubygems.org/ + specs: + activesupport (6.1.4.1) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + zeitwerk (~> 2.3) + addressable (2.8.0) + public_suffix (>= 2.0.2, < 5.0) + autoprefixer-rails (10.2.5.0) + execjs (< 2.8.0) + backports (3.21.0) + coffee-script (2.4.1) + coffee-script-source + execjs + coffee-script-source (1.12.2) + concurrent-ruby (1.1.9) + contracts (0.13.0) + dotenv (2.7.6) + erubis (2.7.0) + execjs (2.7.0) + fast_blank (1.0.1) + fastimage (2.2.5) + ffi (1.15.4) + haml (5.2.2) + temple (>= 0.8.0) + tilt + hamster (3.0.0) + concurrent-ruby (~> 1.0) + hashie (3.6.0) + i18n (1.6.0) + concurrent-ruby (~> 1.0) + kramdown (2.3.1) + rexml + listen (3.0.8) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) + memoist (0.16.2) + middleman (4.4.2) + coffee-script (~> 2.2) + haml (>= 4.0.5) + kramdown (>= 2.3.0) + middleman-cli (= 4.4.2) + middleman-core (= 4.4.2) + middleman-autoprefixer (3.0.0) + autoprefixer-rails (~> 10.0) + middleman-core (>= 4.0.0) + middleman-cli (4.4.2) + thor (>= 0.17.0, < 2.0) + middleman-core (4.4.2) + activesupport (>= 6.1, < 7.0) + addressable (~> 2.4) + backports (~> 3.6) + bundler (~> 2.0) + contracts (~> 0.13.0) + dotenv + erubis + execjs (~> 2.0) + fast_blank + fastimage (~> 2.0) + hamster (~> 3.0) + hashie (~> 3.4) + i18n (~> 1.6.0) + listen (~> 3.0.0) + memoist (~> 0.14) + padrino-helpers (~> 0.15.0) + parallel + rack (>= 1.4.5, < 3) + sassc (~> 2.0) + servolux + tilt (~> 2.0.9) + toml + uglifier (~> 3.0) + webrick + middleman-sprockets (4.1.1) + middleman-core (~> 4.0) + sprockets (>= 3.0) + middleman-syntax (3.2.0) + middleman-core (>= 3.2) + rouge (~> 3.2) + mini_portile2 (2.8.0) + minitest (5.14.4) + nokogiri (1.13.4) + mini_portile2 (~> 2.8.0) + racc (~> 1.4) + padrino-helpers (0.15.1) + i18n (>= 0.6.7, < 2) + padrino-support (= 0.15.1) + tilt (>= 1.4.1, < 3) + padrino-support (0.15.1) + parallel (1.21.0) + parslet (2.0.0) + public_suffix (4.0.6) + racc (1.6.0) + rack (2.2.3) + rb-fsevent (0.11.0) + rb-inotify (0.10.1) + ffi (~> 1.0) + redcarpet (3.5.1) + rexml (3.2.5) + rouge (3.28.0) + sass (3.7.4) + sass-listen (~> 4.0.0) + sass-listen (4.0.0) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) + sassc (2.4.0) + ffi (~> 1.9) + servolux (0.13.0) + sprockets (3.7.2) + concurrent-ruby (~> 1.0) + rack (> 1, < 3) + temple (0.8.2) + thor (1.1.0) + tilt (2.0.10) + toml (0.3.0) + parslet (>= 1.8.0, < 3.0.0) + tzinfo (2.0.4) + concurrent-ruby (~> 1.0) + uglifier (3.2.0) + execjs (>= 0.3.0, < 3) + webrick (1.7.0) + zeitwerk (2.5.1) + +PLATFORMS + ruby + +DEPENDENCIES + middleman (~> 4.4) + middleman-autoprefixer (~> 3.0) + middleman-sprockets (~> 4.1) + middleman-syntax (~> 3.2) + nokogiri (~> 1.13.3) + redcarpet (~> 3.5.0) + rouge (~> 3.21) + sass + webrick + +RUBY VERSION + ruby 2.7.2p137 + +BUNDLED WITH + 2.2.22 diff --git a/docs/api/config.rb b/docs/api/config.rb new file mode 100644 index 00000000..6f8b677f --- /dev/null +++ b/docs/api/config.rb @@ -0,0 +1,63 @@ +# Unique header generation +require './lib/unique_head.rb' + +# Markdown +set :markdown_engine, :redcarpet +set :markdown, + fenced_code_blocks: true, + smartypants: true, + disable_indented_code_blocks: true, + prettify: true, + strikethrough: true, + tables: true, + with_toc_data: true, + no_intra_emphasis: true, + renderer: UniqueHeadCounter + +# Assets +set :css_dir, 'stylesheets' +set :js_dir, 'javascripts' +set :images_dir, 'images' +set :fonts_dir, 'fonts' + +# Activate the syntax highlighter +activate :syntax +ready do + require './lib/monokai_sublime_slate.rb' + require './lib/multilang.rb' +end + +activate :sprockets + +activate :autoprefixer do |config| + config.browsers = ['last 2 version', 'Firefox ESR'] + config.cascade = false + config.inline = true +end + +# Github pages require relative links +activate :relative_assets +set :relative_links, true + +# Build Configuration +configure :build do + # We do want to hash woff and woff2 as there's a bug where woff2 will use + # woff asset hash which breaks things. Trying to use a combination of ignore and + # rewrite_ignore does not work as it conflicts weirdly with relative_assets. Disabling + # the .woff2 extension only does not work as .woff will still activate it so have to + # have both. See https://github.com/slatedocs/slate/issues/1171 for more details. + activate :asset_hash, :exts => app.config[:asset_extensions] - %w[.woff .woff2] + # If you're having trouble with Middleman hanging, commenting + # out the following two lines has been known to help + activate :minify_css + activate :minify_javascript + # activate :gzip +end + +# Deploy Configuration +# If you want Middleman to listen on a different port, you can set that below +set :port, 4567 + +helpers do + require './lib/toc_data.rb' +end diff --git a/docs/api/font-selection.json b/docs/api/font-selection.json new file mode 100755 index 00000000..5e78f5d8 --- /dev/null +++ b/docs/api/font-selection.json @@ -0,0 +1,148 @@ +{ + "IcoMoonType": "selection", + "icons": [ + { + "icon": { + "paths": [ + "M438.857 73.143q119.429 0 220.286 58.857t159.714 159.714 58.857 220.286-58.857 220.286-159.714 159.714-220.286 58.857-220.286-58.857-159.714-159.714-58.857-220.286 58.857-220.286 159.714-159.714 220.286-58.857zM512 785.714v-108.571q0-8-5.143-13.429t-12.571-5.429h-109.714q-7.429 0-13.143 5.714t-5.714 13.143v108.571q0 7.429 5.714 13.143t13.143 5.714h109.714q7.429 0 12.571-5.429t5.143-13.429zM510.857 589.143l10.286-354.857q0-6.857-5.714-10.286-5.714-4.571-13.714-4.571h-125.714q-8 0-13.714 4.571-5.714 3.429-5.714 10.286l9.714 354.857q0 5.714 5.714 10t13.714 4.286h105.714q8 0 13.429-4.286t6-10z" + ], + "attrs": [], + "isMulticolor": false, + "tags": [ + "exclamation-circle" + ], + "defaultCode": 61546, + "grid": 14 + }, + "attrs": [], + "properties": { + "id": 100, + "order": 4, + "prevSize": 28, + "code": 58880, + "name": "exclamation-sign", + "ligatures": "" + }, + "setIdx": 0, + "iconIdx": 0 + }, + { + "icon": { + "paths": [ + "M585.143 786.286v-91.429q0-8-5.143-13.143t-13.143-5.143h-54.857v-292.571q0-8-5.143-13.143t-13.143-5.143h-182.857q-8 0-13.143 5.143t-5.143 13.143v91.429q0 8 5.143 13.143t13.143 5.143h54.857v182.857h-54.857q-8 0-13.143 5.143t-5.143 13.143v91.429q0 8 5.143 13.143t13.143 5.143h256q8 0 13.143-5.143t5.143-13.143zM512 274.286v-91.429q0-8-5.143-13.143t-13.143-5.143h-109.714q-8 0-13.143 5.143t-5.143 13.143v91.429q0 8 5.143 13.143t13.143 5.143h109.714q8 0 13.143-5.143t5.143-13.143zM877.714 512q0 119.429-58.857 220.286t-159.714 159.714-220.286 58.857-220.286-58.857-159.714-159.714-58.857-220.286 58.857-220.286 159.714-159.714 220.286-58.857 220.286 58.857 159.714 159.714 58.857 220.286z" + ], + "attrs": [], + "isMulticolor": false, + "tags": [ + "info-circle" + ], + "defaultCode": 61530, + "grid": 14 + }, + "attrs": [], + "properties": { + "id": 85, + "order": 3, + "name": "info-sign", + "prevSize": 28, + "code": 58882 + }, + "setIdx": 0, + "iconIdx": 2 + }, + { + "icon": { + "paths": [ + "M733.714 419.429q0-16-10.286-26.286l-52-51.429q-10.857-10.857-25.714-10.857t-25.714 10.857l-233.143 232.571-129.143-129.143q-10.857-10.857-25.714-10.857t-25.714 10.857l-52 51.429q-10.286 10.286-10.286 26.286 0 15.429 10.286 25.714l206.857 206.857q10.857 10.857 25.714 10.857 15.429 0 26.286-10.857l310.286-310.286q10.286-10.286 10.286-25.714zM877.714 512q0 119.429-58.857 220.286t-159.714 159.714-220.286 58.857-220.286-58.857-159.714-159.714-58.857-220.286 58.857-220.286 159.714-159.714 220.286-58.857 220.286 58.857 159.714 159.714 58.857 220.286z" + ], + "attrs": [], + "isMulticolor": false, + "tags": [ + "check-circle" + ], + "defaultCode": 61528, + "grid": 14 + }, + "attrs": [], + "properties": { + "id": 83, + "order": 9, + "prevSize": 28, + "code": 58886, + "name": "ok-sign" + }, + "setIdx": 0, + "iconIdx": 6 + }, + { + "icon": { + "paths": [ + "M658.286 475.429q0-105.714-75.143-180.857t-180.857-75.143-180.857 75.143-75.143 180.857 75.143 180.857 180.857 75.143 180.857-75.143 75.143-180.857zM950.857 950.857q0 29.714-21.714 51.429t-51.429 21.714q-30.857 0-51.429-21.714l-196-195.429q-102.286 70.857-228 70.857-81.714 0-156.286-31.714t-128.571-85.714-85.714-128.571-31.714-156.286 31.714-156.286 85.714-128.571 128.571-85.714 156.286-31.714 156.286 31.714 128.571 85.714 85.714 128.571 31.714 156.286q0 125.714-70.857 228l196 196q21.143 21.143 21.143 51.429z" + ], + "width": 951, + "attrs": [], + "isMulticolor": false, + "tags": [ + "search" + ], + "defaultCode": 61442, + "grid": 14 + }, + "attrs": [], + "properties": { + "id": 2, + "order": 1, + "prevSize": 28, + "code": 58887, + "name": "icon-search" + }, + "setIdx": 0, + "iconIdx": 7 + } + ], + "height": 1024, + "metadata": { + "name": "slate", + "license": "SIL OFL 1.1" + }, + "preferences": { + "showGlyphs": true, + "showQuickUse": true, + "showQuickUse2": true, + "showSVGs": true, + "fontPref": { + "prefix": "icon-", + "metadata": { + "fontFamily": "slate", + "majorVersion": 1, + "minorVersion": 0, + "description": "Based on FontAwesome", + "license": "SIL OFL 1.1" + }, + "metrics": { + "emSize": 1024, + "baseline": 6.25, + "whitespace": 50 + }, + "resetPoint": 58880, + "showSelector": false, + "selector": "class", + "classSelector": ".icon", + "showMetrics": false, + "showMetadata": true, + "showVersion": true, + "ie7": false + }, + "imagePref": { + "prefix": "icon-", + "png": true, + "useClassSelector": true, + "color": 4473924, + "bgColor": 16777215 + }, + "historySize": 100, + "showCodes": true, + "gridSize": 16, + "showLiga": false + } +} diff --git a/docs/api/lib/monokai_sublime_slate.rb b/docs/api/lib/monokai_sublime_slate.rb new file mode 100644 index 00000000..cd2de331 --- /dev/null +++ b/docs/api/lib/monokai_sublime_slate.rb @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- # +# frozen_string_literal: true + +# this is based on https://github.com/rouge-ruby/rouge/blob/master/lib/rouge/themes/monokai_sublime.rb +# but without the added background, and changed styling for JSON keys to be soft_yellow instead of white + +module Rouge + module Themes + class MonokaiSublimeSlate < CSSTheme + name 'monokai.sublime.slate' + + palette :black => '#000000' + palette :bright_green => '#a6e22e' + palette :bright_pink => '#f92672' + palette :carmine => '#960050' + palette :dark => '#49483e' + palette :dark_grey => '#888888' + palette :dark_red => '#aa0000' + palette :dimgrey => '#75715e' + palette :emperor => '#555555' + palette :grey => '#999999' + palette :light_grey => '#aaaaaa' + palette :light_violet => '#ae81ff' + palette :soft_cyan => '#66d9ef' + palette :soft_yellow => '#e6db74' + palette :very_dark => '#1e0010' + palette :whitish => '#f8f8f2' + palette :orange => '#f6aa11' + palette :white => '#ffffff' + + style Generic::Heading, :fg => :grey + style Literal::String::Regex, :fg => :orange + style Generic::Output, :fg => :dark_grey + style Generic::Prompt, :fg => :emperor + style Generic::Strong, :bold => false + style Generic::Subheading, :fg => :light_grey + style Name::Builtin, :fg => :orange + style Comment::Multiline, + Comment::Preproc, + Comment::Single, + Comment::Special, + Comment, :fg => :dimgrey + style Error, + Generic::Error, + Generic::Traceback, :fg => :carmine + style Generic::Deleted, + Generic::Inserted, + Generic::Emph, :fg => :dark + style Keyword::Constant, + Keyword::Declaration, + Keyword::Reserved, + Name::Constant, + Keyword::Type, :fg => :soft_cyan + style Literal::Number::Float, + Literal::Number::Hex, + Literal::Number::Integer::Long, + Literal::Number::Integer, + Literal::Number::Oct, + Literal::Number, + Literal::String::Char, + Literal::String::Escape, + Literal::String::Symbol, :fg => :light_violet + style Literal::String::Doc, + Literal::String::Double, + Literal::String::Backtick, + Literal::String::Heredoc, + Literal::String::Interpol, + Literal::String::Other, + Literal::String::Single, + Literal::String, :fg => :soft_yellow + style Name::Attribute, + Name::Class, + Name::Decorator, + Name::Exception, + Name::Function, :fg => :bright_green + style Name::Variable::Class, + Name::Namespace, + Name::Entity, + Name::Builtin::Pseudo, + Name::Variable::Global, + Name::Variable::Instance, + Name::Variable, + Text::Whitespace, + Text, + Name, :fg => :white + style Name::Label, :fg => :bright_pink + style Operator::Word, + Name::Tag, + Keyword, + Keyword::Namespace, + Keyword::Pseudo, + Operator, :fg => :bright_pink + end + end + end diff --git a/docs/api/lib/multilang.rb b/docs/api/lib/multilang.rb new file mode 100644 index 00000000..36fbe5b1 --- /dev/null +++ b/docs/api/lib/multilang.rb @@ -0,0 +1,16 @@ +module Multilang + def block_code(code, full_lang_name) + if full_lang_name + parts = full_lang_name.split('--') + rouge_lang_name = (parts) ? parts[0] : "" # just parts[0] here causes null ref exception when no language specified + super(code, rouge_lang_name).sub("highlight #{rouge_lang_name}") do |match| + match + " tab-" + full_lang_name + end + else + super(code, full_lang_name) + end + end +end + +require 'middleman-core/renderers/redcarpet' +Middleman::Renderers::MiddlemanRedcarpetHTML.send :include, Multilang diff --git a/docs/api/lib/nesting_unique_head.rb b/docs/api/lib/nesting_unique_head.rb new file mode 100644 index 00000000..01278371 --- /dev/null +++ b/docs/api/lib/nesting_unique_head.rb @@ -0,0 +1,22 @@ +# Nested unique header generation +require 'middleman-core/renderers/redcarpet' + +class NestingUniqueHeadCounter < Middleman::Renderers::MiddlemanRedcarpetHTML + def initialize + super + @@headers_history = {} if !defined?(@@headers_history) + end + + def header(text, header_level) + friendly_text = text.gsub(/<[^>]*>/,"").parameterize + @@headers_history[header_level] = text.parameterize + + if header_level > 1 + for i in (header_level - 1).downto(1) + friendly_text.prepend("#{@@headers_history[i]}-") if @@headers_history.key?(i) + end + end + + return "#{text}" + end +end diff --git a/docs/api/lib/toc_data.rb b/docs/api/lib/toc_data.rb new file mode 100644 index 00000000..4a04efee --- /dev/null +++ b/docs/api/lib/toc_data.rb @@ -0,0 +1,31 @@ +require 'nokogiri' + +def toc_data(page_content) + html_doc = Nokogiri::HTML::DocumentFragment.parse(page_content) + + # get a flat list of headers + headers = [] + html_doc.css('h1, h2, h3').each do |header| + headers.push({ + id: header.attribute('id').to_s, + content: header.children, + title: header.children.to_s.gsub(/<[^>]*>/, ''), + level: header.name[1].to_i, + children: [] + }) + end + + [3,2].each do |header_level| + header_to_nest = nil + headers = headers.reject do |header| + if header[:level] == header_level + header_to_nest[:children].push header if header_to_nest + true + else + header_to_nest = header if header[:level] < header_level + false + end + end + end + headers +end diff --git a/docs/api/lib/unique_head.rb b/docs/api/lib/unique_head.rb new file mode 100644 index 00000000..d42bab2a --- /dev/null +++ b/docs/api/lib/unique_head.rb @@ -0,0 +1,24 @@ +# Unique header generation +require 'middleman-core/renderers/redcarpet' +require 'digest' +class UniqueHeadCounter < Middleman::Renderers::MiddlemanRedcarpetHTML + def initialize + super + @head_count = {} + end + def header(text, header_level) + friendly_text = text.gsub(/<[^>]*>/,"").parameterize + if friendly_text.strip.length == 0 + # Looks like parameterize removed the whole thing! It removes many unicode + # characters like Chinese and Russian. To get a unique URL, let's just + # URI escape the whole header + friendly_text = Digest::SHA1.hexdigest(text)[0,10] + end + @head_count[friendly_text] ||= 0 + @head_count[friendly_text] += 1 + if @head_count[friendly_text] > 1 + friendly_text += "-#{@head_count[friendly_text]}" + end + return "#{text}" + end +end diff --git a/docs/api/slate.sh b/docs/api/slate.sh new file mode 100755 index 00000000..a3cc498e --- /dev/null +++ b/docs/api/slate.sh @@ -0,0 +1,248 @@ +#!/usr/bin/env bash +set -o errexit #abort if any command fails + +me=$(basename "$0") + +help_message="\ +Usage: $me [] [] +Run commands related to the slate process. + +Commands: + + serve Run the middleman server process, useful for + development. + build Run the build process. + deploy Will build and deploy files to branch. Use + --no-build to only deploy. + +Global Options: + + -h, --help Show this help information. + -v, --verbose Increase verbosity. Useful for debugging. + +Deploy options: + -e, --allow-empty Allow deployment of an empty directory. + -m, --message MESSAGE Specify the message used when committing on the + deploy branch. + -n, --no-hash Don't append the source commit's hash to the deploy + commit's message. + --no-build Do not build the source files. +" + + +run_serve() { + exec bundle exec middleman serve --watcher-force-polling +} + +run_build() { + bundle exec middleman build --clean +} + +parse_args() { + # Set args from a local environment file. + if [ -e ".env" ]; then + source .env + fi + + command= + + # Parse arg flags + # If something is exposed as an environment variable, set/overwrite it + # here. Otherwise, set/overwrite the internal variable instead. + while : ; do + if [[ $1 = "-h" || $1 = "--help" ]]; then + echo "$help_message" + exit 0 + elif [[ $1 = "-v" || $1 = "--verbose" ]]; then + verbose=true + shift + elif [[ $1 = "-e" || $1 = "--allow-empty" ]]; then + allow_empty=true + shift + elif [[ ( $1 = "-m" || $1 = "--message" ) && -n $2 ]]; then + commit_message=$2 + shift 2 + elif [[ $1 = "-n" || $1 = "--no-hash" ]]; then + GIT_DEPLOY_APPEND_HASH=false + shift + elif [[ $1 = "--no-build" ]]; then + no_build=true + shift + elif [[ $1 = "serve" || $1 = "build" || $1 = "deploy" ]]; then + if [ ! -z "${command}" ]; then + >&2 echo "You can only specify one command." + exit 1 + fi + command=$1 + shift + elif [ -z $1 ]; then + break + fi + done + + if [ -z "${command}" ]; then + >&2 echo "Command not specified." + exit 1 + fi + + # Set internal option vars from the environment and arg flags. All internal + # vars should be declared here, with sane defaults if applicable. + + # Source directory & target branch. + deploy_directory=build + deploy_branch=gh-pages + + #if no user identity is already set in the current git environment, use this: + default_username=${GIT_DEPLOY_USERNAME:-deploy.sh} + default_email=${GIT_DEPLOY_EMAIL:-} + + #repository to deploy to. must be readable and writable. + repo=origin + + #append commit hash to the end of message by default + append_hash=${GIT_DEPLOY_APPEND_HASH:-true} +} + +main() { + enable_expanded_output + + if ! git diff --exit-code --quiet --cached; then + echo Aborting due to uncommitted changes in the index >&2 + return 1 + fi + + commit_title=`git log -n 1 --format="%s" HEAD` + commit_hash=` git log -n 1 --format="%H" HEAD` + + #default commit message uses last title if a custom one is not supplied + if [[ -z $commit_message ]]; then + commit_message="publish: $commit_title" + fi + + #append hash to commit message unless no hash flag was found + if [ $append_hash = true ]; then + commit_message="$commit_message"$'\n\n'"generated from commit $commit_hash" + fi + + previous_branch=`git rev-parse --abbrev-ref HEAD` + + if [ ! -d "$deploy_directory" ]; then + echo "Deploy directory '$deploy_directory' does not exist. Aborting." >&2 + return 1 + fi + + # must use short form of flag in ls for compatibility with macOS and BSD + if [[ -z `ls -A "$deploy_directory" 2> /dev/null` && -z $allow_empty ]]; then + echo "Deploy directory '$deploy_directory' is empty. Aborting. If you're sure you want to deploy an empty tree, use the --allow-empty / -e flag." >&2 + return 1 + fi + + if git ls-remote --exit-code $repo "refs/heads/$deploy_branch" ; then + # deploy_branch exists in $repo; make sure we have the latest version + + disable_expanded_output + git fetch --force $repo $deploy_branch:$deploy_branch + enable_expanded_output + fi + + # check if deploy_branch exists locally + if git show-ref --verify --quiet "refs/heads/$deploy_branch" + then incremental_deploy + else initial_deploy + fi + + restore_head +} + +initial_deploy() { + git --work-tree "$deploy_directory" checkout --orphan $deploy_branch + git --work-tree "$deploy_directory" add --all + commit+push +} + +incremental_deploy() { + #make deploy_branch the current branch + git symbolic-ref HEAD refs/heads/$deploy_branch + #put the previously committed contents of deploy_branch into the index + git --work-tree "$deploy_directory" reset --mixed --quiet + git --work-tree "$deploy_directory" add --all + + set +o errexit + diff=$(git --work-tree "$deploy_directory" diff --exit-code --quiet HEAD --)$? + set -o errexit + case $diff in + 0) echo No changes to files in $deploy_directory. Skipping commit.;; + 1) commit+push;; + *) + echo git diff exited with code $diff. Aborting. Staying on branch $deploy_branch so you can debug. To switch back to main, use: git symbolic-ref HEAD refs/heads/main && git reset --mixed >&2 + return $diff + ;; + esac +} + +commit+push() { + set_user_id + git --work-tree "$deploy_directory" commit -m "$commit_message" + + disable_expanded_output + #--quiet is important here to avoid outputting the repo URL, which may contain a secret token + git push --quiet $repo $deploy_branch + enable_expanded_output +} + +#echo expanded commands as they are executed (for debugging) +enable_expanded_output() { + if [ $verbose ]; then + set -o xtrace + set +o verbose + fi +} + +#this is used to avoid outputting the repo URL, which may contain a secret token +disable_expanded_output() { + if [ $verbose ]; then + set +o xtrace + set -o verbose + fi +} + +set_user_id() { + if [[ -z `git config user.name` ]]; then + git config user.name "$default_username" + fi + if [[ -z `git config user.email` ]]; then + git config user.email "$default_email" + fi +} + +restore_head() { + if [[ $previous_branch = "HEAD" ]]; then + #we weren't on any branch before, so just set HEAD back to the commit it was on + git update-ref --no-deref HEAD $commit_hash $deploy_branch + else + git symbolic-ref HEAD refs/heads/$previous_branch + fi + + git reset --mixed +} + +filter() { + sed -e "s|$repo|\$repo|g" +} + +sanitize() { + "$@" 2> >(filter 1>&2) | filter +} + +parse_args "$@" + +if [ "${command}" = "serve" ]; then + run_serve +elif [[ "${command}" = "build" ]]; then + run_build +elif [[ ${command} = "deploy" ]]; then + if [[ ${no_build} != true ]]; then + run_build + fi + main "$@" +fi diff --git a/docs/api/source/fonts/slate.eot b/docs/api/source/fonts/slate.eot new file mode 100644 index 0000000000000000000000000000000000000000..13c4839a1975d4c92d66753d75553f922743c6ef GIT binary patch literal 1876 zcmaJ?&2Jl35TEDWeRg&sP8)xqh$Kr)Yzaiu#`dnNaHtS!Xx9i53LF}#nAmmP`Xfq= z;s{Zte*qCEK;i~h4hYFPaA-x4sD#vVrBXQ{MIaD@1Ke8kn|8tnQ^fdI9N^x@? zd<=RUJW{D`U;b-v<0kYSA}zO8E|&DaCqF0BzlPme0}$TUZbAP8`m<~GR{K~75*dg= zcCEQu4DE@ZpmS=+>&5muJwb0nf0^x#V!izR7X1vpggLV7&CM3_1j&!tPMS_)mkgrN zC!rsJe5kniow8zt{RT+zltXle=pd}!=-!|+8X9a|iyqm&z#GOh#?Z4hMmoI$K1va6 zrUYgm&_U=R+`ZrJ0!LQ9E`42ef0@uHvC8U zo}w4%B?O&MCX$JGEG)w^HIqqa()pb0xK4IFpUb457c*fwDPqaQf|z%md}h1{#>ac0 zD>_@{@&c$_-fEYWRBE3yj7U8q4MTz%hA^cOxdwei%19MlXpM(P_)+e=@Rm}B>tXap zwoqO*DogaoKF+^`!zvX>{f10 zq*F6dk@0&Ok4=k2cHR|EKKaOn+_Q3KG-~~J-9n!;&D+d{W9=SQ|IqlDm9?y2uUl{( zi(0o$Q@Cby<g+B7U zE7gMM6{=S}Ps}C~lhg72%h#4X&vB-0d)je4Z)w>(?>M%-xE~jH;l@L}Lcyy(n4O9z z65lS`w&R>XbW_^$2bKN!lz(S%N3N!tcP>R={D&-^3rjyfS-aWi!AkH>sk+00G5&qW zC1%n(1Gmpd;5$IPUF_Lw@K%&WbbxEX?LgKcF9x!K$J`8LiMQ^#KsG5yOZ=|rBS1K&l2uG4tC&hwF_o-hDp_Le zTrgI}?0+pse + + +Generated by IcoMoon + + + + + + + + + + diff --git a/docs/api/source/fonts/slate.ttf b/docs/api/source/fonts/slate.ttf new file mode 100644 index 0000000000000000000000000000000000000000..ace9a46a7e1ed6b6ab3de2f30ef3e27c572f88d3 GIT binary patch literal 1720 zcmaJ>-D_KA7=PaL@t*V?ZMro%kz(6pV-rwZyCyk@Q7?wb>UJv9`gO6wj7^qiCTUqB zO%X@;7dX69!8^V6LWJzXiw!F%45qgVdLa}=5xvm6E&iVKo-9$jJ@CHI_viaTf`}a2 zC!H2wcyVDVd0amU$>&(FZ8pn0bm7yth{U7dH)`ef4)6r{E^wmO*t_`0^~*QG?-S|8 zt!lYq{5ky*k?|Sy{uTt*p8hrX-@re<)$DYS^+1t{800m!H_O^}@g4X@@W-3w?hZXf zuY!M;^{sNV`qeJ|2)=?Gg`Mqo2XzAEd#oqjAaRXMBJF+c79{T|EPkbe7-PE;5S;Q~ zaGL1Q(r@%{&}khDI-bPX!~GUGz-0@x9Aaiik?BxrHq?#(PnWnI%nYaReOv*$ZSm>?)ctla|1hAG;T1^YPnfOv{N&_J8ekcvoG^Cm?MLpzb znO-8ASEU{s)D{<<&JKz%JjTWAM|EWWzjHMajT;ECdZ?bsKw&|^tZONrkvOuIkI%De zUYTtG_26}0jYK0>4*B1QgBuPLXU?}t^*TiboK|r`R>P0_HD+(cdi{Ze{FKYDLBs0R~?v6B%Rx~Edo2aasT@IJ-vtfG(iE^ z$Awv_E5l{^C4s84a|;3+->t#z!u^V-9Nj!@+Ph(RslFP9tMyA^DCS*vdNzG<@yc2l z`u?ov&H8>AuC0gXeBbj{4$|U#n6XQ^x*FE+&d;P>_lp(J^Zj%8%oMl&cI_ZN6TKO{ zkvFp2-&{yO{TDd~50<`txN&oc<4*8TskuV~pXj~g5i{t$k=GYVU^@bQTx>a5uvcK? zADE$i`dge4A3((KH9;?{zv*7K*f>Jt^humckR5yQeHf=xv0R7Ti)jP&N=%#Ng5wPM z_VCv|5z{smX^sCCV+<0Gsc02b(JH2*RZK;zn2MJ0U5I^E%r-TsOdYDyD_EDQO?BF) z7OFc*Cuk9TtZz5Uo$8RKb(q)a%}C8|gD__z_YMNoV9|<#sst6tUZ*|mXK52w&tq|_ J6Wq-M;U6NW`v(93 literal 0 HcmV?d00001 diff --git a/docs/api/source/fonts/slate.woff b/docs/api/source/fonts/slate.woff new file mode 100644 index 0000000000000000000000000000000000000000..1e72e0ee0018119d7c814c2097cc60c0bcd05d84 GIT binary patch literal 1796 zcmaJ>&2Jl35TCanZ=GF;-Nqj%Qj#SmwgjS~vAs(b4i$%lc7v*fuR|jhlemtH?Nqf< z5&;$c3y8RZ#0{p83^QtGf$~^ZU#vdPL3b z4fcBr_DN>rKz!G#?&4f~pM4ZRM6a}~ts1aTadwIM%N_dh>UO7#K7YRFXF@YA68l_? z@xFm7>0K?wZ&VUvut!OxMlIIQ5*<0_&Hha~Yl49Y@PK@!7+CqFG*;eClSR)#j$=Xw zNnrjF9T`VX|4zRT99<||DqHk_nzSa(NzO5voBad{L?lOWoE4r?ZbRP(V_X@TZL>{} z(3A8mk}l-3xojrwNJr*pi-lsLQVxSKC{0w##ljO}){#>poy#tYg)pcTCk9|L<3To?f*omEO$b9ODUa}gVj!a zAvUB1l6OCpmTg;7PgnO)phbF-Xik@UVo+OLa3((}zVs*;Zywi?{r}GOL=0{q1ou!q ztD%;HAbGE?Z5HC#RzAMSTXWiN9ioS*i+Usm@#fI}eK@$`F!8DQHtAj`iEnm!UKH}P zNl{d*%%o>TwzLq6ppv_9BR_a$H<|Q)z2RXkyY6k4BJlQ)o4+xU@=Bif%MA~%sib$? zbw%hV*Y96nzi0MvpHdWZeO#D>x^i4rP!XsqKRYk5@ZB2RF5E9QWp(qg81F^VmBvaG ztu(Ggk(kS7r)DyTm#?ozQ4q}d{!9==(dt@sJ_vk&`7k5ChZ~1PD=Sgs?%Z@HoBe#* z_k-a4JvVKwyWWZ=q2@1#9uk~9EfrHBtA=!8%MC` zIGu@c6SyDi7WCym@ z2-w*rE$c_UfviKEL=Vy>={0bo+(i5#P1q3$6VB``hVMrCzU@B6vbqKcPy}m-BtQn- zkylU>byNe6DE>18g5d9bm%bZqa|qF=7&3(|U!AY)%Yg-vOsqKIq$w0D$r*3}ks4=+ z>U6?w2xMd=B$;zQ9v)F1K7KwfQ9|)<9nl&i?6-rchs{I8LC1eK^@#(|^QwB-{VeQz zoHLn@cwG?Yo(X!F(Q8iBOqSNs${h}C*?qJYLuZAnVXqx{ww|}Ux0`ca^V#hz?c7{r z7ldYbmk!M1m3G+IYC6(!VcCzqp&pJkZ#1@kw2W<^EJMqOM;^&dq6ELC*|pK!(cC0< zbZtF%BzLsuTw5O*+eg;dX0a+hcfI=gj8a~qWYJoMamHAo)!Qq|6&~9S8`{d0T?L&% z@$R=P6H0rTp6V+-Dlb>EB67JEMQ>kk%`y}{hvBC(RNEw)*%|U|4>SL+B=3rE@y*rw zR_bLZr&~XN?C;+yx2IilcDnrb?M)}An;DsTe@^rq$9L-UWNqY_oTyWyYisN3CMIh; z*C)b12exYgK^2c4S}J#2YJ+00rUlj%+LqQ7E%lldEc`K|1Bky)iLkwF{Wltvj!{A+PBEzhE3BX) zFBgqmj@&z?N1*=OK?z54T7is8FiA(N66oe`X+cKl>`n&&000000HC;3 zk^ug-#56-JW5JAtMV6Rgj#+|9A(7;@^`+^dWy@Alt7D86w|;R>QMs>d+47HJVfKhH aD;vc&%m&dlj4($-ipx}q&#}A+00017%3vV? literal 0 HcmV?d00001 diff --git a/docs/api/source/images/logo.png b/docs/api/source/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..68a478fae3611d878af18c575cf8218b818f41db GIT binary patch literal 22317 zcmeFZWo%qs(JSi6L=X^=@bIwUZ?M&v zZNPsZoz-O|AgacRjvye!Amk*)H9a8DIuLv`=hi^}_8jqv6)`_*XrLt_m#|ma7;>~> z60F2sY|2HXs2Xt=4xSo(XIVhSu0bq0-%2%$C}hRqOXwn)V7LbJ0s@%GnF5iQh6Lo! zK`)u9oX+O^<7uf(`_4wyoDb{Ho#x&ff)73yy3`1;&|(n(ef{5Wft(o(Rct95!LU;^ z;K0y(>Zz@1zM$dPn6Ky%kpH>@qoe|`!#@K#he&A30W7gQA*Q-TDg(p9P9LDiApUht z<|4#g$0*-R0cn7NDZW&%aKMH5$kWMS$^HL;WH|(a9^um<3zfb~W5Yg$)E6qjARDWf z1o+bL5by#2x?Da&6`=p3ZHux&T5?FC33nk}0?~K_Lg@K8p#FIkx)GG-)yWXS;5qfl z7gUete{nB0dh`L06qLbSwb+L8M0+UL7X zf;G($bI@tNsZM+)`3ixCOrR*uh$Wz{DA@Wxt5%i_U~~owWj3T$u*R!Eg}q}(?|vIx zmrBxSQjWK+Uvu5PBlDhos$~lBzk&Rp1EFK)$n#|}MF(%riFKr6 z?LJ`v?;AJ{g`!S{P4Qy)t_ashA+KLW|2pWh7@le%B2CK>@MA8m;&4WRJ&pa`YFEK)l5}mDXvc0U%zY1zLBD#1#I{kZUoALn+>UPAB zkh&xqQ+hp>cwLl&KP!w)C}gV$b?wGHLYSKDJUA)dYpHhaUssu_THVIIBUV*J*DnBB zj4}p6|A5W{Jx881jj7t#F~)`uIXcWE{S+JM$J{UFB*b?o09?)N*%4^Rj{;qoeh4#O zYpC1M-xYa9wImD1`ZsC=0>CD+L<4q_6NXp&lHLK=(#s9!!V8BwryMp30J-5;2W zQjV-KX8jU)U+>(;dB)jqBx0vzr-v9=V1zn*?Vz5TMO z5PkZA{O@6Wsu5I!ODMr%3b{0H>Oq=_HY6g!)3cE-3FJ4;rW(=E9W(u86-6TK<+lU` zR6nMZ4hBNN|A-DS&hP6<7WxJYiH;6kQk1EDz#4z&O;8HI@+S&$??3_gWCPHBU(SA2 zd-7bm6&zHiexU@4cVwpxoo;l2Mvv#pReT=S+~*SByEeQpI^5|+KB7Yex={t7#4clq z#KA&}2QaJ*9KI?!t_?+3&>5K2brl0M^PD5D#Rn$jAT0{=leX~LlVdvqZGI0+8yXt+ z_`ly|cXM0MXS91<G9^$dF@5suLel7ORKBx9kF7l=!ghZ zH=&_yl^_sh^s!{T$Z{ye5^YslUjuA(o>;GqXdjlD(0=Y&mieiKQ0unwJ8u85e0#nd zXtG_={^mZ*m&;Y3fQ}-^_t%8+Vmh8f!SUqVuz~aFo2;PoDvS&f-(_qZoWhC0!A(aL zNO#ue{sr-B&$_071Qdd-sq4E18{lIZewV9L0fj39_e}oez{p9)HEf z#liA^StrhM@HqHjjBbGiWq7?eM`Icb5C2HxfD?A>fWyeh*hba4-r`iagezquw-h52 zO|te@R$evSydBQSp{}9vwj4Xh{XXtusHf+3bTc6;+G)4eIz!TL{-{AjNA#}x9U1;d zDg<^E+Xv}evd!RteXF#W`!m5}oTA!VwWS8@1*yhee>U)Y-j}Vbl}LGPK1+k%-`Zs2 zrVtkp))ICGqSHlQPV2Dro9ylcM0&D;Q@=IA;|Bac#{G?>=NE!X%rNKV?#jr-R6*_? zx2n?L*C%DAKAp?ISSg^qPoi4Kb!+l!TsHm*8Eh^1DSOV_EkblA)_=O+O?s7Q!D@)j$ z=E$WLb>;cbj2^G;vde3Jw1~i`O1`ZWHK^d8_kvAm=;dhe`tMDm6U4=w#}Ejb&<}HD zdE;i~)%%i!IwQiu!Zzp82tZ%)U#8UbL`z)zZ-;2JiO{5)!U!SE;pm9Qg&bg@&LqIV zYx^RTNA`t$b3SfrcD9$%IRqOGt#K6y7=dXdx(a~&u-$SOWZrqX7!A)st~9A&%4~{C z#OILIsuV>ku#=_GZdGL=xF)||P5(=N6+pa60Ph(|B zbXUUh@gV_;dV(&zs}7$8=*c=PmTjXC4-ZQMC@wXcb94W^_Td?4Y`-amM?{>wAP*H8Xg`_V5Q41KBE`GV%UnkTttW_ zRx(FpH(emNq+{&sDyp6{yQaON9y4d^`Kg7N2HpwB8*Df<_iPGrS7|RDnZ* zhPY=;@eIgeZ#dDo`g3hwO$Yg;0I6F2;pe|ggGC`oz#Xh#$;-=APn-A3fU8w!m0yhL z=KOBoaW}t?MXO9SS*}xUs#8RO{{>K{-b1Vm=BTd}o40vL8U&%Z&|<$oK&lJe$1gZE zJhV6;Fl2e|r&d}u-HRMlC9kfoX3bi~@H=e&c0sE%O6Une%QkMXnwzrkd@536xLI6W zl(iZ=bRS`E+NGkUy|Jr%Pa{(2Q+{Mb26KMkJXS#Wl-JKtNa*i0Vu%9@*pL2(lpf&h z={|#uItKIG0I>3$#`sigGH{=Bp3Ft?FeS{)%p3^&J?rJ63-m^udfYguA}=5AcRX9V z#Pv>{w z8?ERqlyp6oZS_%{&Sr{LPK6&<<3Ay)h*|+0zwMjji%_;C%PlQ#77O))OF;=qu%|=C zCPe-7fCQhXy<{3alJ=>M8e+q!;sfOrw%moYrVWuv zo*k+jCXEN>53V=u%7Odwd5%0eztO?`2TusL-q%kNq2j*S$J8xKF(subdM)15jpH<0rELS{t4Qk95005xa0{p*g zv%lF*#~O&PkV?(uic6(%_&pj@`Lf6YAbn1@oy)kcxuEi942im#TtT$VVS}#?=0pyg!`E z&oWX{FkCx79YqLgq4{E_E;823mjGtwtp)8WpNsa;QUN`h`0+c=#nZTnZoxB zn{<2p2^?-#Cg~t$rP;S`L$w)8ZTSwjA_owuEeSON4 zRz(dYSy^UaJT7_sEe=rs;tH-6e^7pbOw3iJn!+pJ+Oia88@VzGU%^_bKwWB;2Behb zH8K)2GhSH)25#98*u0;%Xn1-Q!mh&!^ab|F+xWDQiP3OF(>(XI3l+K=hcZY?Nc2P+ zhELlB&|*2h2++61_8S!xuIPua(6UR1N`-t%^G96+E7KEM!Y^3so4*FQE>Wvyp<=`n zoncXmY{mkn19BW6kQ)3FE>Dv)1>Mu=M1>mawf6f7?G+D7D}BK=-Li#TkZ{AKL*{g>VLyH9*ck@dIniuim6Kr~(yLY{;+0n|h z=JavxspU%-PLt5I2U$;?ueHGmdG~ND`&a8C$cyF>9W#{O@2 zx(EmewUE86Q^@gVdRli(O4h>4l}we(@xj=rc3T!Rwe*1rI6$&V90YJohr1I4?0CVc z*+)KX<2WslHp^o>9tZ>m;9kfpD$ZWrUs1|LjW(YGM>s67>gX0cdR)hXcr8z5@W=dG zWY<^=X9=bytSwB@NJZM+$yOGf-;a-v2})jyE_={KnUD)``t+AG~y(h=;LP4J`V$893&+AWssPd^Tt~!8U;91*AXti8!P{WHG3B3 z>88&){(i3)Yf&MEukZApWY*d{^?N33c6+kUAM`GC(#LG5b;%iPNk zD>m~HbVw)@7F(8GLLx!9r&e>1$QZ>P&TubXuE1m9b_siCv^9mEHEPUE}dU{IU=|2EIKN%_2nF{pnI~wqo0_zRDfDxjs zru{o6+Hu=gtazdZ;9ONt4#_3hNNz&~1BT2mT0}xax!0>`|5bTIRGg}FTTi9(jAKgy zYuE`dgvOI#p2)gi6FFMqJv<_x*pB8a^{E=nZo8iL)8SN`)G{0%94e@Sd$jPc$PmrJ zsl@?3pc@0VEEEe`2%RiAsHwobuiH{bb6K;}VZGf`D5&t$$Emw-`VOPk3rNuuIim>d5G*_LrI|pD8by2c~wO|k(mbO zLhDip_fjPBHG1ay>BFRX7LURx&V+-!6To84SA4MX*FIu2G&G>pV_J}3*khL6aklSy zUT*#gbKmfNfH;0X>N)a`zuUx9l9v&)&h!X{8wZ0<{O{bMxu7AzZ3KAkc<)a7?)hb9 zW%q^2M$1M91_m2xk3GamZ5$Sr(by|i`CmyrBuo^&CXJXrz#)J9o!$v!B9bpw9Sf`# zN%;*AmV3|%{&HjwMs0wY7_{us<4-7LXO!ML0aRiEm#hcYe^u+P=W!7_j>+S;O>6V% zHp*UJUcbF-(RJ*GJfcre3M|;H|JBX?hX>N2Z7PXg`_8UJ2h)qE$P|qPUZHG>?elKZ zE(Dp4sxlwG4ULTvqaPYE@$eM7_{5)X$WHNrd=G-J1I5fKHm?8+sfdB?-^I+S#N-DI zh9BbJ_cPlJEFQG6#?08y);m@-jg0K>d2M8p^N%XnzmZvJYHG5bd>}657^i$uQRlJ} z^zw`qML!G&hm=cSTNH;oJQzfmiitR;v@mJph~<-?qvZ)q^?>#pB3Ng@vKDXaoU6tL zp-33H0^GHCQyy!=P;9>_Hq7X=VWaw5<1-zFw5xS=S z5^l&#Iq&i9-qv5gex;8Dv8g>M41sGik3Z$8)^_bz{VvOfJhb3ehdEU7TK$qVW-lN&M@fv3VUoU(2aN(RP#k|5y(+5B*y1&$@FlpY zvhl@5^_U^n|1ya5HkHrun{rIsk~ZeArlI#>?8q7MKyk9aWK0PrP4k=GB2%Q*%PoblEa3BaX-Wbw-!^47EjB5(!QLLT z7-H(_(;t)wTK%RRjt>!lXJ!;J z<~#SRT7A0~>Ye))d(y>f5)Edy7{ z24DwD0!1laSah+LR8x_#r$BKiX53R%9*nAH|Gw>z09uwml+0Ur2}JpVvh-zC9PyTJ z=}W(x3(6!N^?#JEAP9OCUDIr`7lb7*;QFZft|y!>va~x{FaAHbh>=0d#vV$$F)T@$ zPQ(Ld#N61IwgP)$bpM$T7;16>*vP4;AAzYN!Q}rXMF02a|8WaUrosV=^wEBN5?~1Dh&6q-7)(?b zdLlUg>R6_pF!C)uZGHa8MQa?vpkSn)9>}D2)j`NGVj6dNZpdJ<@uAnG3H~fN9UbBJ zqYNaEDuK)g6nIZCJQ%^e>VYYilL(!L18^%H;@SXN92eHtcg~bv>zy->;%Peit5qCb(H0tteH^?jEtPnOovP0|r|)q?s%tu54LyHQGB0rX>><2^)5kb8 zMFy)Q47i$M4O|PybT288L{MDXTr{DIAF8h?C|tJj(2^QX9j&XG!0Jj=37;! zmk&>xrkv)`$u_1K?;sXV?9>0~|AuL@ty~J|iA}O5IKFiN?^bN4gEQyGFEU7|b7*WS zWv)~!ujUu%&2MT#jr9AJ`cUw*`+3xHw=(u&+OrQs>Isw`9Ya4>CRS%-Zhd7bR{>(L zWRqE7(y6EQEm*Te@L`s-Uxu@+Z^YgouF**nH&;`0!Kax~+}JuXzGD7>0M7qair~6e zr}Kr*TNV0U$`D!jt?b8J*n1pRUsT63>Ck=5lYAU9yC0PeN8zX{HvMvXN!1XMbf5eM z^HI#>95;7};sCPTPjm!$7Y?XaiL)! zO?BEeN@I@5$xF(jwfQ(RH_4HSV_erib=f_%dOC)&DI5XjBhX)`kAvWUkb(Wf1J1{n zbOB-k&=hiklc>{@I6J&yK%w78xpC7|V!(|U-++W@(gpeHYHP(JJU$;iy(@KHlCRmr zLw3YNL^=tKSc)`Vb*3ELmx{#Eh;X%=Krf~XV{E|tTQU^^M;?G@JZ^8uPla|qJa7%u zL|$dcDQW_rJFB|~+?IT*C`{jf!hS^y_^MI-gSAmHYzF4ds4hO5iI9;%ziuKiIxyF z_<#T^@vYw)5K!cllab#91c!873Oo{Mk2&qG&1;#A9fL>w8}g`3EnV!nCcm2%uqn}B z<#?o&6RsvCIP76ufeU08mcLB3@Frm=2oZ=Y7ygid5--;!&7N$Rdya*+#*!uB#`F*v zXL@w+IH46RiKx-ZPkp6x?iXj8Nzq2DG*V63eB=t;Z>1q7w;%if@3VpM1$p4%4K?3~ zoK|e)#_k@sw6vXHCr(jDUVeOvFLE@P>V;?Nlct*YdE^!DSN)eMj&x?PciE~U+I&H_F+%acuxcL?>EVa_HdG!%8tTPHjh z?qHA@9tE3T+`Yruj(1c+ln!>LT%HZJo^AqmOy|VZ(Q8zN&!pz!OSALf+NEh)ezoqZ zoF@Yhqy9@$x@?OR4~+(mO6;4XOs4Orhu#qzyaV7R4y&bd2(~+EGnmVvB_IqL0w=#S z!Lvc?+F2GkA_w-=R4_erXnAmlt3Tsv4HK(x6_?hu4`rmwow%8GF>Tmp2gX;#dxV$& z+@c41Gs`FaG%t4Uj@!>XPa^LjP5{d%s9q*9l~GEYhy(+wiAiQ}%V-qkG#WRG@uj^L zbgtqP$M6)o+2R%qrvP6exxV9fg|oy@YsluasAbOa^c(?Wr<#z0o0j@a>FbaYkxYSN zgW%p6o?l&ZQ41b9Vb9#6U)rr91fTHTfi1PHiGH{RHQz;(OSeDW!t@Q9`}CKa{-;lJ z3h7nkipoM#ZYm*$;#z_e4lrO@iZ&2+seo`CiqZ}po*I%MLQVFn@KUUoqpTvGyPxY( ze!{d>Qa%J*&VPCx_ zY!bxLDtwGLIy@ve|B?rU(uw`%iP)kkOL!9POFS*Owz^SCJ7xs>%xrBHdz(+KqD7ax zJV)iea5CGjpMa2<5}~67 zw7y)45kR-{fZt;H@}7T7P9qjBgC#X&h-PC<1@l86+AO0;SgzgvJ8o%92z;#Q;F16};-#>7 zq}I~XGb@ixhj(p%sG51Ts}L98&K#kEJ+*c0RJP8srrxvUf^GW)Bna%6%WNDf@!w&; zze;&F0#C(SDhd0BR8Ao|!3KQOH&d>Ts6y8A^2VVq$2*I~EPFKAVNcz4 zH&|Of5lC2FE=QKdgBD-}FX6Dl9rfw2aAHs}wQaaUgkMuZ_pA$8q9=%1wC896g|w<+ zhjiVYLN$a==IQBHo%>X(gbL*BoezzzV#k_V>xSJ@?fmITz3$&pJS{4edoF(K-uqGX z#zIZ|^39lK{dnX!&g!NExKFku!X$MPGxB43PdweMj+t?q zh@=!&M-AAwAx)>zq3)7iYv5aKFnA9$KoC5ObHz<4C=vvlY99aMLYrpTlIGdb8QC-A znRSo_4z|xA%>rMy<5{aB?^*hF#z8yaO(;g_RU%1=SBD$-e%4~I#x8BXwrCJmohM1k zT`{MZ3=Lcau>I^wg{u<#%@|0CdHbnc81?m*U^d@ePw%zTw&5^&hGUHjhv$#!bty`#Q`pGpo(C=y#qLE*N>wB^ADHMSsf)hHjJShQGd8P2rM zujc^X(liW!cz-qAq{$D>rJW2aYMu@lPD!b+5B}4)E~!6PyhbvsgJZDk7hJ|% z{Vb+`iOFx=slGB1!ibW@?Y%l1C76%8aX|Sq#v>$NUzQ_ORxAez6Ffl}dx3Bw0;t*L zsTjJE=-BNX+Ls#4naW9>(W%x}#JJ43to4ZRBP+f`GRK86O8$HGy?j5`xIWL$XfD>* zE6g5K@Q;~ZC8{)lMC6`>27W74YsqZ8jMr+x^pel6@(DgKHd7SaY(asklnlyA zZ2GT$ekmVNR0hP%mgE_u`K&yUWjZowWJ=w)ANAp%KH;Bj{Qo*Z^538TA1vS^0p&zH zdh@_CLP{_1qAva?-_gsr(t68kY`-}pVQ>cjgN#4Rh*Lv3=%>aV7UxLk+05bbmJ2a8 zz)etN*?IEp>GYIYSgtHlI*RBumFa=+1RKUMwBBm8(9xt{(dSbVb)Q!M!rjZsLRVr@ zM5%H#bEm`J-9y*5!uobK9Y#7T&YF(r>*l9erMs{OW@foCwc#V{>ccuVz^3a79Vzp% zhos1duX(7f|2A}>!W%nQLte!{2V93<9#HFm($DFIh%We=ByP^yyTD~CJjfQr) z)Pg3jMx=z|@(x8<=~7B*Lrw-XZDaPrWX}8=Y2M|U3H!pf3j)f+q*zI#uso3_gsFjU&}E@ zx3LwU40i;v@$AHSt2OMim8T$}RI#&fu2t zxVat+p(6!0#<}*}{x%X%G3JNk7e*Dc6l>tDo!)^O#sOoI=%Wqd%Tv5jduOe~@hwRY z;K{BnZbx>62_HFHvQ_(5@2G5Te+Eg^vBNRlf%7AUI^gM#lVhc;fwz3>=gzsu)TC?& z&fTI_hKKBWaRb3?dV~6CD;hn_cL((l&X32iBiEz(QMba**Hrn+&IVo1tbmQ%*NA&A zS^QLct~9JO)GAtT7h`EPlB%o6ZoeR-+qGZ)obxewg&`zqJNKv_5ed1?d;QhAG}MhY zpn?}a{%P*Q1vG}4>HCk#wJ)JAGu>NFqIVy&CkhX0L-ssf64unTlP>LMA9^vyF_Nt< z{7ofOc<<-m>Q@4BEIj*PKKCLk#(OxwOh*pF$4TF4KHTUo%vSV~e zId{?uBI@Ge`Jdu&86f6fAz;P zNY(oIeEu#lZH21_<6`q@Bm|f@-p=_pZB_d}9`I8RdhB zcb106P@~S-_61bx7v*_PtngfEUcc}9nA23`$F5COJA>1zB2+4pneo$+~$CPVP$fH@OP&;rBkx zOw$xNl$k61_EA*AN5%^w8l&TQjjYzi;RhbN$H--)ey7jegXAE2r5m{?Y44vJ-w5wF zTrcvQRFkDzB$VDms7panfN~xg-OeKGf?oDyW+&w-FC1hn3^bhAgl}vG%T?*})bx%x z4XCOxWPK;)r7(wATP|hqXlm5#8qjEGu||#b(k4mV>UKlTf`&<)N~7E9(w2)=I3rE{ zja)Q@$dOBE!n?wj{N}ID(^pDK)n{ejkMXI_4moljzx|SStHXa@7vTZ%$PGKwMIy>J&1*-zlt8DZ_U%YYM6l@w3f-~#6;uhf`CEW-QCE=3u{so=hBoKz`5G!eza zY594&XNZ!vOJTwUX)P#p4yM>#i*)ImM$TQqqtrEMvhq&$2ipDNvwxw-tDh)zI!y?Y z)RXnPz8{ZLVwkr~v2rxA(*Yvkr?nnE_N=PxK5ghAp*JI_QR6li6;j?1XfligFO zlO5FjMbYiQ-UF0Ot!*1$*AXbQN@YnKCox!3^$iV?{lx%A1#BoJ75g4Y3%C7fN}ty) z*pP8!{ncho?fKE|{tyvfH+9i>OG!#Db*|s4YyC-2<95;LtQ8wPD9~okblLEaEO63o zH9jmcARh^iUI+D@hWaIUnX=vH4?m2c48HnxUPWnyidsR=k!x9F6= z>MFw&X6SSXd+DLKL2yez<3U2kVKiH{ZG0R>(M+9F_4kXvDQjjEnyJ)m28fI%z z87`rZpr2qqofz=$zt_*<{0Mwnw&I}LoBbZP&7di>0Ah5$xp+hOL6uPRi=vFg{$|Zm zfxPo`y}_Wb=4fA>{pkqy)2}}2}DU6Ele*Fj|NLYkH5-km#hBvOwrdJkUuyVg-$6YfeO`kzXino
hg5Z+!{Sa)O9QNxi$tfwtDZos~b$g zf~%$5p41?m+pKz)p$R;(OqAz%NpBzC-Bs|s=5}Fzx{?20LS6w?=8B26nsri7pgQJ;3C5bj0$M1LtS^c{;>_#l@s`gohuYN|zamk(kUh|tL7}g=G2^;)T zUeU-G^*(GZXbEnp8=VhipQGRpEcT%zP$hEGm7S_jENi};dXF4YE~JMgi|Exc7>E5T zb4#?cV^K*FT4Iwbzn+t~$3Qv%rDtC+iB%YXhLdUC{%$m}ruUffNRcHVvPJ%0=IQ&k z9?VV+SK>;krY?^g89zh1#h|K&9TQ zYDRz|t5`ubY6z4BGJK1jhHI0bmu*XuwbnjvGRn%l8qbMM1tsl`tetlhb0vsl`B4); zE#{rhT`06C@8aH^Voznp;rm-!_ikp7iME;3qaYW;HsrzZDomfEQ03=W5i6|cQz+s9 zUo~5bxg-uq1fnQZB4>(Gi~Z0P9(Ixsrn8Ei(}Co24=Cl{pIr)Td?YnY;bD5rhS-JEa3%c%9-tHAd4-hKh&vo)8l%bBQ7v$7%XmKi0jYZXS@ zWG5JWdM#@s)Ic!Ae#4vW`iIFT^raF1Nx$Rf`Ict6!AUcf~GCWX8>1OEAjAA z`4e8So@KJO(KVab`gGRsv3@&dGMN#R!xOD7NV`|2Lv0)37@e;5$M2#363V?ojwG|O z47H#0L!+;=-jO1{eeJZ}<&2BPL6rkDFF=G?UGlX-aAJ+vx-E2it{dVUgGL~rG zoqj=Yk8m!7&==0dFS4R4l%8{68LdZKjEVSq?lWyql9-6y4?e;A>uC}6yBJfb^$n&z zKFSJjT=Z3!w`$NJv!t`4g&DMXL{rrQo3G0EHzvRK_{6(Z%KCTbgc`@{r>Ceo?IMe+ ztO?z8wx=wt6}f~-IOxcqmheaPack{kaFx@J_?^8-VyxM5?=Lc0;fG%y_&r9=GfbsD z1a(>R4F%`D-Yz?SJ)Avmy6@QYc3)}JCn}v0yKFD0Rbe@_c=#RrDFa6SufB$fRObW8 z6xx2g+F?b~*HSM~=kZ|JpkohZr{d=~z!aU4HTDAccuBK(XLXE(aV~03UQe*G+pi|m zRJp>Dvi3lqrAsowwVQE2vp~HgFvkbPC-eD|1bHUrkLSsjD9AnCgg3mXpA3IE*2Xy6 z5TeV{|8cDg$){51%Cm3(TCT2ZW4Q)ouve(#TJS{2Sd3!VQT8mj|{J@P@B%=9sYjut=gZE|TDiBJ<7@;(bJ_jQL$R zz{mWyO6!xn`^s)!f~bz(^O>pv`_pgeh&ZS=}l+4QRzk`0knS(nO zv_G#7s8Ua3%E%js)N)bcxm*#J5P+bQyrM{{X{|njlk*=GXnxo@6kllqaKQ(JQMsN+ z)LEH)Lp#4MX&B}7*_~I~pv>42+p> zU08qLH2D*qSwnLpP-*#v*f}S*B0}RU;2Ny9Ge%{S78l{D#ay?}VhHv3NUi#LrnCml zejqp_Ns9lH$~S=dic{?GD$MxNYUGL=27|uZr_2wQZ(Na}#3F^m(MhYz+8B{*cpya9 znt9_C+hHUY>t@GxY+0i^{4F^2q?CNxioEXqcc43_Er~|;ui6F|_#p?9Pa|rX8U)tl zB-}$94~EY2_pliK7EB@(;|)<5v%+MnGJyVfjSR+Tx(KL&g%F;(sbALabvd%Fx-L2> zhvc@d$uR|+8)$m9PY1Z+*D}<%?-B)#R;f$Vl?+u!{QLOwG$K{!v}btd$0l^wIZi}`SLI`KAoH@odD&^$rO}l;`p)v~%>u)=^@wgP9jpTj1rJ&4C*zVhaEY)m{6hrPlLzedM7u3~+H}=A z$DGFW6?~3xP6ckG+_6^x@#Wtafi z?WoIE9#(F>oJ-^te$A^BL@BTccaxu(Gky_}*1NlSc&IWsY8@_Bwz8yGYJFJ$W&JMG zO37l#o&NRoM@ZJxF9kK+X{T|sci@T<_7hac55JM1(?gh>Q=grz;y%0vN9!GP#tr|b z%WFvf>Wr!gSymB^+mY})>6P|(?4!|n(Kl8U7@sNr$jubKfhqQ?ciD1q6`+ZH2XQJg z!~}c+u&@o>a}bO8y~?%h)>L}`G(Ksde&5&Q-LBe@EwigmSiFAg-LO=OUu1U|rYvRX zNm!KU#rK~%NpLC%;3d*5I8yGtVKCDg(KV8@?JwX|7(czwXZgZ13EGtr>sWA&=gi}#=l0D$Pt-P*!Rv^?g8Wnm z@e$FRlV=pW(6Psr%)G+f6n0)`$Pd%nTKftW0NLo|ZT;aF)$2toqs2Czshtn<}<;k!zWe-PP1CJGTA%>V{Qe+Ov3%>Eh$4HPI-L zcjHB#);{O#)+b2;vindYsHG84H6XPW*#jJ0&tuJ`V4}21P zFORC8Pr!==fxDCfX8`x@Xsb#d$FB6$x`<~~+DuXy^s`4vaWq`+=Y&b*{5VRPL(y^` zAtQi8YjLfHouW@W=`4Hs#N3q!h&B62Wh?4u-fLa;#asB*$TopG0~6@$xQd$Gnc@(K z@cNZct#@`oe}k`6Hi$VJ2`gMEwAsOi6uk3kNqSDAp+s8ZuD@yR=I{AQ%W7QRI!E?c zkN!zj5*-6WgMPe&l#silVR{CeN(MBOw*K*}cUa6dx!jwX{Vg_ao6M@`8f!49)!rJE z&3CZ>s$2iv?D$Zy*t(QQ4I>ig=g^XgT`I>2$D-zs8`11#MM)t$nK_Fmy3BP-_gFto zN#;t8E$c1TsYx59$YIL~O^J$cn4ZlUW{GSOB29#LHuEXDnAP?yT#6(3>n5iaFciOx zWz-+7wmEHgVCe4r>O|d$m=^c41Zlfn;k_*bS z@|PCG?Y|`~4hah_00XJt#m~!8Q+eZD zAy-VNpOZ?3`&#>32?jT^uz`tAWTY)%drA|N&;Na}#z2RS2lrdcM+TtcME;8a@A_v| zA|cr{Mwhe^ks{RnrY9RRAdvx;SBLSqQk3s6E8M?xRlB;w)xMV5k9smX2>tqIt zo4I@Vor{Oor#Iifr?yYe+?kZ|3|{xQN>u~lsaxi?oS$FKx`vfc1!c_ZZK+-7BcQhr;-a}8(@@73qbT3kqqb-yE zjQ{Z72Ni^&H7^GP6WxDwXQ$x3kT&MXPaj*%2^;mEac#&vJtH)or}zJ^lN;!WVZWX! za9Bwb>?PSadtMp`HKANe+H0RPw+8|x1-luLYMwve_AINXd(jx<5==(T{tM&Zo zeoObG=bgU?G&PsVdYP#ornW4drPls%e9P@ya3f*Nc6nbhBj&1!#(w#X*2+#UQ=-ynh31gdV%&hvbKV3NY)9@-$v`;?_|E zdVP$fT{8KXZd`tknyZwn9Ilkxe^*Y?)sDADkt3hLBWGj~v6iP%4`=`dM_r?%E)WSFj^?b37B0aT&T#TW2vctPUCo2;7yw{|f-~D|fVu z=(^F}(SAxAX0oq1(X<+dE6xl_^D~DoiLG&!VWxjb!AmM*Q%<&m*6dAFh7WtK#S2o! zB3Qrgt?Ay1Y+xu^ITBExRD9}(D$hIIS3<29mA7LO=3G|Da)w`jYEC$hJ>a7{Y9)Ge z{!rZo!l}dtrwNSev>*p(;rX z{=3*&okSZ{ef{acKi7TS0?W+Tr1h1r*_avKClr~(`@09tHOW$>^v)MSeI@t^yp<+j zQ~mmvajMUp?_O5=gQ}m^dH8-gT0fa58jq(lcb7jQf$#Bg>7{3O-{Nt%-m2Cm-_;j3 zebwmN=5$RpxJVaR``Z%?m%_|)?0!3hAt_YnG4NSqF}7E&!XAe8#$71bV;I(Nhi)qJ zjL`cHR=3ZTmNo+-nQMoncao`(WoMss+S;p4>qvBqEAu0v&Q8%M&OT`jod;{LqwCdd^}0Gm<|X;n@JhI>a@OSs33;0#KhMLN zkvYeAz*1RHJO6jK_@C6m8A49iZP<;$-O?2-e8T0=gGo=?{vD=V($nX5xuiA8`2Mka zs#zIgjx!=SxL6%dGYeg1!Ve9F_#N#mzCZHXr}UFZo48OwiJj{4*Of}AOqrM~HJ~bQ z(vIM3L*=I=GzE1K@rbr<6vu+;6>4sn35&jI*xcU0my{G1IpG#Xy`L^33fJd;ng}%b zk+*%!ay`?GB}f#PSJ3^H&l4q3Ib&T!mD1Jw_Qpy$p&Qae#!bDteCiCV%eO==9fzlO z9c7;OPfJ{h10DGq#RI4<&cSDXCyhkb&G`-saMq6_YhP5KJ(gkSjP3r2B$qMCJ2#Qk z(YuXJ19q}&=-jOQ{U>z!BXsS2bO#4ctdGk5_dheV72<@n!8Ee!H>^F6%1&oF%c;x^ z{C;t|&To@SX`+j2-kX0Jw`=5+zHuH{xIHao1_vY}aRUi}TB|BCL}csMA4!UYk65Mu z&cFU?vQSRSTg1T3HY_Z5{49f4CN68%#uedK7`x1TNJDpX{r&IBK9Df{C)8$&>fNO8 zIc=lIN;2n4z}*VnR-uP2Q2bs9PG8A$~ zpwNOo}sg^=@7h`m*!TZRzUoHI<3?aWz5+ka9%T6({?YiUA$cAIF>G9zhgFwl5R&MT<@7)e=i z-kzP*pttn5-J$2`Ds;Mft0P=_+t1}P_0V_i+;A_w;H{e|W`|^VmnD3eX{Kz;rVf1@ z@VEOTlzeac_#Vv)guV ze7UL5vm`1xjj2twRcax@?@3u_fcH>gb}CYp($L*}J*bLf`}t3LfBT^=I)H5$R_FWG z0IzOmLP(*G2K7gu5zViF2RZqk6CEeN^d!PA+^744&-X{2qKN$3B=T@7wQKnvass#g zzTGNBnHTOI124X`C@k7&PPkihGX5yj`7*eD=hm?90L5FO^lQamw$HuP~$f#RD!1d zYWECz{{3AKs&v`Zplg`Ydou7Z4unfW=#}i=xjrKMO5Vzhv`(mYdJkMTDA?UsGi+U) zedONW7exYLj(8xMECKzQSvFd(@^uo!g>{fb@7VK7r7$G(n1YQ0gn(NAiX4nK@Uve-OMyYi58 zW<^nFJ8kFf#0`b3kon#4iWx+srw+b9%);GBle^$rXun|9>w4m;1}NFIvYNJ#-T|5% z`0%;meMrVZ-_BZayOV@3O>3}Tt~(={(o3n!n{5HzH0m`coh`BVp6#!5UUbN_>BB@u?iD#Qe^=3xVo2*B? z#Gjfm&VQEBK548hXpdHYXo-4qB;L4qX_WW%g(ef_!6|14t75LK6GPVZZ-EG_zO|5r z`D31JWSS=yfBGmp%TX{*eCJ`rw=?PuuX0J~9fZ&Gf-974&eSXeiQ1%6TIhy=CT5=f0kZ+GreHr*Q`X#_I^ zAzBNX(vIrOsdz?Ai#(IEoK_8>MwY3#Vo-r&xs3&}aqg=U67qY8D!Z(jVsXQoXAdjh zQe>!^Qw7-JvoUH zud0$NIqIBO{GR<*ZzyPo-KvI%EAVTwu@av|d|s8_RWU6%WZC)D%<3$tD?N!x>1ML2 z6YKVi2OXqDl&AM_!j-oX?*yl?EL7?*fDDf%)gDQ5V_#`66K)DS7SXc$N+-G=rMgMw zeTFebq7p)7=*c35{n^NfR&< z)rl>OH3xHyUV1n7x{!8fS@zxXeBkwjPWuDQ4~#x6QAW?)@}9_FO9O*2PC5(NSK(z& z>mbQ-Ve?D4=@@_88X2_?67i?tI^*1yWZP%dH)!HYOLri%zj&@UtLJ-K(Qf0hl;I`}mk`cS_iTM7rv!79T$VK<^#>EDT3vBM4wt+m?%xNFUYL+BeY;z^jtfEdiglxO`qP;3D%0fGrjXE zg!D+t;{q@3Y_PUUtEqTwZTvoi&pR!~KmZt?Sz@&QS$E)6x|#U7FgF7$oa`h5s31M- z*um<^Y+SK%ban&x!!nhvz#PyK|8NHnd5ZqFQZH+;sD% z_hf)BFAOM3sSiOWtJaxIUOb5*_mazzNkcG8CLe)Gs)3EmRB@U!%bZDnxFt`P zLYYr3yn)tewRuw8r&=i|ns>zUF?3j_CdpQvboGX4s%|iRN`G8(p*B7Yd2PQjpBtdb z3(YfHFTI-NZ*__?Rg(oS@|kD4y&c2Pao#Cn2jO@dN>P~OSgSTFo?6*(Un#A*bZp9Y zxn$-u>0>2~I1guGKySX|pW(rz&!Q5pzl(Hau$&{5tMu~m!(UsG zaoeA=_h0Q-R}26rxt=M9p6|}Ndp?O9#=qYkw?IbGh zy1u!Xx=3wLx6FV#WHPRDxzfAh&$G|pWIF>{T!@P_)GAE+PPGfYywR@gT7Dj>huP>= zvYLS_25*0F!QpUAJay-&GgPFC@a7tWt1ZXXQIZ$8lBP*Ia51^`Z&cn%&3w71wwuln z6LmiMG7x|e?$oQ@#AfmAL?5+4(P)37JLE|hSGko8MSZ(G!Ijp9E=v{xY@1AmO^%&VnGZT__ z@V3ms@erHoqBQqd4_T%CE~pSJG`B~|{pV@Eh0&kpE75LTtLes-Z4SvYm8!2*hv8cq z=T9WK$#r3|Et$)spM~_j;2iC>h>`Ei`fzt1BU_8EKka(-$Y|I21S^djms3`PEI=?j zy>0;l;XeuYTuII+&hoM>eEFSYs)o)z4h&%&d+c{{4bs+5bjoGujmvWFV&lTfmdYwC z?t{RaYF*RC9|0fXuEh{x!Tt5T_9LYzDJ!O0ayT=2l=-lNke$sxs&`69RA6@z%! z>0OP=Rts2np+?WM6VthIn=wGn^M;Ej@TU(&{87&UprMx2h33srC_NKsqA!Pi9OCpu zkJ^%K7!Mfe?GJKkL!MkabaI3oWICL~Ovn|aH);w-rNS{Wp@R*>=~S(^xa2j&Sjuhx z_ZaytkNL2SxeWPoud%w_v(5-5#!Ia};MM!tswhWgk;X=W8&&d{=o(<)TXN1>hd+}@ z)lvhSYko9@M-m8fo0ObgJ0~x_cwkRTJP&+nKk+Lp<*-jZ2Vapz+ndl-IsDO{s;e`U z4t$%EfiN&`*y+VT>TDAzyq7Ijl7TFDCtY_s=7Qy~E>Wu2q~e)V(j-sxirjvUXyHoY z6$2}-*BcFM+ck3D$yYPod*frE-xKznDUFhj(uw2w;`cl0?B}Z?m4my@(z6^~STC(g ztdP{^Ys-QM=Rf+Us~_vO2i86w&f1bt~8EnTyzL_g--|{ z9nVx>=l1(~HqTB&DV;xb@Rv*nvmDJU%`33Mj+cNL_Tcg)k5&^+!^BmMar>?TUnwKz&qv9N3|Jj3c&L| z>*(keg~1g!gK+-Gj3d`0a|drt2Ph+1eg;3|FPtfATSWcrDc0_|=)FvILvY0ATgUcMa7@ATTWxOU493lGQc2-Atku>vjmO91y z!?u`)ck`^07!7MSb6ugaZ?8IoxoB8>@+E3+ni>#qudw&>~NYjS_f7N8b=8WsF zDjY}y+agAyI471;MdJ0$)Fi`v<`o&yfGiJ;mt*inKa0Q+E6VYKA0Hvc`;??*^L+g$ zzoV1Ur~^vFB*Ja1FnaN-y60k zH?lnLkM2i>xf1$Yzn7gSD7zdqHst2a*_tD^@~m@({8YWCkxT}?V%O;@V1L<3Y86L_ zo9<876ChnrXK9=+WYqo0gLPk=Rg zbi7J?5aFy~W~TUDG825HvN007Ap&>u7Ir+n`Kf>ViN1p;!_?N}9`H(FFo5GLjfsqR zkb>hKMtN(T{{_LtAQ>5_-r5B%>Fp#FG4t z61LJ3@5f4L7K7G(amMN3=!sU36fAo2^_1YBtQhrhX6H)S1+X*5sEHxPRQ48!rp{Vq z*=stS65K@YZ0@IZ#HI|c)`?yr2zZkY(Q;iu#JfQnm19hOBCp&#(bKYX{?Z?a@ zzEfQ>oR3MIJUaEGIeUi(-Qi}`{}(--BFvDHAUGv?wo0y|L&8PbBlJ)ARAQUF+MpsPS3x+00!x4ls~b4 F`9JqINYVfR literal 0 HcmV?d00001 diff --git a/docs/api/source/images/navbar.png b/docs/api/source/images/navbar.png new file mode 100644 index 0000000000000000000000000000000000000000..df38e90d87e1a215371b4977e18cde90f8832537 GIT binary patch literal 96 zcmeAS@N?(olHy`uVBq!ia0vp^3Lwk@BpAX3RW*PVQ%R6tFatx` +This error section is stored in a separate file in includes/_errors.md. Slate allows you to optionally separate out your docs into many files...just save them to the includes folder and add them to the top of your index.md's frontmatter. Files are included in the order listed. + + +The Kittn API uses the following error codes: + + +Error Code | Meaning +---------- | ------- +400 | Bad Request -- Your request is invalid. +401 | Unauthorized -- Your API key is wrong. +403 | Forbidden -- The kitten requested is hidden for administrators only. +404 | Not Found -- The specified kitten could not be found. +405 | Method Not Allowed -- You tried to access a kitten with an invalid method. +406 | Not Acceptable -- You requested a format that isn't json. +410 | Gone -- The kitten requested has been removed from our servers. +418 | I'm a teapot. +429 | Too Many Requests -- You're requesting too many kittens! Slow down! +500 | Internal Server Error -- We had a problem with our server. Try again later. +503 | Service Unavailable -- We're temporarily offline for maintenance. Please try again later. diff --git a/docs/api/source/includes/_git.md b/docs/api/source/includes/_git.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/api/source/includes/_logs.md b/docs/api/source/includes/_logs.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/api/source/includes/_repository.md b/docs/api/source/includes/_repository.md new file mode 100644 index 00000000..20f5e5fe --- /dev/null +++ b/docs/api/source/includes/_repository.md @@ -0,0 +1,95 @@ +# Repository + +Besides providing a RESTful API, the Vieter server is also a Pacman-compatible +repository server. This section describes the various routes that make this +possible. + +## Get a package archive or database file + +```shell +curl -OL https://example.com/bur/x86_64/tuxedo-keyboard-3.0.10-1-x86_64.pkg.tar.zst +``` + +This endpoint is really the entire repository. It serves both the package +archives & the database files for a specific arch-repo. It has three different +behaviors, depending on `filename`: + +* If the file extension is one of `.db`, `.files`, `.db.tar.gz` or + `.files.tar.gz`, it tries to serve the requested database file. +* If the filename contains `.pkg`, it serves the package file. +* Otherwise, it assumes `filename` is the name & version of a package inside + the repository (e.g. `vieter-0.3.0_alpha.2-1`) & serves that package's `desc` + file from inside the database archive. + + + +### HTTP Request + +`GET /:repo/:arch/:filename` + +### URL Parameters + +Parameter | Description +--------- | ----------- +repo | Repository containing the package +arch | Arch-repo containing the package +filename | actual filename to request + +## Check whether file exists + +```shell +curl -IL https://example.com/bur/x86_64/tuxedo-keyboard-3.0.10-1-x86_64.pkg.tar.zst +``` + +The above request can also be performed as a HEAD request. The behavior is the +same, except no data is returned besides an error 404 if the file doesn't exist +& an error 200 otherwise. + +### HTTP Request + +`GET /:repo/:arch/:filename` + +### URL Parameters + +Parameter | Description +--------- | ----------- +repo | Repository containing the package +arch | Arch-repo containing the package +filename | actual filename to request + +## Publish package + + + +```shell +curl \ + -H 'X-Api-Key: secret' \ + -XPOST + -T tuxedo-keyboard-3.0.10-1-x86_64.pkg.tar.zst \ + https://example.com/some-repo/publish +``` + +This endpoint allows you to publish a new package archive to a given repo. + +If the package's architecture is not `any`, it is added to that specific +arch-repo. Otherwise, it is added to the configured default architecture & any +other already present arch-repos. + +### HTTP Request + +`POST /:repo/publish` + +### URL Parameters + +Parameter | Description +--------- | ----------- +repo | Repository to publish package to diff --git a/docs/api/source/index.html.md b/docs/api/source/index.html.md new file mode 100644 index 00000000..a46d47ea --- /dev/null +++ b/docs/api/source/index.html.md @@ -0,0 +1,40 @@ +--- +title: API Reference + +language_tabs: # must be one of https://git.io/vQNgJ + - shell: cURL + +toc_footers: + - Documentation Powered by Slate + +includes: + - repository + - git + - logs + - errors + +search: true + +code_clipboard: true + +meta: + - name: description + content: Documentation for the Vieter API +--- + +# Introduction + +Welcome to the Vieter API documentation! Here, you can find everything related +to interacting with Vieter's HTTP API. + +# Authentication + +```shell +curl -H 'X-Api-Key: secret' https://example.com/api/some/path +``` + +> Don't forget to replace `secret` with your Vieter instance's secret. + +Authentication is done by passing the HTTP header `X-Api-Key: secret` along +with each request, where `secret` is replaced with your Vieter server's +configured secret. diff --git a/docs/api/source/javascripts/all.js b/docs/api/source/javascripts/all.js new file mode 100644 index 00000000..5f5d4067 --- /dev/null +++ b/docs/api/source/javascripts/all.js @@ -0,0 +1,2 @@ +//= require ./all_nosearch +//= require ./app/_search diff --git a/docs/api/source/javascripts/all_nosearch.js b/docs/api/source/javascripts/all_nosearch.js new file mode 100644 index 00000000..026e5a20 --- /dev/null +++ b/docs/api/source/javascripts/all_nosearch.js @@ -0,0 +1,27 @@ +//= require ./lib/_energize +//= require ./app/_copy +//= require ./app/_toc +//= require ./app/_lang + +function adjustLanguageSelectorWidth() { + const elem = $('.dark-box > .lang-selector'); + elem.width(elem.parent().width()); +} + +$(function() { + loadToc($('#toc'), '.toc-link', '.toc-list-h2', 10); + setupLanguages($('body').data('languages')); + $('.content').imagesLoaded( function() { + window.recacheHeights(); + window.refreshToc(); + }); + + $(window).resize(function() { + adjustLanguageSelectorWidth(); + }); + adjustLanguageSelectorWidth(); +}); + +window.onpopstate = function() { + activateLanguage(getLanguageFromQueryString()); +}; diff --git a/docs/api/source/javascripts/app/_copy.js b/docs/api/source/javascripts/app/_copy.js new file mode 100644 index 00000000..4dfbbb6c --- /dev/null +++ b/docs/api/source/javascripts/app/_copy.js @@ -0,0 +1,15 @@ +function copyToClipboard(container) { + const el = document.createElement('textarea'); + el.value = container.textContent.replace(/\n$/, ''); + document.body.appendChild(el); + el.select(); + document.execCommand('copy'); + document.body.removeChild(el); +} + +function setupCodeCopy() { + $('pre.highlight').prepend('
Copy to Clipboard
'); + $('.copy-clipboard').on('click', function() { + copyToClipboard(this.parentNode.children[1]); + }); +} diff --git a/docs/api/source/javascripts/app/_lang.js b/docs/api/source/javascripts/app/_lang.js new file mode 100644 index 00000000..cc5ac8b6 --- /dev/null +++ b/docs/api/source/javascripts/app/_lang.js @@ -0,0 +1,171 @@ +//= require ../lib/_jquery + +/* +Copyright 2008-2013 Concur Technologies, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. You may obtain +a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +License for the specific language governing permissions and limitations +under the License. +*/ +;(function () { + 'use strict'; + + var languages = []; + + window.setupLanguages = setupLanguages; + window.activateLanguage = activateLanguage; + window.getLanguageFromQueryString = getLanguageFromQueryString; + + function activateLanguage(language) { + if (!language) return; + if (language === "") return; + + $(".lang-selector a").removeClass('active'); + $(".lang-selector a[data-language-name='" + language + "']").addClass('active'); + for (var i=0; i < languages.length; i++) { + $(".highlight.tab-" + languages[i]).hide(); + $(".lang-specific." + languages[i]).hide(); + } + $(".highlight.tab-" + language).show(); + $(".lang-specific." + language).show(); + + window.recacheHeights(); + + // scroll to the new location of the position + if ($(window.location.hash).get(0)) { + $(window.location.hash).get(0).scrollIntoView(true); + } + } + + // parseURL and stringifyURL are from https://github.com/sindresorhus/query-string + // MIT licensed + // https://github.com/sindresorhus/query-string/blob/7bee64c16f2da1a326579e96977b9227bf6da9e6/license + function parseURL(str) { + if (typeof str !== 'string') { + return {}; + } + + str = str.trim().replace(/^(\?|#|&)/, ''); + + if (!str) { + return {}; + } + + return str.split('&').reduce(function (ret, param) { + var parts = param.replace(/\+/g, ' ').split('='); + var key = parts[0]; + var val = parts[1]; + + key = decodeURIComponent(key); + // missing `=` should be `null`: + // http://w3.org/TR/2012/WD-url-20120524/#collect-url-parameters + val = val === undefined ? null : decodeURIComponent(val); + + if (!ret.hasOwnProperty(key)) { + ret[key] = val; + } else if (Array.isArray(ret[key])) { + ret[key].push(val); + } else { + ret[key] = [ret[key], val]; + } + + return ret; + }, {}); + }; + + function stringifyURL(obj) { + return obj ? Object.keys(obj).sort().map(function (key) { + var val = obj[key]; + + if (Array.isArray(val)) { + return val.sort().map(function (val2) { + return encodeURIComponent(key) + '=' + encodeURIComponent(val2); + }).join('&'); + } + + return encodeURIComponent(key) + '=' + encodeURIComponent(val); + }).join('&') : ''; + }; + + // gets the language set in the query string + function getLanguageFromQueryString() { + if (location.search.length >= 1) { + var language = parseURL(location.search).language; + if (language) { + return language; + } else if (jQuery.inArray(location.search.substr(1), languages) != -1) { + return location.search.substr(1); + } + } + + return false; + } + + // returns a new query string with the new language in it + function generateNewQueryString(language) { + var url = parseURL(location.search); + if (url.language) { + url.language = language; + return stringifyURL(url); + } + return language; + } + + // if a button is clicked, add the state to the history + function pushURL(language) { + if (!history) { return; } + var hash = window.location.hash; + if (hash) { + hash = hash.replace(/^#+/, ''); + } + history.pushState({}, '', '?' + generateNewQueryString(language) + '#' + hash); + + // save language as next default + if (localStorage) { + localStorage.setItem("language", language); + } + } + + function setupLanguages(l) { + var defaultLanguage = null; + if (localStorage) { + defaultLanguage = localStorage.getItem("language"); + } + + languages = l; + + var presetLanguage = getLanguageFromQueryString(); + if (presetLanguage) { + // the language is in the URL, so use that language! + activateLanguage(presetLanguage); + + if (localStorage) { + localStorage.setItem("language", presetLanguage); + } + } else if ((defaultLanguage !== null) && (jQuery.inArray(defaultLanguage, languages) != -1)) { + // the language was the last selected one saved in localstorage, so use that language! + activateLanguage(defaultLanguage); + } else { + // no language selected, so use the default + activateLanguage(languages[0]); + } + } + + // if we click on a language tab, activate that language + $(function() { + $(".lang-selector a").on("click", function() { + var language = $(this).data("language-name"); + pushURL(language); + activateLanguage(language); + return false; + }); + }); +})(); diff --git a/docs/api/source/javascripts/app/_search.js b/docs/api/source/javascripts/app/_search.js new file mode 100644 index 00000000..0b0ccd97 --- /dev/null +++ b/docs/api/source/javascripts/app/_search.js @@ -0,0 +1,102 @@ +//= require ../lib/_lunr +//= require ../lib/_jquery +//= require ../lib/_jquery.highlight +;(function () { + 'use strict'; + + var content, searchResults; + var highlightOpts = { element: 'span', className: 'search-highlight' }; + var searchDelay = 0; + var timeoutHandle = 0; + var index; + + function populate() { + index = lunr(function(){ + + this.ref('id'); + this.field('title', { boost: 10 }); + this.field('body'); + this.pipeline.add(lunr.trimmer, lunr.stopWordFilter); + var lunrConfig = this; + + $('h1, h2').each(function() { + var title = $(this); + var body = title.nextUntil('h1, h2'); + lunrConfig.add({ + id: title.prop('id'), + title: title.text(), + body: body.text() + }); + }); + + }); + determineSearchDelay(); + } + + $(populate); + $(bind); + + function determineSearchDelay() { + if (index.tokenSet.toArray().length>5000) { + searchDelay = 300; + } + } + + function bind() { + content = $('.content'); + searchResults = $('.search-results'); + + $('#input-search').on('keyup',function(e) { + var wait = function() { + return function(executingFunction, waitTime){ + clearTimeout(timeoutHandle); + timeoutHandle = setTimeout(executingFunction, waitTime); + }; + }(); + wait(function(){ + search(e); + }, searchDelay); + }); + } + + function search(event) { + + var searchInput = $('#input-search')[0]; + + unhighlight(); + searchResults.addClass('visible'); + + // ESC clears the field + if (event.keyCode === 27) searchInput.value = ''; + + if (searchInput.value) { + var results = index.search(searchInput.value).filter(function(r) { + return r.score > 0.0001; + }); + + if (results.length) { + searchResults.empty(); + $.each(results, function (index, result) { + var elem = document.getElementById(result.ref); + searchResults.append("
  • " + $(elem).text() + "
  • "); + }); + highlight.call(searchInput); + } else { + searchResults.html('
  • '); + $('.search-results li').text('No Results Found for "' + searchInput.value + '"'); + } + } else { + unhighlight(); + searchResults.removeClass('visible'); + } + } + + function highlight() { + if (this.value) content.highlight(this.value, highlightOpts); + } + + function unhighlight() { + content.unhighlight(highlightOpts); + } +})(); + diff --git a/docs/api/source/javascripts/app/_toc.js b/docs/api/source/javascripts/app/_toc.js new file mode 100644 index 00000000..f70bdc0f --- /dev/null +++ b/docs/api/source/javascripts/app/_toc.js @@ -0,0 +1,122 @@ +//= require ../lib/_jquery +//= require ../lib/_imagesloaded.min +;(function () { + 'use strict'; + + var htmlPattern = /<[^>]*>/g; + var loaded = false; + + var debounce = function(func, waitTime) { + var timeout = false; + return function() { + if (timeout === false) { + setTimeout(function() { + func(); + timeout = false; + }, waitTime); + timeout = true; + } + }; + }; + + var closeToc = function() { + $(".toc-wrapper").removeClass('open'); + $("#nav-button").removeClass('open'); + }; + + function loadToc($toc, tocLinkSelector, tocListSelector, scrollOffset) { + var headerHeights = {}; + var pageHeight = 0; + var windowHeight = 0; + var originalTitle = document.title; + + var recacheHeights = function() { + headerHeights = {}; + pageHeight = $(document).height(); + windowHeight = $(window).height(); + + $toc.find(tocLinkSelector).each(function() { + var targetId = $(this).attr('href'); + if (targetId[0] === "#") { + headerHeights[targetId] = $("#" + $.escapeSelector(targetId.substring(1))).offset().top; + } + }); + }; + + var refreshToc = function() { + var currentTop = $(document).scrollTop() + scrollOffset; + + if (currentTop + windowHeight >= pageHeight) { + // at bottom of page, so just select last header by making currentTop very large + // this fixes the problem where the last header won't ever show as active if its content + // is shorter than the window height + currentTop = pageHeight + 1000; + } + + var best = null; + for (var name in headerHeights) { + if ((headerHeights[name] < currentTop && headerHeights[name] > headerHeights[best]) || best === null) { + best = name; + } + } + + // Catch the initial load case + if (currentTop == scrollOffset && !loaded) { + best = window.location.hash; + loaded = true; + } + + var $best = $toc.find("[href='" + best + "']").first(); + if (!$best.hasClass("active")) { + // .active is applied to the ToC link we're currently on, and its parent
      s selected by tocListSelector + // .active-expanded is applied to the ToC links that are parents of this one + $toc.find(".active").removeClass("active"); + $toc.find(".active-parent").removeClass("active-parent"); + $best.addClass("active"); + $best.parents(tocListSelector).addClass("active").siblings(tocLinkSelector).addClass('active-parent'); + $best.siblings(tocListSelector).addClass("active"); + $toc.find(tocListSelector).filter(":not(.active)").slideUp(150); + $toc.find(tocListSelector).filter(".active").slideDown(150); + if (window.history.replaceState) { + window.history.replaceState(null, "", best); + } + var thisTitle = $best.data("title"); + if (thisTitle !== undefined && thisTitle.length > 0) { + document.title = thisTitle.replace(htmlPattern, "") + " – " + originalTitle; + } else { + document.title = originalTitle; + } + } + }; + + var makeToc = function() { + recacheHeights(); + refreshToc(); + + $("#nav-button").click(function() { + $(".toc-wrapper").toggleClass('open'); + $("#nav-button").toggleClass('open'); + return false; + }); + $(".page-wrapper").click(closeToc); + $(".toc-link").click(closeToc); + + // reload immediately after scrolling on toc click + $toc.find(tocLinkSelector).click(function() { + setTimeout(function() { + refreshToc(); + }, 0); + }); + + $(window).scroll(debounce(refreshToc, 200)); + $(window).resize(debounce(recacheHeights, 200)); + }; + + makeToc(); + + window.recacheHeights = recacheHeights; + window.refreshToc = refreshToc; + } + + window.loadToc = loadToc; +})(); diff --git a/docs/api/source/javascripts/lib/_energize.js b/docs/api/source/javascripts/lib/_energize.js new file mode 100644 index 00000000..6798f3c0 --- /dev/null +++ b/docs/api/source/javascripts/lib/_energize.js @@ -0,0 +1,169 @@ +/** + * energize.js v0.1.0 + * + * Speeds up click events on mobile devices. + * https://github.com/davidcalhoun/energize.js + */ + +(function() { // Sandbox + /** + * Don't add to non-touch devices, which don't need to be sped up + */ + if(!('ontouchstart' in window)) return; + + var lastClick = {}, + isThresholdReached, touchstart, touchmove, touchend, + click, closest; + + /** + * isThresholdReached + * + * Compare touchstart with touchend xy coordinates, + * and only fire simulated click event if the coordinates + * are nearby. (don't want clicking to be confused with a swipe) + */ + isThresholdReached = function(startXY, xy) { + return Math.abs(startXY[0] - xy[0]) > 5 || Math.abs(startXY[1] - xy[1]) > 5; + }; + + /** + * touchstart + * + * Save xy coordinates when the user starts touching the screen + */ + touchstart = function(e) { + this.startXY = [e.touches[0].clientX, e.touches[0].clientY]; + this.threshold = false; + }; + + /** + * touchmove + * + * Check if the user is scrolling past the threshold. + * Have to check here because touchend will not always fire + * on some tested devices (Kindle Fire?) + */ + touchmove = function(e) { + // NOOP if the threshold has already been reached + if(this.threshold) return false; + + this.threshold = isThresholdReached(this.startXY, [e.touches[0].clientX, e.touches[0].clientY]); + }; + + /** + * touchend + * + * If the user didn't scroll past the threshold between + * touchstart and touchend, fire a simulated click. + * + * (This will fire before a native click) + */ + touchend = function(e) { + // Don't fire a click if the user scrolled past the threshold + if(this.threshold || isThresholdReached(this.startXY, [e.changedTouches[0].clientX, e.changedTouches[0].clientY])) { + return; + } + + /** + * Create and fire a click event on the target element + * https://developer.mozilla.org/en/DOM/event.initMouseEvent + */ + var touch = e.changedTouches[0], + evt = document.createEvent('MouseEvents'); + evt.initMouseEvent('click', true, true, window, 0, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null); + evt.simulated = true; // distinguish from a normal (nonsimulated) click + e.target.dispatchEvent(evt); + }; + + /** + * click + * + * Because we've already fired a click event in touchend, + * we need to listed for all native click events here + * and suppress them as necessary. + */ + click = function(e) { + /** + * Prevent ghost clicks by only allowing clicks we created + * in the click event we fired (look for e.simulated) + */ + var time = Date.now(), + timeDiff = time - lastClick.time, + x = e.clientX, + y = e.clientY, + xyDiff = [Math.abs(lastClick.x - x), Math.abs(lastClick.y - y)], + target = closest(e.target, 'A') || e.target, // needed for standalone apps + nodeName = target.nodeName, + isLink = nodeName === 'A', + standAlone = window.navigator.standalone && isLink && e.target.getAttribute("href"); + + lastClick.time = time; + lastClick.x = x; + lastClick.y = y; + + /** + * Unfortunately Android sometimes fires click events without touch events (seen on Kindle Fire), + * so we have to add more logic to determine the time of the last click. Not perfect... + * + * Older, simpler check: if((!e.simulated) || standAlone) + */ + if((!e.simulated && (timeDiff < 500 || (timeDiff < 1500 && xyDiff[0] < 50 && xyDiff[1] < 50))) || standAlone) { + e.preventDefault(); + e.stopPropagation(); + if(!standAlone) return false; + } + + /** + * Special logic for standalone web apps + * See http://stackoverflow.com/questions/2898740/iphone-safari-web-app-opens-links-in-new-window + */ + if(standAlone) { + window.location = target.getAttribute("href"); + } + + /** + * Add an energize-focus class to the targeted link (mimics :focus behavior) + * TODO: test and/or remove? Does this work? + */ + if(!target || !target.classList) return; + target.classList.add("energize-focus"); + window.setTimeout(function(){ + target.classList.remove("energize-focus"); + }, 150); + }; + + /** + * closest + * @param {HTMLElement} node current node to start searching from. + * @param {string} tagName the (uppercase) name of the tag you're looking for. + * + * Find the closest ancestor tag of a given node. + * + * Starts at node and goes up the DOM tree looking for a + * matching nodeName, continuing until hitting document.body + */ + closest = function(node, tagName){ + var curNode = node; + + while(curNode !== document.body) { // go up the dom until we find the tag we're after + if(!curNode || curNode.nodeName === tagName) { return curNode; } // found + curNode = curNode.parentNode; // not found, so keep going up + } + + return null; // not found + }; + + /** + * Add all delegated event listeners + * + * All the events we care about bubble up to document, + * so we can take advantage of event delegation. + * + * Note: no need to wait for DOMContentLoaded here + */ + document.addEventListener('touchstart', touchstart, false); + document.addEventListener('touchmove', touchmove, false); + document.addEventListener('touchend', touchend, false); + document.addEventListener('click', click, true); // TODO: why does this use capture? + +})(); \ No newline at end of file diff --git a/docs/api/source/javascripts/lib/_imagesloaded.min.js b/docs/api/source/javascripts/lib/_imagesloaded.min.js new file mode 100644 index 00000000..e443a77d --- /dev/null +++ b/docs/api/source/javascripts/lib/_imagesloaded.min.js @@ -0,0 +1,7 @@ +/*! + * imagesLoaded PACKAGED v4.1.4 + * JavaScript is all like "You images are done yet or what?" + * MIT License + */ + +!function(e,t){"function"==typeof define&&define.amd?define("ev-emitter/ev-emitter",t):"object"==typeof module&&module.exports?module.exports=t():e.EvEmitter=t()}("undefined"!=typeof window?window:this,function(){function e(){}var t=e.prototype;return t.on=function(e,t){if(e&&t){var i=this._events=this._events||{},n=i[e]=i[e]||[];return n.indexOf(t)==-1&&n.push(t),this}},t.once=function(e,t){if(e&&t){this.on(e,t);var i=this._onceEvents=this._onceEvents||{},n=i[e]=i[e]||{};return n[t]=!0,this}},t.off=function(e,t){var i=this._events&&this._events[e];if(i&&i.length){var n=i.indexOf(t);return n!=-1&&i.splice(n,1),this}},t.emitEvent=function(e,t){var i=this._events&&this._events[e];if(i&&i.length){i=i.slice(0),t=t||[];for(var n=this._onceEvents&&this._onceEvents[e],o=0;o (default options) + * $('#content').highlight('lorem'); + * + * // search for and highlight more terms at once + * // so you can save some time on traversing DOM + * $('#content').highlight(['lorem', 'ipsum']); + * $('#content').highlight('lorem ipsum'); + * + * // search only for entire word 'lorem' + * $('#content').highlight('lorem', { wordsOnly: true }); + * + * // don't ignore case during search of term 'lorem' + * $('#content').highlight('lorem', { caseSensitive: true }); + * + * // wrap every occurrance of term 'ipsum' in content + * // with + * $('#content').highlight('ipsum', { element: 'em', className: 'important' }); + * + * // remove default highlight + * $('#content').unhighlight(); + * + * // remove custom highlight + * $('#content').unhighlight({ element: 'em', className: 'important' }); + * + * + * Copyright (c) 2009 Bartek Szopka + * + * Licensed under MIT license. + * + */ + +jQuery.extend({ + highlight: function (node, re, nodeName, className) { + if (node.nodeType === 3) { + var match = node.data.match(re); + if (match) { + var highlight = document.createElement(nodeName || 'span'); + highlight.className = className || 'highlight'; + var wordNode = node.splitText(match.index); + wordNode.splitText(match[0].length); + var wordClone = wordNode.cloneNode(true); + highlight.appendChild(wordClone); + wordNode.parentNode.replaceChild(highlight, wordNode); + return 1; //skip added node in parent + } + } else if ((node.nodeType === 1 && node.childNodes) && // only element nodes that have children + !/(script|style)/i.test(node.tagName) && // ignore script and style nodes + !(node.tagName === nodeName.toUpperCase() && node.className === className)) { // skip if already highlighted + for (var i = 0; i < node.childNodes.length; i++) { + i += jQuery.highlight(node.childNodes[i], re, nodeName, className); + } + } + return 0; + } +}); + +jQuery.fn.unhighlight = function (options) { + var settings = { className: 'highlight', element: 'span' }; + jQuery.extend(settings, options); + + return this.find(settings.element + "." + settings.className).each(function () { + var parent = this.parentNode; + parent.replaceChild(this.firstChild, this); + parent.normalize(); + }).end(); +}; + +jQuery.fn.highlight = function (words, options) { + var settings = { className: 'highlight', element: 'span', caseSensitive: false, wordsOnly: false }; + jQuery.extend(settings, options); + + if (words.constructor === String) { + words = [words]; + } + words = jQuery.grep(words, function(word, i){ + return word != ''; + }); + words = jQuery.map(words, function(word, i) { + return word.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); + }); + if (words.length == 0) { return this; }; + + var flag = settings.caseSensitive ? "" : "i"; + var pattern = "(" + words.join("|") + ")"; + if (settings.wordsOnly) { + pattern = "\\b" + pattern + "\\b"; + } + var re = new RegExp(pattern, flag); + + return this.each(function () { + jQuery.highlight(this, re, settings.element, settings.className); + }); +}; + diff --git a/docs/api/source/javascripts/lib/_jquery.js b/docs/api/source/javascripts/lib/_jquery.js new file mode 100644 index 00000000..fc6c299b --- /dev/null +++ b/docs/api/source/javascripts/lib/_jquery.js @@ -0,0 +1,10881 @@ +/*! + * jQuery JavaScript Library v3.6.0 + * https://jquery.com/ + * + * Includes Sizzle.js + * https://sizzlejs.com/ + * + * Copyright OpenJS Foundation and other contributors + * Released under the MIT license + * https://jquery.org/license + * + * Date: 2021-03-02T17:08Z + */ +( function( global, factory ) { + + "use strict"; + + if ( typeof module === "object" && typeof module.exports === "object" ) { + + // For CommonJS and CommonJS-like environments where a proper `window` + // is present, execute the factory and get jQuery. + // For environments that do not have a `window` with a `document` + // (such as Node.js), expose a factory as module.exports. + // This accentuates the need for the creation of a real `window`. + // e.g. var jQuery = require("jquery")(window); + // See ticket #14549 for more info. + module.exports = global.document ? + factory( global, true ) : + function( w ) { + if ( !w.document ) { + throw new Error( "jQuery requires a window with a document" ); + } + return factory( w ); + }; + } else { + factory( global ); + } + +// Pass this if window is not defined yet +} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) { + +// Edge <= 12 - 13+, Firefox <=18 - 45+, IE 10 - 11, Safari 5.1 - 9+, iOS 6 - 9.1 +// throw exceptions when non-strict code (e.g., ASP.NET 4.5) accesses strict mode +// arguments.callee.caller (trac-13335). But as of jQuery 3.0 (2016), strict mode should be common +// enough that all such attempts are guarded in a try block. +"use strict"; + +var arr = []; + +var getProto = Object.getPrototypeOf; + +var slice = arr.slice; + +var flat = arr.flat ? function( array ) { + return arr.flat.call( array ); +} : function( array ) { + return arr.concat.apply( [], array ); +}; + + +var push = arr.push; + +var indexOf = arr.indexOf; + +var class2type = {}; + +var toString = class2type.toString; + +var hasOwn = class2type.hasOwnProperty; + +var fnToString = hasOwn.toString; + +var ObjectFunctionString = fnToString.call( Object ); + +var support = {}; + +var isFunction = function isFunction( obj ) { + + // Support: Chrome <=57, Firefox <=52 + // In some browsers, typeof returns "function" for HTML elements + // (i.e., `typeof document.createElement( "object" ) === "function"`). + // We don't want to classify *any* DOM node as a function. + // Support: QtWeb <=3.8.5, WebKit <=534.34, wkhtmltopdf tool <=0.12.5 + // Plus for old WebKit, typeof returns "function" for HTML collections + // (e.g., `typeof document.getElementsByTagName("div") === "function"`). (gh-4756) + return typeof obj === "function" && typeof obj.nodeType !== "number" && + typeof obj.item !== "function"; + }; + + +var isWindow = function isWindow( obj ) { + return obj != null && obj === obj.window; + }; + + +var document = window.document; + + + + var preservedScriptAttributes = { + type: true, + src: true, + nonce: true, + noModule: true + }; + + function DOMEval( code, node, doc ) { + doc = doc || document; + + var i, val, + script = doc.createElement( "script" ); + + script.text = code; + if ( node ) { + for ( i in preservedScriptAttributes ) { + + // Support: Firefox 64+, Edge 18+ + // Some browsers don't support the "nonce" property on scripts. + // On the other hand, just using `getAttribute` is not enough as + // the `nonce` attribute is reset to an empty string whenever it + // becomes browsing-context connected. + // See https://github.com/whatwg/html/issues/2369 + // See https://html.spec.whatwg.org/#nonce-attributes + // The `node.getAttribute` check was added for the sake of + // `jQuery.globalEval` so that it can fake a nonce-containing node + // via an object. + val = node[ i ] || node.getAttribute && node.getAttribute( i ); + if ( val ) { + script.setAttribute( i, val ); + } + } + } + doc.head.appendChild( script ).parentNode.removeChild( script ); + } + + +function toType( obj ) { + if ( obj == null ) { + return obj + ""; + } + + // Support: Android <=2.3 only (functionish RegExp) + return typeof obj === "object" || typeof obj === "function" ? + class2type[ toString.call( obj ) ] || "object" : + typeof obj; +} +/* global Symbol */ +// Defining this global in .eslintrc.json would create a danger of using the global +// unguarded in another place, it seems safer to define global only for this module + + + +var + version = "3.6.0", + + // Define a local copy of jQuery + jQuery = function( selector, context ) { + + // The jQuery object is actually just the init constructor 'enhanced' + // Need init if jQuery is called (just allow error to be thrown if not included) + return new jQuery.fn.init( selector, context ); + }; + +jQuery.fn = jQuery.prototype = { + + // The current version of jQuery being used + jquery: version, + + constructor: jQuery, + + // The default length of a jQuery object is 0 + length: 0, + + toArray: function() { + return slice.call( this ); + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + + // Return all the elements in a clean array + if ( num == null ) { + return slice.call( this ); + } + + // Return just the one element from the set + return num < 0 ? this[ num + this.length ] : this[ num ]; + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems ) { + + // Build a new jQuery matched element set + var ret = jQuery.merge( this.constructor(), elems ); + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + + // Return the newly-formed element set + return ret; + }, + + // Execute a callback for every element in the matched set. + each: function( callback ) { + return jQuery.each( this, callback ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map( this, function( elem, i ) { + return callback.call( elem, i, elem ); + } ) ); + }, + + slice: function() { + return this.pushStack( slice.apply( this, arguments ) ); + }, + + first: function() { + return this.eq( 0 ); + }, + + last: function() { + return this.eq( -1 ); + }, + + even: function() { + return this.pushStack( jQuery.grep( this, function( _elem, i ) { + return ( i + 1 ) % 2; + } ) ); + }, + + odd: function() { + return this.pushStack( jQuery.grep( this, function( _elem, i ) { + return i % 2; + } ) ); + }, + + eq: function( i ) { + var len = this.length, + j = +i + ( i < 0 ? len : 0 ); + return this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] ); + }, + + end: function() { + return this.prevObject || this.constructor(); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: push, + sort: arr.sort, + splice: arr.splice +}; + +jQuery.extend = jQuery.fn.extend = function() { + var options, name, src, copy, copyIsArray, clone, + target = arguments[ 0 ] || {}, + i = 1, + length = arguments.length, + deep = false; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + + // Skip the boolean and the target + target = arguments[ i ] || {}; + i++; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !isFunction( target ) ) { + target = {}; + } + + // Extend jQuery itself if only one argument is passed + if ( i === length ) { + target = this; + i--; + } + + for ( ; i < length; i++ ) { + + // Only deal with non-null/undefined values + if ( ( options = arguments[ i ] ) != null ) { + + // Extend the base object + for ( name in options ) { + copy = options[ name ]; + + // Prevent Object.prototype pollution + // Prevent never-ending loop + if ( name === "__proto__" || target === copy ) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if ( deep && copy && ( jQuery.isPlainObject( copy ) || + ( copyIsArray = Array.isArray( copy ) ) ) ) { + src = target[ name ]; + + // Ensure proper type for the source value + if ( copyIsArray && !Array.isArray( src ) ) { + clone = []; + } else if ( !copyIsArray && !jQuery.isPlainObject( src ) ) { + clone = {}; + } else { + clone = src; + } + copyIsArray = false; + + // Never move original objects, clone them + target[ name ] = jQuery.extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + // Return the modified object + return target; +}; + +jQuery.extend( { + + // Unique for each copy of jQuery on the page + expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ), + + // Assume jQuery is ready without the ready module + isReady: true, + + error: function( msg ) { + throw new Error( msg ); + }, + + noop: function() {}, + + isPlainObject: function( obj ) { + var proto, Ctor; + + // Detect obvious negatives + // Use toString instead of jQuery.type to catch host objects + if ( !obj || toString.call( obj ) !== "[object Object]" ) { + return false; + } + + proto = getProto( obj ); + + // Objects with no prototype (e.g., `Object.create( null )`) are plain + if ( !proto ) { + return true; + } + + // Objects with prototype are plain iff they were constructed by a global Object function + Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor; + return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString; + }, + + isEmptyObject: function( obj ) { + var name; + + for ( name in obj ) { + return false; + } + return true; + }, + + // Evaluates a script in a provided context; falls back to the global one + // if not specified. + globalEval: function( code, options, doc ) { + DOMEval( code, { nonce: options && options.nonce }, doc ); + }, + + each: function( obj, callback ) { + var length, i = 0; + + if ( isArrayLike( obj ) ) { + length = obj.length; + for ( ; i < length; i++ ) { + if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { + break; + } + } + } else { + for ( i in obj ) { + if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { + break; + } + } + } + + return obj; + }, + + // results is for internal usage only + makeArray: function( arr, results ) { + var ret = results || []; + + if ( arr != null ) { + if ( isArrayLike( Object( arr ) ) ) { + jQuery.merge( ret, + typeof arr === "string" ? + [ arr ] : arr + ); + } else { + push.call( ret, arr ); + } + } + + return ret; + }, + + inArray: function( elem, arr, i ) { + return arr == null ? -1 : indexOf.call( arr, elem, i ); + }, + + // Support: Android <=4.0 only, PhantomJS 1 only + // push.apply(_, arraylike) throws on ancient WebKit + merge: function( first, second ) { + var len = +second.length, + j = 0, + i = first.length; + + for ( ; j < len; j++ ) { + first[ i++ ] = second[ j ]; + } + + first.length = i; + + return first; + }, + + grep: function( elems, callback, invert ) { + var callbackInverse, + matches = [], + i = 0, + length = elems.length, + callbackExpect = !invert; + + // Go through the array, only saving the items + // that pass the validator function + for ( ; i < length; i++ ) { + callbackInverse = !callback( elems[ i ], i ); + if ( callbackInverse !== callbackExpect ) { + matches.push( elems[ i ] ); + } + } + + return matches; + }, + + // arg is for internal usage only + map: function( elems, callback, arg ) { + var length, value, + i = 0, + ret = []; + + // Go through the array, translating each of the items to their new values + if ( isArrayLike( elems ) ) { + length = elems.length; + for ( ; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + + // Go through every key on the object, + } else { + for ( i in elems ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + } + + // Flatten any nested arrays + return flat( ret ); + }, + + // A global GUID counter for objects + guid: 1, + + // jQuery.support is not used in Core but other projects attach their + // properties to it so it needs to exist. + support: support +} ); + +if ( typeof Symbol === "function" ) { + jQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ]; +} + +// Populate the class2type map +jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ), + function( _i, name ) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); + } ); + +function isArrayLike( obj ) { + + // Support: real iOS 8.2 only (not reproducible in simulator) + // `in` check used to prevent JIT error (gh-2145) + // hasOwn isn't used here due to false negatives + // regarding Nodelist length in IE + var length = !!obj && "length" in obj && obj.length, + type = toType( obj ); + + if ( isFunction( obj ) || isWindow( obj ) ) { + return false; + } + + return type === "array" || length === 0 || + typeof length === "number" && length > 0 && ( length - 1 ) in obj; +} +var Sizzle = +/*! + * Sizzle CSS Selector Engine v2.3.6 + * https://sizzlejs.com/ + * + * Copyright JS Foundation and other contributors + * Released under the MIT license + * https://js.foundation/ + * + * Date: 2021-02-16 + */ +( function( window ) { +var i, + support, + Expr, + getText, + isXML, + tokenize, + compile, + select, + outermostContext, + sortInput, + hasDuplicate, + + // Local document vars + setDocument, + document, + docElem, + documentIsHTML, + rbuggyQSA, + rbuggyMatches, + matches, + contains, + + // Instance-specific data + expando = "sizzle" + 1 * new Date(), + preferredDoc = window.document, + dirruns = 0, + done = 0, + classCache = createCache(), + tokenCache = createCache(), + compilerCache = createCache(), + nonnativeSelectorCache = createCache(), + sortOrder = function( a, b ) { + if ( a === b ) { + hasDuplicate = true; + } + return 0; + }, + + // Instance methods + hasOwn = ( {} ).hasOwnProperty, + arr = [], + pop = arr.pop, + pushNative = arr.push, + push = arr.push, + slice = arr.slice, + + // Use a stripped-down indexOf as it's faster than native + // https://jsperf.com/thor-indexof-vs-for/5 + indexOf = function( list, elem ) { + var i = 0, + len = list.length; + for ( ; i < len; i++ ) { + if ( list[ i ] === elem ) { + return i; + } + } + return -1; + }, + + booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|" + + "ismap|loop|multiple|open|readonly|required|scoped", + + // Regular expressions + + // http://www.w3.org/TR/css3-selectors/#whitespace + whitespace = "[\\x20\\t\\r\\n\\f]", + + // https://www.w3.org/TR/css-syntax-3/#ident-token-diagram + identifier = "(?:\\\\[\\da-fA-F]{1,6}" + whitespace + + "?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+", + + // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors + attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace + + + // Operator (capture 2) + "*([*^$|!~]?=)" + whitespace + + + // "Attribute values must be CSS identifiers [capture 5] + // or strings [capture 3 or capture 4]" + "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + + whitespace + "*\\]", + + pseudos = ":(" + identifier + ")(?:\\((" + + + // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments: + // 1. quoted (capture 3; capture 4 or capture 5) + "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + + + // 2. simple (capture 6) + "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" + + + // 3. anything else (capture 2) + ".*" + + ")\\)|)", + + // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter + rwhitespace = new RegExp( whitespace + "+", "g" ), + rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + + whitespace + "+$", "g" ), + + rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), + rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + + "*" ), + rdescend = new RegExp( whitespace + "|>" ), + + rpseudo = new RegExp( pseudos ), + ridentifier = new RegExp( "^" + identifier + "$" ), + + matchExpr = { + "ID": new RegExp( "^#(" + identifier + ")" ), + "CLASS": new RegExp( "^\\.(" + identifier + ")" ), + "TAG": new RegExp( "^(" + identifier + "|[*])" ), + "ATTR": new RegExp( "^" + attributes ), + "PSEUDO": new RegExp( "^" + pseudos ), + "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + + whitespace + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + + whitespace + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), + "bool": new RegExp( "^(?:" + booleans + ")$", "i" ), + + // For use in libraries implementing .is() + // We use this for POS matching in `select` + "needsContext": new RegExp( "^" + whitespace + + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + whitespace + + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) + }, + + rhtml = /HTML$/i, + rinputs = /^(?:input|select|textarea|button)$/i, + rheader = /^h\d$/i, + + rnative = /^[^{]+\{\s*\[native \w/, + + // Easily-parseable/retrievable ID or TAG or CLASS selectors + rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, + + rsibling = /[+~]/, + + // CSS escapes + // http://www.w3.org/TR/CSS21/syndata.html#escaped-characters + runescape = new RegExp( "\\\\[\\da-fA-F]{1,6}" + whitespace + "?|\\\\([^\\r\\n\\f])", "g" ), + funescape = function( escape, nonHex ) { + var high = "0x" + escape.slice( 1 ) - 0x10000; + + return nonHex ? + + // Strip the backslash prefix from a non-hex escape sequence + nonHex : + + // Replace a hexadecimal escape sequence with the encoded Unicode code point + // Support: IE <=11+ + // For values outside the Basic Multilingual Plane (BMP), manually construct a + // surrogate pair + high < 0 ? + String.fromCharCode( high + 0x10000 ) : + String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); + }, + + // CSS string/identifier serialization + // https://drafts.csswg.org/cssom/#common-serializing-idioms + rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g, + fcssescape = function( ch, asCodePoint ) { + if ( asCodePoint ) { + + // U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER + if ( ch === "\0" ) { + return "\uFFFD"; + } + + // Control characters and (dependent upon position) numbers get escaped as code points + return ch.slice( 0, -1 ) + "\\" + + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " "; + } + + // Other potentially-special ASCII characters get backslash-escaped + return "\\" + ch; + }, + + // Used for iframes + // See setDocument() + // Removing the function wrapper causes a "Permission Denied" + // error in IE + unloadHandler = function() { + setDocument(); + }, + + inDisabledFieldset = addCombinator( + function( elem ) { + return elem.disabled === true && elem.nodeName.toLowerCase() === "fieldset"; + }, + { dir: "parentNode", next: "legend" } + ); + +// Optimize for push.apply( _, NodeList ) +try { + push.apply( + ( arr = slice.call( preferredDoc.childNodes ) ), + preferredDoc.childNodes + ); + + // Support: Android<4.0 + // Detect silently failing push.apply + // eslint-disable-next-line no-unused-expressions + arr[ preferredDoc.childNodes.length ].nodeType; +} catch ( e ) { + push = { apply: arr.length ? + + // Leverage slice if possible + function( target, els ) { + pushNative.apply( target, slice.call( els ) ); + } : + + // Support: IE<9 + // Otherwise append directly + function( target, els ) { + var j = target.length, + i = 0; + + // Can't trust NodeList.length + while ( ( target[ j++ ] = els[ i++ ] ) ) {} + target.length = j - 1; + } + }; +} + +function Sizzle( selector, context, results, seed ) { + var m, i, elem, nid, match, groups, newSelector, + newContext = context && context.ownerDocument, + + // nodeType defaults to 9, since context defaults to document + nodeType = context ? context.nodeType : 9; + + results = results || []; + + // Return early from calls with invalid selector or context + if ( typeof selector !== "string" || !selector || + nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) { + + return results; + } + + // Try to shortcut find operations (as opposed to filters) in HTML documents + if ( !seed ) { + setDocument( context ); + context = context || document; + + if ( documentIsHTML ) { + + // If the selector is sufficiently simple, try using a "get*By*" DOM method + // (excepting DocumentFragment context, where the methods don't exist) + if ( nodeType !== 11 && ( match = rquickExpr.exec( selector ) ) ) { + + // ID selector + if ( ( m = match[ 1 ] ) ) { + + // Document context + if ( nodeType === 9 ) { + if ( ( elem = context.getElementById( m ) ) ) { + + // Support: IE, Opera, Webkit + // TODO: identify versions + // getElementById can match elements by name instead of ID + if ( elem.id === m ) { + results.push( elem ); + return results; + } + } else { + return results; + } + + // Element context + } else { + + // Support: IE, Opera, Webkit + // TODO: identify versions + // getElementById can match elements by name instead of ID + if ( newContext && ( elem = newContext.getElementById( m ) ) && + contains( context, elem ) && + elem.id === m ) { + + results.push( elem ); + return results; + } + } + + // Type selector + } else if ( match[ 2 ] ) { + push.apply( results, context.getElementsByTagName( selector ) ); + return results; + + // Class selector + } else if ( ( m = match[ 3 ] ) && support.getElementsByClassName && + context.getElementsByClassName ) { + + push.apply( results, context.getElementsByClassName( m ) ); + return results; + } + } + + // Take advantage of querySelectorAll + if ( support.qsa && + !nonnativeSelectorCache[ selector + " " ] && + ( !rbuggyQSA || !rbuggyQSA.test( selector ) ) && + + // Support: IE 8 only + // Exclude object elements + ( nodeType !== 1 || context.nodeName.toLowerCase() !== "object" ) ) { + + newSelector = selector; + newContext = context; + + // qSA considers elements outside a scoping root when evaluating child or + // descendant combinators, which is not what we want. + // In such cases, we work around the behavior by prefixing every selector in the + // list with an ID selector referencing the scope context. + // The technique has to be used as well when a leading combinator is used + // as such selectors are not recognized by querySelectorAll. + // Thanks to Andrew Dupont for this technique. + if ( nodeType === 1 && + ( rdescend.test( selector ) || rcombinators.test( selector ) ) ) { + + // Expand context for sibling selectors + newContext = rsibling.test( selector ) && testContext( context.parentNode ) || + context; + + // We can use :scope instead of the ID hack if the browser + // supports it & if we're not changing the context. + if ( newContext !== context || !support.scope ) { + + // Capture the context ID, setting it first if necessary + if ( ( nid = context.getAttribute( "id" ) ) ) { + nid = nid.replace( rcssescape, fcssescape ); + } else { + context.setAttribute( "id", ( nid = expando ) ); + } + } + + // Prefix every selector in the list + groups = tokenize( selector ); + i = groups.length; + while ( i-- ) { + groups[ i ] = ( nid ? "#" + nid : ":scope" ) + " " + + toSelector( groups[ i ] ); + } + newSelector = groups.join( "," ); + } + + try { + push.apply( results, + newContext.querySelectorAll( newSelector ) + ); + return results; + } catch ( qsaError ) { + nonnativeSelectorCache( selector, true ); + } finally { + if ( nid === expando ) { + context.removeAttribute( "id" ); + } + } + } + } + } + + // All others + return select( selector.replace( rtrim, "$1" ), context, results, seed ); +} + +/** + * Create key-value caches of limited size + * @returns {function(string, object)} Returns the Object data after storing it on itself with + * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) + * deleting the oldest entry + */ +function createCache() { + var keys = []; + + function cache( key, value ) { + + // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) + if ( keys.push( key + " " ) > Expr.cacheLength ) { + + // Only keep the most recent entries + delete cache[ keys.shift() ]; + } + return ( cache[ key + " " ] = value ); + } + return cache; +} + +/** + * Mark a function for special use by Sizzle + * @param {Function} fn The function to mark + */ +function markFunction( fn ) { + fn[ expando ] = true; + return fn; +} + +/** + * Support testing using an element + * @param {Function} fn Passed the created element and returns a boolean result + */ +function assert( fn ) { + var el = document.createElement( "fieldset" ); + + try { + return !!fn( el ); + } catch ( e ) { + return false; + } finally { + + // Remove from its parent by default + if ( el.parentNode ) { + el.parentNode.removeChild( el ); + } + + // release memory in IE + el = null; + } +} + +/** + * Adds the same handler for all of the specified attrs + * @param {String} attrs Pipe-separated list of attributes + * @param {Function} handler The method that will be applied + */ +function addHandle( attrs, handler ) { + var arr = attrs.split( "|" ), + i = arr.length; + + while ( i-- ) { + Expr.attrHandle[ arr[ i ] ] = handler; + } +} + +/** + * Checks document order of two siblings + * @param {Element} a + * @param {Element} b + * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b + */ +function siblingCheck( a, b ) { + var cur = b && a, + diff = cur && a.nodeType === 1 && b.nodeType === 1 && + a.sourceIndex - b.sourceIndex; + + // Use IE sourceIndex if available on both nodes + if ( diff ) { + return diff; + } + + // Check if b follows a + if ( cur ) { + while ( ( cur = cur.nextSibling ) ) { + if ( cur === b ) { + return -1; + } + } + } + + return a ? 1 : -1; +} + +/** + * Returns a function to use in pseudos for input types + * @param {String} type + */ +function createInputPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === type; + }; +} + +/** + * Returns a function to use in pseudos for buttons + * @param {String} type + */ +function createButtonPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return ( name === "input" || name === "button" ) && elem.type === type; + }; +} + +/** + * Returns a function to use in pseudos for :enabled/:disabled + * @param {Boolean} disabled true for :disabled; false for :enabled + */ +function createDisabledPseudo( disabled ) { + + // Known :disabled false positives: fieldset[disabled] > legend:nth-of-type(n+2) :can-disable + return function( elem ) { + + // Only certain elements can match :enabled or :disabled + // https://html.spec.whatwg.org/multipage/scripting.html#selector-enabled + // https://html.spec.whatwg.org/multipage/scripting.html#selector-disabled + if ( "form" in elem ) { + + // Check for inherited disabledness on relevant non-disabled elements: + // * listed form-associated elements in a disabled fieldset + // https://html.spec.whatwg.org/multipage/forms.html#category-listed + // https://html.spec.whatwg.org/multipage/forms.html#concept-fe-disabled + // * option elements in a disabled optgroup + // https://html.spec.whatwg.org/multipage/forms.html#concept-option-disabled + // All such elements have a "form" property. + if ( elem.parentNode && elem.disabled === false ) { + + // Option elements defer to a parent optgroup if present + if ( "label" in elem ) { + if ( "label" in elem.parentNode ) { + return elem.parentNode.disabled === disabled; + } else { + return elem.disabled === disabled; + } + } + + // Support: IE 6 - 11 + // Use the isDisabled shortcut property to check for disabled fieldset ancestors + return elem.isDisabled === disabled || + + // Where there is no isDisabled, check manually + /* jshint -W018 */ + elem.isDisabled !== !disabled && + inDisabledFieldset( elem ) === disabled; + } + + return elem.disabled === disabled; + + // Try to winnow out elements that can't be disabled before trusting the disabled property. + // Some victims get caught in our net (label, legend, menu, track), but it shouldn't + // even exist on them, let alone have a boolean value. + } else if ( "label" in elem ) { + return elem.disabled === disabled; + } + + // Remaining elements are neither :enabled nor :disabled + return false; + }; +} + +/** + * Returns a function to use in pseudos for positionals + * @param {Function} fn + */ +function createPositionalPseudo( fn ) { + return markFunction( function( argument ) { + argument = +argument; + return markFunction( function( seed, matches ) { + var j, + matchIndexes = fn( [], seed.length, argument ), + i = matchIndexes.length; + + // Match elements found at the specified indexes + while ( i-- ) { + if ( seed[ ( j = matchIndexes[ i ] ) ] ) { + seed[ j ] = !( matches[ j ] = seed[ j ] ); + } + } + } ); + } ); +} + +/** + * Checks a node for validity as a Sizzle context + * @param {Element|Object=} context + * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value + */ +function testContext( context ) { + return context && typeof context.getElementsByTagName !== "undefined" && context; +} + +// Expose support vars for convenience +support = Sizzle.support = {}; + +/** + * Detects XML nodes + * @param {Element|Object} elem An element or a document + * @returns {Boolean} True iff elem is a non-HTML XML node + */ +isXML = Sizzle.isXML = function( elem ) { + var namespace = elem && elem.namespaceURI, + docElem = elem && ( elem.ownerDocument || elem ).documentElement; + + // Support: IE <=8 + // Assume HTML when documentElement doesn't yet exist, such as inside loading iframes + // https://bugs.jquery.com/ticket/4833 + return !rhtml.test( namespace || docElem && docElem.nodeName || "HTML" ); +}; + +/** + * Sets document-related variables once based on the current document + * @param {Element|Object} [doc] An element or document object to use to set the document + * @returns {Object} Returns the current document + */ +setDocument = Sizzle.setDocument = function( node ) { + var hasCompare, subWindow, + doc = node ? node.ownerDocument || node : preferredDoc; + + // Return early if doc is invalid or already selected + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( doc == document || doc.nodeType !== 9 || !doc.documentElement ) { + return document; + } + + // Update global variables + document = doc; + docElem = document.documentElement; + documentIsHTML = !isXML( document ); + + // Support: IE 9 - 11+, Edge 12 - 18+ + // Accessing iframe documents after unload throws "permission denied" errors (jQuery #13936) + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( preferredDoc != document && + ( subWindow = document.defaultView ) && subWindow.top !== subWindow ) { + + // Support: IE 11, Edge + if ( subWindow.addEventListener ) { + subWindow.addEventListener( "unload", unloadHandler, false ); + + // Support: IE 9 - 10 only + } else if ( subWindow.attachEvent ) { + subWindow.attachEvent( "onunload", unloadHandler ); + } + } + + // Support: IE 8 - 11+, Edge 12 - 18+, Chrome <=16 - 25 only, Firefox <=3.6 - 31 only, + // Safari 4 - 5 only, Opera <=11.6 - 12.x only + // IE/Edge & older browsers don't support the :scope pseudo-class. + // Support: Safari 6.0 only + // Safari 6.0 supports :scope but it's an alias of :root there. + support.scope = assert( function( el ) { + docElem.appendChild( el ).appendChild( document.createElement( "div" ) ); + return typeof el.querySelectorAll !== "undefined" && + !el.querySelectorAll( ":scope fieldset div" ).length; + } ); + + /* Attributes + ---------------------------------------------------------------------- */ + + // Support: IE<8 + // Verify that getAttribute really returns attributes and not properties + // (excepting IE8 booleans) + support.attributes = assert( function( el ) { + el.className = "i"; + return !el.getAttribute( "className" ); + } ); + + /* getElement(s)By* + ---------------------------------------------------------------------- */ + + // Check if getElementsByTagName("*") returns only elements + support.getElementsByTagName = assert( function( el ) { + el.appendChild( document.createComment( "" ) ); + return !el.getElementsByTagName( "*" ).length; + } ); + + // Support: IE<9 + support.getElementsByClassName = rnative.test( document.getElementsByClassName ); + + // Support: IE<10 + // Check if getElementById returns elements by name + // The broken getElementById methods don't pick up programmatically-set names, + // so use a roundabout getElementsByName test + support.getById = assert( function( el ) { + docElem.appendChild( el ).id = expando; + return !document.getElementsByName || !document.getElementsByName( expando ).length; + } ); + + // ID filter and find + if ( support.getById ) { + Expr.filter[ "ID" ] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + return elem.getAttribute( "id" ) === attrId; + }; + }; + Expr.find[ "ID" ] = function( id, context ) { + if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { + var elem = context.getElementById( id ); + return elem ? [ elem ] : []; + } + }; + } else { + Expr.filter[ "ID" ] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + var node = typeof elem.getAttributeNode !== "undefined" && + elem.getAttributeNode( "id" ); + return node && node.value === attrId; + }; + }; + + // Support: IE 6 - 7 only + // getElementById is not reliable as a find shortcut + Expr.find[ "ID" ] = function( id, context ) { + if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { + var node, i, elems, + elem = context.getElementById( id ); + + if ( elem ) { + + // Verify the id attribute + node = elem.getAttributeNode( "id" ); + if ( node && node.value === id ) { + return [ elem ]; + } + + // Fall back on getElementsByName + elems = context.getElementsByName( id ); + i = 0; + while ( ( elem = elems[ i++ ] ) ) { + node = elem.getAttributeNode( "id" ); + if ( node && node.value === id ) { + return [ elem ]; + } + } + } + + return []; + } + }; + } + + // Tag + Expr.find[ "TAG" ] = support.getElementsByTagName ? + function( tag, context ) { + if ( typeof context.getElementsByTagName !== "undefined" ) { + return context.getElementsByTagName( tag ); + + // DocumentFragment nodes don't have gEBTN + } else if ( support.qsa ) { + return context.querySelectorAll( tag ); + } + } : + + function( tag, context ) { + var elem, + tmp = [], + i = 0, + + // By happy coincidence, a (broken) gEBTN appears on DocumentFragment nodes too + results = context.getElementsByTagName( tag ); + + // Filter out possible comments + if ( tag === "*" ) { + while ( ( elem = results[ i++ ] ) ) { + if ( elem.nodeType === 1 ) { + tmp.push( elem ); + } + } + + return tmp; + } + return results; + }; + + // Class + Expr.find[ "CLASS" ] = support.getElementsByClassName && function( className, context ) { + if ( typeof context.getElementsByClassName !== "undefined" && documentIsHTML ) { + return context.getElementsByClassName( className ); + } + }; + + /* QSA/matchesSelector + ---------------------------------------------------------------------- */ + + // QSA and matchesSelector support + + // matchesSelector(:active) reports false when true (IE9/Opera 11.5) + rbuggyMatches = []; + + // qSa(:focus) reports false when true (Chrome 21) + // We allow this because of a bug in IE8/9 that throws an error + // whenever `document.activeElement` is accessed on an iframe + // So, we allow :focus to pass through QSA all the time to avoid the IE error + // See https://bugs.jquery.com/ticket/13378 + rbuggyQSA = []; + + if ( ( support.qsa = rnative.test( document.querySelectorAll ) ) ) { + + // Build QSA regex + // Regex strategy adopted from Diego Perini + assert( function( el ) { + + var input; + + // Select is set to empty string on purpose + // This is to test IE's treatment of not explicitly + // setting a boolean content attribute, + // since its presence should be enough + // https://bugs.jquery.com/ticket/12359 + docElem.appendChild( el ).innerHTML = "" + + ""; + + // Support: IE8, Opera 11-12.16 + // Nothing should be selected when empty strings follow ^= or $= or *= + // The test attribute must be unknown in Opera but "safe" for WinRT + // https://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section + if ( el.querySelectorAll( "[msallowcapture^='']" ).length ) { + rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" ); + } + + // Support: IE8 + // Boolean attributes and "value" are not treated correctly + if ( !el.querySelectorAll( "[selected]" ).length ) { + rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" ); + } + + // Support: Chrome<29, Android<4.4, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.8+ + if ( !el.querySelectorAll( "[id~=" + expando + "-]" ).length ) { + rbuggyQSA.push( "~=" ); + } + + // Support: IE 11+, Edge 15 - 18+ + // IE 11/Edge don't find elements on a `[name='']` query in some cases. + // Adding a temporary attribute to the document before the selection works + // around the issue. + // Interestingly, IE 10 & older don't seem to have the issue. + input = document.createElement( "input" ); + input.setAttribute( "name", "" ); + el.appendChild( input ); + if ( !el.querySelectorAll( "[name='']" ).length ) { + rbuggyQSA.push( "\\[" + whitespace + "*name" + whitespace + "*=" + + whitespace + "*(?:''|\"\")" ); + } + + // Webkit/Opera - :checked should return selected option elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + // IE8 throws error here and will not see later tests + if ( !el.querySelectorAll( ":checked" ).length ) { + rbuggyQSA.push( ":checked" ); + } + + // Support: Safari 8+, iOS 8+ + // https://bugs.webkit.org/show_bug.cgi?id=136851 + // In-page `selector#id sibling-combinator selector` fails + if ( !el.querySelectorAll( "a#" + expando + "+*" ).length ) { + rbuggyQSA.push( ".#.+[+~]" ); + } + + // Support: Firefox <=3.6 - 5 only + // Old Firefox doesn't throw on a badly-escaped identifier. + el.querySelectorAll( "\\\f" ); + rbuggyQSA.push( "[\\r\\n\\f]" ); + } ); + + assert( function( el ) { + el.innerHTML = "" + + ""; + + // Support: Windows 8 Native Apps + // The type and name attributes are restricted during .innerHTML assignment + var input = document.createElement( "input" ); + input.setAttribute( "type", "hidden" ); + el.appendChild( input ).setAttribute( "name", "D" ); + + // Support: IE8 + // Enforce case-sensitivity of name attribute + if ( el.querySelectorAll( "[name=d]" ).length ) { + rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" ); + } + + // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) + // IE8 throws error here and will not see later tests + if ( el.querySelectorAll( ":enabled" ).length !== 2 ) { + rbuggyQSA.push( ":enabled", ":disabled" ); + } + + // Support: IE9-11+ + // IE's :disabled selector does not pick up the children of disabled fieldsets + docElem.appendChild( el ).disabled = true; + if ( el.querySelectorAll( ":disabled" ).length !== 2 ) { + rbuggyQSA.push( ":enabled", ":disabled" ); + } + + // Support: Opera 10 - 11 only + // Opera 10-11 does not throw on post-comma invalid pseudos + el.querySelectorAll( "*,:x" ); + rbuggyQSA.push( ",.*:" ); + } ); + } + + if ( ( support.matchesSelector = rnative.test( ( matches = docElem.matches || + docElem.webkitMatchesSelector || + docElem.mozMatchesSelector || + docElem.oMatchesSelector || + docElem.msMatchesSelector ) ) ) ) { + + assert( function( el ) { + + // Check to see if it's possible to do matchesSelector + // on a disconnected node (IE 9) + support.disconnectedMatch = matches.call( el, "*" ); + + // This should fail with an exception + // Gecko does not error, returns false instead + matches.call( el, "[s!='']:x" ); + rbuggyMatches.push( "!=", pseudos ); + } ); + } + + rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join( "|" ) ); + rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join( "|" ) ); + + /* Contains + ---------------------------------------------------------------------- */ + hasCompare = rnative.test( docElem.compareDocumentPosition ); + + // Element contains another + // Purposefully self-exclusive + // As in, an element does not contain itself + contains = hasCompare || rnative.test( docElem.contains ) ? + function( a, b ) { + var adown = a.nodeType === 9 ? a.documentElement : a, + bup = b && b.parentNode; + return a === bup || !!( bup && bup.nodeType === 1 && ( + adown.contains ? + adown.contains( bup ) : + a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 + ) ); + } : + function( a, b ) { + if ( b ) { + while ( ( b = b.parentNode ) ) { + if ( b === a ) { + return true; + } + } + } + return false; + }; + + /* Sorting + ---------------------------------------------------------------------- */ + + // Document order sorting + sortOrder = hasCompare ? + function( a, b ) { + + // Flag for duplicate removal + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + // Sort on method existence if only one input has compareDocumentPosition + var compare = !a.compareDocumentPosition - !b.compareDocumentPosition; + if ( compare ) { + return compare; + } + + // Calculate position if both inputs belong to the same document + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + compare = ( a.ownerDocument || a ) == ( b.ownerDocument || b ) ? + a.compareDocumentPosition( b ) : + + // Otherwise we know they are disconnected + 1; + + // Disconnected nodes + if ( compare & 1 || + ( !support.sortDetached && b.compareDocumentPosition( a ) === compare ) ) { + + // Choose the first element that is related to our preferred document + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( a == document || a.ownerDocument == preferredDoc && + contains( preferredDoc, a ) ) { + return -1; + } + + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( b == document || b.ownerDocument == preferredDoc && + contains( preferredDoc, b ) ) { + return 1; + } + + // Maintain original order + return sortInput ? + ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) : + 0; + } + + return compare & 4 ? -1 : 1; + } : + function( a, b ) { + + // Exit early if the nodes are identical + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + var cur, + i = 0, + aup = a.parentNode, + bup = b.parentNode, + ap = [ a ], + bp = [ b ]; + + // Parentless nodes are either documents or disconnected + if ( !aup || !bup ) { + + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + /* eslint-disable eqeqeq */ + return a == document ? -1 : + b == document ? 1 : + /* eslint-enable eqeqeq */ + aup ? -1 : + bup ? 1 : + sortInput ? + ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) : + 0; + + // If the nodes are siblings, we can do a quick check + } else if ( aup === bup ) { + return siblingCheck( a, b ); + } + + // Otherwise we need full lists of their ancestors for comparison + cur = a; + while ( ( cur = cur.parentNode ) ) { + ap.unshift( cur ); + } + cur = b; + while ( ( cur = cur.parentNode ) ) { + bp.unshift( cur ); + } + + // Walk down the tree looking for a discrepancy + while ( ap[ i ] === bp[ i ] ) { + i++; + } + + return i ? + + // Do a sibling check if the nodes have a common ancestor + siblingCheck( ap[ i ], bp[ i ] ) : + + // Otherwise nodes in our document sort first + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + /* eslint-disable eqeqeq */ + ap[ i ] == preferredDoc ? -1 : + bp[ i ] == preferredDoc ? 1 : + /* eslint-enable eqeqeq */ + 0; + }; + + return document; +}; + +Sizzle.matches = function( expr, elements ) { + return Sizzle( expr, null, null, elements ); +}; + +Sizzle.matchesSelector = function( elem, expr ) { + setDocument( elem ); + + if ( support.matchesSelector && documentIsHTML && + !nonnativeSelectorCache[ expr + " " ] && + ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) && + ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) { + + try { + var ret = matches.call( elem, expr ); + + // IE 9's matchesSelector returns false on disconnected nodes + if ( ret || support.disconnectedMatch || + + // As well, disconnected nodes are said to be in a document + // fragment in IE 9 + elem.document && elem.document.nodeType !== 11 ) { + return ret; + } + } catch ( e ) { + nonnativeSelectorCache( expr, true ); + } + } + + return Sizzle( expr, document, null, [ elem ] ).length > 0; +}; + +Sizzle.contains = function( context, elem ) { + + // Set document vars if needed + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( ( context.ownerDocument || context ) != document ) { + setDocument( context ); + } + return contains( context, elem ); +}; + +Sizzle.attr = function( elem, name ) { + + // Set document vars if needed + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( ( elem.ownerDocument || elem ) != document ) { + setDocument( elem ); + } + + var fn = Expr.attrHandle[ name.toLowerCase() ], + + // Don't get fooled by Object.prototype properties (jQuery #13807) + val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ? + fn( elem, name, !documentIsHTML ) : + undefined; + + return val !== undefined ? + val : + support.attributes || !documentIsHTML ? + elem.getAttribute( name ) : + ( val = elem.getAttributeNode( name ) ) && val.specified ? + val.value : + null; +}; + +Sizzle.escape = function( sel ) { + return ( sel + "" ).replace( rcssescape, fcssescape ); +}; + +Sizzle.error = function( msg ) { + throw new Error( "Syntax error, unrecognized expression: " + msg ); +}; + +/** + * Document sorting and removing duplicates + * @param {ArrayLike} results + */ +Sizzle.uniqueSort = function( results ) { + var elem, + duplicates = [], + j = 0, + i = 0; + + // Unless we *know* we can detect duplicates, assume their presence + hasDuplicate = !support.detectDuplicates; + sortInput = !support.sortStable && results.slice( 0 ); + results.sort( sortOrder ); + + if ( hasDuplicate ) { + while ( ( elem = results[ i++ ] ) ) { + if ( elem === results[ i ] ) { + j = duplicates.push( i ); + } + } + while ( j-- ) { + results.splice( duplicates[ j ], 1 ); + } + } + + // Clear input after sorting to release objects + // See https://github.com/jquery/sizzle/pull/225 + sortInput = null; + + return results; +}; + +/** + * Utility function for retrieving the text value of an array of DOM nodes + * @param {Array|Element} elem + */ +getText = Sizzle.getText = function( elem ) { + var node, + ret = "", + i = 0, + nodeType = elem.nodeType; + + if ( !nodeType ) { + + // If no nodeType, this is expected to be an array + while ( ( node = elem[ i++ ] ) ) { + + // Do not traverse comment nodes + ret += getText( node ); + } + } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { + + // Use textContent for elements + // innerText usage removed for consistency of new lines (jQuery #11153) + if ( typeof elem.textContent === "string" ) { + return elem.textContent; + } else { + + // Traverse its children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + ret += getText( elem ); + } + } + } else if ( nodeType === 3 || nodeType === 4 ) { + return elem.nodeValue; + } + + // Do not include comment or processing instruction nodes + + return ret; +}; + +Expr = Sizzle.selectors = { + + // Can be adjusted by the user + cacheLength: 50, + + createPseudo: markFunction, + + match: matchExpr, + + attrHandle: {}, + + find: {}, + + relative: { + ">": { dir: "parentNode", first: true }, + " ": { dir: "parentNode" }, + "+": { dir: "previousSibling", first: true }, + "~": { dir: "previousSibling" } + }, + + preFilter: { + "ATTR": function( match ) { + match[ 1 ] = match[ 1 ].replace( runescape, funescape ); + + // Move the given value to match[3] whether quoted or unquoted + match[ 3 ] = ( match[ 3 ] || match[ 4 ] || + match[ 5 ] || "" ).replace( runescape, funescape ); + + if ( match[ 2 ] === "~=" ) { + match[ 3 ] = " " + match[ 3 ] + " "; + } + + return match.slice( 0, 4 ); + }, + + "CHILD": function( match ) { + + /* matches from matchExpr["CHILD"] + 1 type (only|nth|...) + 2 what (child|of-type) + 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) + 4 xn-component of xn+y argument ([+-]?\d*n|) + 5 sign of xn-component + 6 x of xn-component + 7 sign of y-component + 8 y of y-component + */ + match[ 1 ] = match[ 1 ].toLowerCase(); + + if ( match[ 1 ].slice( 0, 3 ) === "nth" ) { + + // nth-* requires argument + if ( !match[ 3 ] ) { + Sizzle.error( match[ 0 ] ); + } + + // numeric x and y parameters for Expr.filter.CHILD + // remember that false/true cast respectively to 0/1 + match[ 4 ] = +( match[ 4 ] ? + match[ 5 ] + ( match[ 6 ] || 1 ) : + 2 * ( match[ 3 ] === "even" || match[ 3 ] === "odd" ) ); + match[ 5 ] = +( ( match[ 7 ] + match[ 8 ] ) || match[ 3 ] === "odd" ); + + // other types prohibit arguments + } else if ( match[ 3 ] ) { + Sizzle.error( match[ 0 ] ); + } + + return match; + }, + + "PSEUDO": function( match ) { + var excess, + unquoted = !match[ 6 ] && match[ 2 ]; + + if ( matchExpr[ "CHILD" ].test( match[ 0 ] ) ) { + return null; + } + + // Accept quoted arguments as-is + if ( match[ 3 ] ) { + match[ 2 ] = match[ 4 ] || match[ 5 ] || ""; + + // Strip excess characters from unquoted arguments + } else if ( unquoted && rpseudo.test( unquoted ) && + + // Get excess from tokenize (recursively) + ( excess = tokenize( unquoted, true ) ) && + + // advance to the next closing parenthesis + ( excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length ) ) { + + // excess is a negative index + match[ 0 ] = match[ 0 ].slice( 0, excess ); + match[ 2 ] = unquoted.slice( 0, excess ); + } + + // Return only captures needed by the pseudo filter method (type and argument) + return match.slice( 0, 3 ); + } + }, + + filter: { + + "TAG": function( nodeNameSelector ) { + var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase(); + return nodeNameSelector === "*" ? + function() { + return true; + } : + function( elem ) { + return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; + }; + }, + + "CLASS": function( className ) { + var pattern = classCache[ className + " " ]; + + return pattern || + ( pattern = new RegExp( "(^|" + whitespace + + ")" + className + "(" + whitespace + "|$)" ) ) && classCache( + className, function( elem ) { + return pattern.test( + typeof elem.className === "string" && elem.className || + typeof elem.getAttribute !== "undefined" && + elem.getAttribute( "class" ) || + "" + ); + } ); + }, + + "ATTR": function( name, operator, check ) { + return function( elem ) { + var result = Sizzle.attr( elem, name ); + + if ( result == null ) { + return operator === "!="; + } + if ( !operator ) { + return true; + } + + result += ""; + + /* eslint-disable max-len */ + + return operator === "=" ? result === check : + operator === "!=" ? result !== check : + operator === "^=" ? check && result.indexOf( check ) === 0 : + operator === "*=" ? check && result.indexOf( check ) > -1 : + operator === "$=" ? check && result.slice( -check.length ) === check : + operator === "~=" ? ( " " + result.replace( rwhitespace, " " ) + " " ).indexOf( check ) > -1 : + operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" : + false; + /* eslint-enable max-len */ + + }; + }, + + "CHILD": function( type, what, _argument, first, last ) { + var simple = type.slice( 0, 3 ) !== "nth", + forward = type.slice( -4 ) !== "last", + ofType = what === "of-type"; + + return first === 1 && last === 0 ? + + // Shortcut for :nth-*(n) + function( elem ) { + return !!elem.parentNode; + } : + + function( elem, _context, xml ) { + var cache, uniqueCache, outerCache, node, nodeIndex, start, + dir = simple !== forward ? "nextSibling" : "previousSibling", + parent = elem.parentNode, + name = ofType && elem.nodeName.toLowerCase(), + useCache = !xml && !ofType, + diff = false; + + if ( parent ) { + + // :(first|last|only)-(child|of-type) + if ( simple ) { + while ( dir ) { + node = elem; + while ( ( node = node[ dir ] ) ) { + if ( ofType ? + node.nodeName.toLowerCase() === name : + node.nodeType === 1 ) { + + return false; + } + } + + // Reverse direction for :only-* (if we haven't yet done so) + start = dir = type === "only" && !start && "nextSibling"; + } + return true; + } + + start = [ forward ? parent.firstChild : parent.lastChild ]; + + // non-xml :nth-child(...) stores cache data on `parent` + if ( forward && useCache ) { + + // Seek `elem` from a previously-cached index + + // ...in a gzip-friendly way + node = parent; + outerCache = node[ expando ] || ( node[ expando ] = {} ); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + ( outerCache[ node.uniqueID ] = {} ); + + cache = uniqueCache[ type ] || []; + nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; + diff = nodeIndex && cache[ 2 ]; + node = nodeIndex && parent.childNodes[ nodeIndex ]; + + while ( ( node = ++nodeIndex && node && node[ dir ] || + + // Fallback to seeking `elem` from the start + ( diff = nodeIndex = 0 ) || start.pop() ) ) { + + // When found, cache indexes on `parent` and break + if ( node.nodeType === 1 && ++diff && node === elem ) { + uniqueCache[ type ] = [ dirruns, nodeIndex, diff ]; + break; + } + } + + } else { + + // Use previously-cached element index if available + if ( useCache ) { + + // ...in a gzip-friendly way + node = elem; + outerCache = node[ expando ] || ( node[ expando ] = {} ); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + ( outerCache[ node.uniqueID ] = {} ); + + cache = uniqueCache[ type ] || []; + nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; + diff = nodeIndex; + } + + // xml :nth-child(...) + // or :nth-last-child(...) or :nth(-last)?-of-type(...) + if ( diff === false ) { + + // Use the same loop as above to seek `elem` from the start + while ( ( node = ++nodeIndex && node && node[ dir ] || + ( diff = nodeIndex = 0 ) || start.pop() ) ) { + + if ( ( ofType ? + node.nodeName.toLowerCase() === name : + node.nodeType === 1 ) && + ++diff ) { + + // Cache the index of each encountered element + if ( useCache ) { + outerCache = node[ expando ] || + ( node[ expando ] = {} ); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + ( outerCache[ node.uniqueID ] = {} ); + + uniqueCache[ type ] = [ dirruns, diff ]; + } + + if ( node === elem ) { + break; + } + } + } + } + } + + // Incorporate the offset, then check against cycle size + diff -= last; + return diff === first || ( diff % first === 0 && diff / first >= 0 ); + } + }; + }, + + "PSEUDO": function( pseudo, argument ) { + + // pseudo-class names are case-insensitive + // http://www.w3.org/TR/selectors/#pseudo-classes + // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters + // Remember that setFilters inherits from pseudos + var args, + fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || + Sizzle.error( "unsupported pseudo: " + pseudo ); + + // The user may use createPseudo to indicate that + // arguments are needed to create the filter function + // just as Sizzle does + if ( fn[ expando ] ) { + return fn( argument ); + } + + // But maintain support for old signatures + if ( fn.length > 1 ) { + args = [ pseudo, pseudo, "", argument ]; + return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? + markFunction( function( seed, matches ) { + var idx, + matched = fn( seed, argument ), + i = matched.length; + while ( i-- ) { + idx = indexOf( seed, matched[ i ] ); + seed[ idx ] = !( matches[ idx ] = matched[ i ] ); + } + } ) : + function( elem ) { + return fn( elem, 0, args ); + }; + } + + return fn; + } + }, + + pseudos: { + + // Potentially complex pseudos + "not": markFunction( function( selector ) { + + // Trim the selector passed to compile + // to avoid treating leading and trailing + // spaces as combinators + var input = [], + results = [], + matcher = compile( selector.replace( rtrim, "$1" ) ); + + return matcher[ expando ] ? + markFunction( function( seed, matches, _context, xml ) { + var elem, + unmatched = matcher( seed, null, xml, [] ), + i = seed.length; + + // Match elements unmatched by `matcher` + while ( i-- ) { + if ( ( elem = unmatched[ i ] ) ) { + seed[ i ] = !( matches[ i ] = elem ); + } + } + } ) : + function( elem, _context, xml ) { + input[ 0 ] = elem; + matcher( input, null, xml, results ); + + // Don't keep the element (issue #299) + input[ 0 ] = null; + return !results.pop(); + }; + } ), + + "has": markFunction( function( selector ) { + return function( elem ) { + return Sizzle( selector, elem ).length > 0; + }; + } ), + + "contains": markFunction( function( text ) { + text = text.replace( runescape, funescape ); + return function( elem ) { + return ( elem.textContent || getText( elem ) ).indexOf( text ) > -1; + }; + } ), + + // "Whether an element is represented by a :lang() selector + // is based solely on the element's language value + // being equal to the identifier C, + // or beginning with the identifier C immediately followed by "-". + // The matching of C against the element's language value is performed case-insensitively. + // The identifier C does not have to be a valid language name." + // http://www.w3.org/TR/selectors/#lang-pseudo + "lang": markFunction( function( lang ) { + + // lang value must be a valid identifier + if ( !ridentifier.test( lang || "" ) ) { + Sizzle.error( "unsupported lang: " + lang ); + } + lang = lang.replace( runescape, funescape ).toLowerCase(); + return function( elem ) { + var elemLang; + do { + if ( ( elemLang = documentIsHTML ? + elem.lang : + elem.getAttribute( "xml:lang" ) || elem.getAttribute( "lang" ) ) ) { + + elemLang = elemLang.toLowerCase(); + return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; + } + } while ( ( elem = elem.parentNode ) && elem.nodeType === 1 ); + return false; + }; + } ), + + // Miscellaneous + "target": function( elem ) { + var hash = window.location && window.location.hash; + return hash && hash.slice( 1 ) === elem.id; + }, + + "root": function( elem ) { + return elem === docElem; + }, + + "focus": function( elem ) { + return elem === document.activeElement && + ( !document.hasFocus || document.hasFocus() ) && + !!( elem.type || elem.href || ~elem.tabIndex ); + }, + + // Boolean properties + "enabled": createDisabledPseudo( false ), + "disabled": createDisabledPseudo( true ), + + "checked": function( elem ) { + + // In CSS3, :checked should return both checked and selected elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + var nodeName = elem.nodeName.toLowerCase(); + return ( nodeName === "input" && !!elem.checked ) || + ( nodeName === "option" && !!elem.selected ); + }, + + "selected": function( elem ) { + + // Accessing this property makes selected-by-default + // options in Safari work properly + if ( elem.parentNode ) { + // eslint-disable-next-line no-unused-expressions + elem.parentNode.selectedIndex; + } + + return elem.selected === true; + }, + + // Contents + "empty": function( elem ) { + + // http://www.w3.org/TR/selectors/#empty-pseudo + // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5), + // but not by others (comment: 8; processing instruction: 7; etc.) + // nodeType < 6 works because attributes (2) do not appear as children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + if ( elem.nodeType < 6 ) { + return false; + } + } + return true; + }, + + "parent": function( elem ) { + return !Expr.pseudos[ "empty" ]( elem ); + }, + + // Element/input types + "header": function( elem ) { + return rheader.test( elem.nodeName ); + }, + + "input": function( elem ) { + return rinputs.test( elem.nodeName ); + }, + + "button": function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === "button" || name === "button"; + }, + + "text": function( elem ) { + var attr; + return elem.nodeName.toLowerCase() === "input" && + elem.type === "text" && + + // Support: IE<8 + // New HTML5 attribute values (e.g., "search") appear with elem.type === "text" + ( ( attr = elem.getAttribute( "type" ) ) == null || + attr.toLowerCase() === "text" ); + }, + + // Position-in-collection + "first": createPositionalPseudo( function() { + return [ 0 ]; + } ), + + "last": createPositionalPseudo( function( _matchIndexes, length ) { + return [ length - 1 ]; + } ), + + "eq": createPositionalPseudo( function( _matchIndexes, length, argument ) { + return [ argument < 0 ? argument + length : argument ]; + } ), + + "even": createPositionalPseudo( function( matchIndexes, length ) { + var i = 0; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + } ), + + "odd": createPositionalPseudo( function( matchIndexes, length ) { + var i = 1; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + } ), + + "lt": createPositionalPseudo( function( matchIndexes, length, argument ) { + var i = argument < 0 ? + argument + length : + argument > length ? + length : + argument; + for ( ; --i >= 0; ) { + matchIndexes.push( i ); + } + return matchIndexes; + } ), + + "gt": createPositionalPseudo( function( matchIndexes, length, argument ) { + var i = argument < 0 ? argument + length : argument; + for ( ; ++i < length; ) { + matchIndexes.push( i ); + } + return matchIndexes; + } ) + } +}; + +Expr.pseudos[ "nth" ] = Expr.pseudos[ "eq" ]; + +// Add button/input type pseudos +for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { + Expr.pseudos[ i ] = createInputPseudo( i ); +} +for ( i in { submit: true, reset: true } ) { + Expr.pseudos[ i ] = createButtonPseudo( i ); +} + +// Easy API for creating new setFilters +function setFilters() {} +setFilters.prototype = Expr.filters = Expr.pseudos; +Expr.setFilters = new setFilters(); + +tokenize = Sizzle.tokenize = function( selector, parseOnly ) { + var matched, match, tokens, type, + soFar, groups, preFilters, + cached = tokenCache[ selector + " " ]; + + if ( cached ) { + return parseOnly ? 0 : cached.slice( 0 ); + } + + soFar = selector; + groups = []; + preFilters = Expr.preFilter; + + while ( soFar ) { + + // Comma and first run + if ( !matched || ( match = rcomma.exec( soFar ) ) ) { + if ( match ) { + + // Don't consume trailing commas as valid + soFar = soFar.slice( match[ 0 ].length ) || soFar; + } + groups.push( ( tokens = [] ) ); + } + + matched = false; + + // Combinators + if ( ( match = rcombinators.exec( soFar ) ) ) { + matched = match.shift(); + tokens.push( { + value: matched, + + // Cast descendant combinators to space + type: match[ 0 ].replace( rtrim, " " ) + } ); + soFar = soFar.slice( matched.length ); + } + + // Filters + for ( type in Expr.filter ) { + if ( ( match = matchExpr[ type ].exec( soFar ) ) && ( !preFilters[ type ] || + ( match = preFilters[ type ]( match ) ) ) ) { + matched = match.shift(); + tokens.push( { + value: matched, + type: type, + matches: match + } ); + soFar = soFar.slice( matched.length ); + } + } + + if ( !matched ) { + break; + } + } + + // Return the length of the invalid excess + // if we're just parsing + // Otherwise, throw an error or return tokens + return parseOnly ? + soFar.length : + soFar ? + Sizzle.error( selector ) : + + // Cache the tokens + tokenCache( selector, groups ).slice( 0 ); +}; + +function toSelector( tokens ) { + var i = 0, + len = tokens.length, + selector = ""; + for ( ; i < len; i++ ) { + selector += tokens[ i ].value; + } + return selector; +} + +function addCombinator( matcher, combinator, base ) { + var dir = combinator.dir, + skip = combinator.next, + key = skip || dir, + checkNonElements = base && key === "parentNode", + doneName = done++; + + return combinator.first ? + + // Check against closest ancestor/preceding element + function( elem, context, xml ) { + while ( ( elem = elem[ dir ] ) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + return matcher( elem, context, xml ); + } + } + return false; + } : + + // Check against all ancestor/preceding elements + function( elem, context, xml ) { + var oldCache, uniqueCache, outerCache, + newCache = [ dirruns, doneName ]; + + // We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching + if ( xml ) { + while ( ( elem = elem[ dir ] ) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + if ( matcher( elem, context, xml ) ) { + return true; + } + } + } + } else { + while ( ( elem = elem[ dir ] ) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + outerCache = elem[ expando ] || ( elem[ expando ] = {} ); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ elem.uniqueID ] || + ( outerCache[ elem.uniqueID ] = {} ); + + if ( skip && skip === elem.nodeName.toLowerCase() ) { + elem = elem[ dir ] || elem; + } else if ( ( oldCache = uniqueCache[ key ] ) && + oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) { + + // Assign to newCache so results back-propagate to previous elements + return ( newCache[ 2 ] = oldCache[ 2 ] ); + } else { + + // Reuse newcache so results back-propagate to previous elements + uniqueCache[ key ] = newCache; + + // A match means we're done; a fail means we have to keep checking + if ( ( newCache[ 2 ] = matcher( elem, context, xml ) ) ) { + return true; + } + } + } + } + } + return false; + }; +} + +function elementMatcher( matchers ) { + return matchers.length > 1 ? + function( elem, context, xml ) { + var i = matchers.length; + while ( i-- ) { + if ( !matchers[ i ]( elem, context, xml ) ) { + return false; + } + } + return true; + } : + matchers[ 0 ]; +} + +function multipleContexts( selector, contexts, results ) { + var i = 0, + len = contexts.length; + for ( ; i < len; i++ ) { + Sizzle( selector, contexts[ i ], results ); + } + return results; +} + +function condense( unmatched, map, filter, context, xml ) { + var elem, + newUnmatched = [], + i = 0, + len = unmatched.length, + mapped = map != null; + + for ( ; i < len; i++ ) { + if ( ( elem = unmatched[ i ] ) ) { + if ( !filter || filter( elem, context, xml ) ) { + newUnmatched.push( elem ); + if ( mapped ) { + map.push( i ); + } + } + } + } + + return newUnmatched; +} + +function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { + if ( postFilter && !postFilter[ expando ] ) { + postFilter = setMatcher( postFilter ); + } + if ( postFinder && !postFinder[ expando ] ) { + postFinder = setMatcher( postFinder, postSelector ); + } + return markFunction( function( seed, results, context, xml ) { + var temp, i, elem, + preMap = [], + postMap = [], + preexisting = results.length, + + // Get initial elements from seed or context + elems = seed || multipleContexts( + selector || "*", + context.nodeType ? [ context ] : context, + [] + ), + + // Prefilter to get matcher input, preserving a map for seed-results synchronization + matcherIn = preFilter && ( seed || !selector ) ? + condense( elems, preMap, preFilter, context, xml ) : + elems, + + matcherOut = matcher ? + + // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, + postFinder || ( seed ? preFilter : preexisting || postFilter ) ? + + // ...intermediate processing is necessary + [] : + + // ...otherwise use results directly + results : + matcherIn; + + // Find primary matches + if ( matcher ) { + matcher( matcherIn, matcherOut, context, xml ); + } + + // Apply postFilter + if ( postFilter ) { + temp = condense( matcherOut, postMap ); + postFilter( temp, [], context, xml ); + + // Un-match failing elements by moving them back to matcherIn + i = temp.length; + while ( i-- ) { + if ( ( elem = temp[ i ] ) ) { + matcherOut[ postMap[ i ] ] = !( matcherIn[ postMap[ i ] ] = elem ); + } + } + } + + if ( seed ) { + if ( postFinder || preFilter ) { + if ( postFinder ) { + + // Get the final matcherOut by condensing this intermediate into postFinder contexts + temp = []; + i = matcherOut.length; + while ( i-- ) { + if ( ( elem = matcherOut[ i ] ) ) { + + // Restore matcherIn since elem is not yet a final match + temp.push( ( matcherIn[ i ] = elem ) ); + } + } + postFinder( null, ( matcherOut = [] ), temp, xml ); + } + + // Move matched elements from seed to results to keep them synchronized + i = matcherOut.length; + while ( i-- ) { + if ( ( elem = matcherOut[ i ] ) && + ( temp = postFinder ? indexOf( seed, elem ) : preMap[ i ] ) > -1 ) { + + seed[ temp ] = !( results[ temp ] = elem ); + } + } + } + + // Add elements to results, through postFinder if defined + } else { + matcherOut = condense( + matcherOut === results ? + matcherOut.splice( preexisting, matcherOut.length ) : + matcherOut + ); + if ( postFinder ) { + postFinder( null, results, matcherOut, xml ); + } else { + push.apply( results, matcherOut ); + } + } + } ); +} + +function matcherFromTokens( tokens ) { + var checkContext, matcher, j, + len = tokens.length, + leadingRelative = Expr.relative[ tokens[ 0 ].type ], + implicitRelative = leadingRelative || Expr.relative[ " " ], + i = leadingRelative ? 1 : 0, + + // The foundational matcher ensures that elements are reachable from top-level context(s) + matchContext = addCombinator( function( elem ) { + return elem === checkContext; + }, implicitRelative, true ), + matchAnyContext = addCombinator( function( elem ) { + return indexOf( checkContext, elem ) > -1; + }, implicitRelative, true ), + matchers = [ function( elem, context, xml ) { + var ret = ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( + ( checkContext = context ).nodeType ? + matchContext( elem, context, xml ) : + matchAnyContext( elem, context, xml ) ); + + // Avoid hanging onto element (issue #299) + checkContext = null; + return ret; + } ]; + + for ( ; i < len; i++ ) { + if ( ( matcher = Expr.relative[ tokens[ i ].type ] ) ) { + matchers = [ addCombinator( elementMatcher( matchers ), matcher ) ]; + } else { + matcher = Expr.filter[ tokens[ i ].type ].apply( null, tokens[ i ].matches ); + + // Return special upon seeing a positional matcher + if ( matcher[ expando ] ) { + + // Find the next relative operator (if any) for proper handling + j = ++i; + for ( ; j < len; j++ ) { + if ( Expr.relative[ tokens[ j ].type ] ) { + break; + } + } + return setMatcher( + i > 1 && elementMatcher( matchers ), + i > 1 && toSelector( + + // If the preceding token was a descendant combinator, insert an implicit any-element `*` + tokens + .slice( 0, i - 1 ) + .concat( { value: tokens[ i - 2 ].type === " " ? "*" : "" } ) + ).replace( rtrim, "$1" ), + matcher, + i < j && matcherFromTokens( tokens.slice( i, j ) ), + j < len && matcherFromTokens( ( tokens = tokens.slice( j ) ) ), + j < len && toSelector( tokens ) + ); + } + matchers.push( matcher ); + } + } + + return elementMatcher( matchers ); +} + +function matcherFromGroupMatchers( elementMatchers, setMatchers ) { + var bySet = setMatchers.length > 0, + byElement = elementMatchers.length > 0, + superMatcher = function( seed, context, xml, results, outermost ) { + var elem, j, matcher, + matchedCount = 0, + i = "0", + unmatched = seed && [], + setMatched = [], + contextBackup = outermostContext, + + // We must always have either seed elements or outermost context + elems = seed || byElement && Expr.find[ "TAG" ]( "*", outermost ), + + // Use integer dirruns iff this is the outermost matcher + dirrunsUnique = ( dirruns += contextBackup == null ? 1 : Math.random() || 0.1 ), + len = elems.length; + + if ( outermost ) { + + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + outermostContext = context == document || context || outermost; + } + + // Add elements passing elementMatchers directly to results + // Support: IE<9, Safari + // Tolerate NodeList properties (IE: "length"; Safari: ) matching elements by id + for ( ; i !== len && ( elem = elems[ i ] ) != null; i++ ) { + if ( byElement && elem ) { + j = 0; + + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( !context && elem.ownerDocument != document ) { + setDocument( elem ); + xml = !documentIsHTML; + } + while ( ( matcher = elementMatchers[ j++ ] ) ) { + if ( matcher( elem, context || document, xml ) ) { + results.push( elem ); + break; + } + } + if ( outermost ) { + dirruns = dirrunsUnique; + } + } + + // Track unmatched elements for set filters + if ( bySet ) { + + // They will have gone through all possible matchers + if ( ( elem = !matcher && elem ) ) { + matchedCount--; + } + + // Lengthen the array for every element, matched or not + if ( seed ) { + unmatched.push( elem ); + } + } + } + + // `i` is now the count of elements visited above, and adding it to `matchedCount` + // makes the latter nonnegative. + matchedCount += i; + + // Apply set filters to unmatched elements + // NOTE: This can be skipped if there are no unmatched elements (i.e., `matchedCount` + // equals `i`), unless we didn't visit _any_ elements in the above loop because we have + // no element matchers and no seed. + // Incrementing an initially-string "0" `i` allows `i` to remain a string only in that + // case, which will result in a "00" `matchedCount` that differs from `i` but is also + // numerically zero. + if ( bySet && i !== matchedCount ) { + j = 0; + while ( ( matcher = setMatchers[ j++ ] ) ) { + matcher( unmatched, setMatched, context, xml ); + } + + if ( seed ) { + + // Reintegrate element matches to eliminate the need for sorting + if ( matchedCount > 0 ) { + while ( i-- ) { + if ( !( unmatched[ i ] || setMatched[ i ] ) ) { + setMatched[ i ] = pop.call( results ); + } + } + } + + // Discard index placeholder values to get only actual matches + setMatched = condense( setMatched ); + } + + // Add matches to results + push.apply( results, setMatched ); + + // Seedless set matches succeeding multiple successful matchers stipulate sorting + if ( outermost && !seed && setMatched.length > 0 && + ( matchedCount + setMatchers.length ) > 1 ) { + + Sizzle.uniqueSort( results ); + } + } + + // Override manipulation of globals by nested matchers + if ( outermost ) { + dirruns = dirrunsUnique; + outermostContext = contextBackup; + } + + return unmatched; + }; + + return bySet ? + markFunction( superMatcher ) : + superMatcher; +} + +compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) { + var i, + setMatchers = [], + elementMatchers = [], + cached = compilerCache[ selector + " " ]; + + if ( !cached ) { + + // Generate a function of recursive functions that can be used to check each element + if ( !match ) { + match = tokenize( selector ); + } + i = match.length; + while ( i-- ) { + cached = matcherFromTokens( match[ i ] ); + if ( cached[ expando ] ) { + setMatchers.push( cached ); + } else { + elementMatchers.push( cached ); + } + } + + // Cache the compiled function + cached = compilerCache( + selector, + matcherFromGroupMatchers( elementMatchers, setMatchers ) + ); + + // Save selector and tokenization + cached.selector = selector; + } + return cached; +}; + +/** + * A low-level selection function that works with Sizzle's compiled + * selector functions + * @param {String|Function} selector A selector or a pre-compiled + * selector function built with Sizzle.compile + * @param {Element} context + * @param {Array} [results] + * @param {Array} [seed] A set of elements to match against + */ +select = Sizzle.select = function( selector, context, results, seed ) { + var i, tokens, token, type, find, + compiled = typeof selector === "function" && selector, + match = !seed && tokenize( ( selector = compiled.selector || selector ) ); + + results = results || []; + + // Try to minimize operations if there is only one selector in the list and no seed + // (the latter of which guarantees us context) + if ( match.length === 1 ) { + + // Reduce context if the leading compound selector is an ID + tokens = match[ 0 ] = match[ 0 ].slice( 0 ); + if ( tokens.length > 2 && ( token = tokens[ 0 ] ).type === "ID" && + context.nodeType === 9 && documentIsHTML && Expr.relative[ tokens[ 1 ].type ] ) { + + context = ( Expr.find[ "ID" ]( token.matches[ 0 ] + .replace( runescape, funescape ), context ) || [] )[ 0 ]; + if ( !context ) { + return results; + + // Precompiled matchers will still verify ancestry, so step up a level + } else if ( compiled ) { + context = context.parentNode; + } + + selector = selector.slice( tokens.shift().value.length ); + } + + // Fetch a seed set for right-to-left matching + i = matchExpr[ "needsContext" ].test( selector ) ? 0 : tokens.length; + while ( i-- ) { + token = tokens[ i ]; + + // Abort if we hit a combinator + if ( Expr.relative[ ( type = token.type ) ] ) { + break; + } + if ( ( find = Expr.find[ type ] ) ) { + + // Search, expanding context for leading sibling combinators + if ( ( seed = find( + token.matches[ 0 ].replace( runescape, funescape ), + rsibling.test( tokens[ 0 ].type ) && testContext( context.parentNode ) || + context + ) ) ) { + + // If seed is empty or no tokens remain, we can return early + tokens.splice( i, 1 ); + selector = seed.length && toSelector( tokens ); + if ( !selector ) { + push.apply( results, seed ); + return results; + } + + break; + } + } + } + } + + // Compile and execute a filtering function if one is not provided + // Provide `match` to avoid retokenization if we modified the selector above + ( compiled || compile( selector, match ) )( + seed, + context, + !documentIsHTML, + results, + !context || rsibling.test( selector ) && testContext( context.parentNode ) || context + ); + return results; +}; + +// One-time assignments + +// Sort stability +support.sortStable = expando.split( "" ).sort( sortOrder ).join( "" ) === expando; + +// Support: Chrome 14-35+ +// Always assume duplicates if they aren't passed to the comparison function +support.detectDuplicates = !!hasDuplicate; + +// Initialize against the default document +setDocument(); + +// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27) +// Detached nodes confoundingly follow *each other* +support.sortDetached = assert( function( el ) { + + // Should return 1, but returns 4 (following) + return el.compareDocumentPosition( document.createElement( "fieldset" ) ) & 1; +} ); + +// Support: IE<8 +// Prevent attribute/property "interpolation" +// https://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx +if ( !assert( function( el ) { + el.innerHTML = ""; + return el.firstChild.getAttribute( "href" ) === "#"; +} ) ) { + addHandle( "type|href|height|width", function( elem, name, isXML ) { + if ( !isXML ) { + return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 ); + } + } ); +} + +// Support: IE<9 +// Use defaultValue in place of getAttribute("value") +if ( !support.attributes || !assert( function( el ) { + el.innerHTML = ""; + el.firstChild.setAttribute( "value", "" ); + return el.firstChild.getAttribute( "value" ) === ""; +} ) ) { + addHandle( "value", function( elem, _name, isXML ) { + if ( !isXML && elem.nodeName.toLowerCase() === "input" ) { + return elem.defaultValue; + } + } ); +} + +// Support: IE<9 +// Use getAttributeNode to fetch booleans when getAttribute lies +if ( !assert( function( el ) { + return el.getAttribute( "disabled" ) == null; +} ) ) { + addHandle( booleans, function( elem, name, isXML ) { + var val; + if ( !isXML ) { + return elem[ name ] === true ? name.toLowerCase() : + ( val = elem.getAttributeNode( name ) ) && val.specified ? + val.value : + null; + } + } ); +} + +return Sizzle; + +} )( window ); + + + +jQuery.find = Sizzle; +jQuery.expr = Sizzle.selectors; + +// Deprecated +jQuery.expr[ ":" ] = jQuery.expr.pseudos; +jQuery.uniqueSort = jQuery.unique = Sizzle.uniqueSort; +jQuery.text = Sizzle.getText; +jQuery.isXMLDoc = Sizzle.isXML; +jQuery.contains = Sizzle.contains; +jQuery.escapeSelector = Sizzle.escape; + + + + +var dir = function( elem, dir, until ) { + var matched = [], + truncate = until !== undefined; + + while ( ( elem = elem[ dir ] ) && elem.nodeType !== 9 ) { + if ( elem.nodeType === 1 ) { + if ( truncate && jQuery( elem ).is( until ) ) { + break; + } + matched.push( elem ); + } + } + return matched; +}; + + +var siblings = function( n, elem ) { + var matched = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + matched.push( n ); + } + } + + return matched; +}; + + +var rneedsContext = jQuery.expr.match.needsContext; + + + +function nodeName( elem, name ) { + + return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); + +} +var rsingleTag = ( /^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i ); + + + +// Implement the identical functionality for filter and not +function winnow( elements, qualifier, not ) { + if ( isFunction( qualifier ) ) { + return jQuery.grep( elements, function( elem, i ) { + return !!qualifier.call( elem, i, elem ) !== not; + } ); + } + + // Single element + if ( qualifier.nodeType ) { + return jQuery.grep( elements, function( elem ) { + return ( elem === qualifier ) !== not; + } ); + } + + // Arraylike of elements (jQuery, arguments, Array) + if ( typeof qualifier !== "string" ) { + return jQuery.grep( elements, function( elem ) { + return ( indexOf.call( qualifier, elem ) > -1 ) !== not; + } ); + } + + // Filtered directly for both simple and complex selectors + return jQuery.filter( qualifier, elements, not ); +} + +jQuery.filter = function( expr, elems, not ) { + var elem = elems[ 0 ]; + + if ( not ) { + expr = ":not(" + expr + ")"; + } + + if ( elems.length === 1 && elem.nodeType === 1 ) { + return jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : []; + } + + return jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) { + return elem.nodeType === 1; + } ) ); +}; + +jQuery.fn.extend( { + find: function( selector ) { + var i, ret, + len = this.length, + self = this; + + if ( typeof selector !== "string" ) { + return this.pushStack( jQuery( selector ).filter( function() { + for ( i = 0; i < len; i++ ) { + if ( jQuery.contains( self[ i ], this ) ) { + return true; + } + } + } ) ); + } + + ret = this.pushStack( [] ); + + for ( i = 0; i < len; i++ ) { + jQuery.find( selector, self[ i ], ret ); + } + + return len > 1 ? jQuery.uniqueSort( ret ) : ret; + }, + filter: function( selector ) { + return this.pushStack( winnow( this, selector || [], false ) ); + }, + not: function( selector ) { + return this.pushStack( winnow( this, selector || [], true ) ); + }, + is: function( selector ) { + return !!winnow( + this, + + // If this is a positional/relative selector, check membership in the returned set + // so $("p:first").is("p:last") won't return true for a doc with two "p". + typeof selector === "string" && rneedsContext.test( selector ) ? + jQuery( selector ) : + selector || [], + false + ).length; + } +} ); + + +// Initialize a jQuery object + + +// A central reference to the root jQuery(document) +var rootjQuery, + + // A simple way to check for HTML strings + // Prioritize #id over to avoid XSS via location.hash (#9521) + // Strict HTML recognition (#11290: must start with <) + // Shortcut simple #id case for speed + rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/, + + init = jQuery.fn.init = function( selector, context, root ) { + var match, elem; + + // HANDLE: $(""), $(null), $(undefined), $(false) + if ( !selector ) { + return this; + } + + // Method init() accepts an alternate rootjQuery + // so migrate can support jQuery.sub (gh-2101) + root = root || rootjQuery; + + // Handle HTML strings + if ( typeof selector === "string" ) { + if ( selector[ 0 ] === "<" && + selector[ selector.length - 1 ] === ">" && + selector.length >= 3 ) { + + // Assume that strings that start and end with <> are HTML and skip the regex check + match = [ null, selector, null ]; + + } else { + match = rquickExpr.exec( selector ); + } + + // Match html or make sure no context is specified for #id + if ( match && ( match[ 1 ] || !context ) ) { + + // HANDLE: $(html) -> $(array) + if ( match[ 1 ] ) { + context = context instanceof jQuery ? context[ 0 ] : context; + + // Option to run scripts is true for back-compat + // Intentionally let the error be thrown if parseHTML is not present + jQuery.merge( this, jQuery.parseHTML( + match[ 1 ], + context && context.nodeType ? context.ownerDocument || context : document, + true + ) ); + + // HANDLE: $(html, props) + if ( rsingleTag.test( match[ 1 ] ) && jQuery.isPlainObject( context ) ) { + for ( match in context ) { + + // Properties of context are called as methods if possible + if ( isFunction( this[ match ] ) ) { + this[ match ]( context[ match ] ); + + // ...and otherwise set as attributes + } else { + this.attr( match, context[ match ] ); + } + } + } + + return this; + + // HANDLE: $(#id) + } else { + elem = document.getElementById( match[ 2 ] ); + + if ( elem ) { + + // Inject the element directly into the jQuery object + this[ 0 ] = elem; + this.length = 1; + } + return this; + } + + // HANDLE: $(expr, $(...)) + } else if ( !context || context.jquery ) { + return ( context || root ).find( selector ); + + // HANDLE: $(expr, context) + // (which is just equivalent to: $(context).find(expr) + } else { + return this.constructor( context ).find( selector ); + } + + // HANDLE: $(DOMElement) + } else if ( selector.nodeType ) { + this[ 0 ] = selector; + this.length = 1; + return this; + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( isFunction( selector ) ) { + return root.ready !== undefined ? + root.ready( selector ) : + + // Execute immediately if ready is not present + selector( jQuery ); + } + + return jQuery.makeArray( selector, this ); + }; + +// Give the init function the jQuery prototype for later instantiation +init.prototype = jQuery.fn; + +// Initialize central reference +rootjQuery = jQuery( document ); + + +var rparentsprev = /^(?:parents|prev(?:Until|All))/, + + // Methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + +jQuery.fn.extend( { + has: function( target ) { + var targets = jQuery( target, this ), + l = targets.length; + + return this.filter( function() { + var i = 0; + for ( ; i < l; i++ ) { + if ( jQuery.contains( this, targets[ i ] ) ) { + return true; + } + } + } ); + }, + + closest: function( selectors, context ) { + var cur, + i = 0, + l = this.length, + matched = [], + targets = typeof selectors !== "string" && jQuery( selectors ); + + // Positional selectors never match, since there's no _selection_ context + if ( !rneedsContext.test( selectors ) ) { + for ( ; i < l; i++ ) { + for ( cur = this[ i ]; cur && cur !== context; cur = cur.parentNode ) { + + // Always skip document fragments + if ( cur.nodeType < 11 && ( targets ? + targets.index( cur ) > -1 : + + // Don't pass non-elements to Sizzle + cur.nodeType === 1 && + jQuery.find.matchesSelector( cur, selectors ) ) ) { + + matched.push( cur ); + break; + } + } + } + } + + return this.pushStack( matched.length > 1 ? jQuery.uniqueSort( matched ) : matched ); + }, + + // Determine the position of an element within the set + index: function( elem ) { + + // No argument, return index in parent + if ( !elem ) { + return ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1; + } + + // Index in selector + if ( typeof elem === "string" ) { + return indexOf.call( jQuery( elem ), this[ 0 ] ); + } + + // Locate the position of the desired element + return indexOf.call( this, + + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[ 0 ] : elem + ); + }, + + add: function( selector, context ) { + return this.pushStack( + jQuery.uniqueSort( + jQuery.merge( this.get(), jQuery( selector, context ) ) + ) + ); + }, + + addBack: function( selector ) { + return this.add( selector == null ? + this.prevObject : this.prevObject.filter( selector ) + ); + } +} ); + +function sibling( cur, dir ) { + while ( ( cur = cur[ dir ] ) && cur.nodeType !== 1 ) {} + return cur; +} + +jQuery.each( { + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, _i, until ) { + return dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return sibling( elem, "nextSibling" ); + }, + prev: function( elem ) { + return sibling( elem, "previousSibling" ); + }, + nextAll: function( elem ) { + return dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, _i, until ) { + return dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, _i, until ) { + return dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return siblings( ( elem.parentNode || {} ).firstChild, elem ); + }, + children: function( elem ) { + return siblings( elem.firstChild ); + }, + contents: function( elem ) { + if ( elem.contentDocument != null && + + // Support: IE 11+ + // elements with no `data` attribute has an object + // `contentDocument` with a `null` prototype. + getProto( elem.contentDocument ) ) { + + return elem.contentDocument; + } + + // Support: IE 9 - 11 only, iOS 7 only, Android Browser <=4.3 only + // Treat the template element as a regular one in browsers that + // don't support it. + if ( nodeName( elem, "template" ) ) { + elem = elem.content || elem; + } + + return jQuery.merge( [], elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var matched = jQuery.map( this, fn, until ); + + if ( name.slice( -5 ) !== "Until" ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + matched = jQuery.filter( selector, matched ); + } + + if ( this.length > 1 ) { + + // Remove duplicates + if ( !guaranteedUnique[ name ] ) { + jQuery.uniqueSort( matched ); + } + + // Reverse order for parents* and prev-derivatives + if ( rparentsprev.test( name ) ) { + matched.reverse(); + } + } + + return this.pushStack( matched ); + }; +} ); +var rnothtmlwhite = ( /[^\x20\t\r\n\f]+/g ); + + + +// Convert String-formatted options into Object-formatted ones +function createOptions( options ) { + var object = {}; + jQuery.each( options.match( rnothtmlwhite ) || [], function( _, flag ) { + object[ flag ] = true; + } ); + return object; +} + +/* + * Create a callback list using the following parameters: + * + * options: an optional list of space-separated options that will change how + * the callback list behaves or a more traditional option object + * + * By default a callback list will act like an event callback list and can be + * "fired" multiple times. + * + * Possible options: + * + * once: will ensure the callback list can only be fired once (like a Deferred) + * + * memory: will keep track of previous values and will call any callback added + * after the list has been fired right away with the latest "memorized" + * values (like a Deferred) + * + * unique: will ensure a callback can only be added once (no duplicate in the list) + * + * stopOnFalse: interrupt callings when a callback returns false + * + */ +jQuery.Callbacks = function( options ) { + + // Convert options from String-formatted to Object-formatted if needed + // (we check in cache first) + options = typeof options === "string" ? + createOptions( options ) : + jQuery.extend( {}, options ); + + var // Flag to know if list is currently firing + firing, + + // Last fire value for non-forgettable lists + memory, + + // Flag to know if list was already fired + fired, + + // Flag to prevent firing + locked, + + // Actual callback list + list = [], + + // Queue of execution data for repeatable lists + queue = [], + + // Index of currently firing callback (modified by add/remove as needed) + firingIndex = -1, + + // Fire callbacks + fire = function() { + + // Enforce single-firing + locked = locked || options.once; + + // Execute callbacks for all pending executions, + // respecting firingIndex overrides and runtime changes + fired = firing = true; + for ( ; queue.length; firingIndex = -1 ) { + memory = queue.shift(); + while ( ++firingIndex < list.length ) { + + // Run callback and check for early termination + if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false && + options.stopOnFalse ) { + + // Jump to end and forget the data so .add doesn't re-fire + firingIndex = list.length; + memory = false; + } + } + } + + // Forget the data if we're done with it + if ( !options.memory ) { + memory = false; + } + + firing = false; + + // Clean up if we're done firing for good + if ( locked ) { + + // Keep an empty list if we have data for future add calls + if ( memory ) { + list = []; + + // Otherwise, this object is spent + } else { + list = ""; + } + } + }, + + // Actual Callbacks object + self = { + + // Add a callback or a collection of callbacks to the list + add: function() { + if ( list ) { + + // If we have memory from a past run, we should fire after adding + if ( memory && !firing ) { + firingIndex = list.length - 1; + queue.push( memory ); + } + + ( function add( args ) { + jQuery.each( args, function( _, arg ) { + if ( isFunction( arg ) ) { + if ( !options.unique || !self.has( arg ) ) { + list.push( arg ); + } + } else if ( arg && arg.length && toType( arg ) !== "string" ) { + + // Inspect recursively + add( arg ); + } + } ); + } )( arguments ); + + if ( memory && !firing ) { + fire(); + } + } + return this; + }, + + // Remove a callback from the list + remove: function() { + jQuery.each( arguments, function( _, arg ) { + var index; + while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { + list.splice( index, 1 ); + + // Handle firing indexes + if ( index <= firingIndex ) { + firingIndex--; + } + } + } ); + return this; + }, + + // Check if a given callback is in the list. + // If no argument is given, return whether or not list has callbacks attached. + has: function( fn ) { + return fn ? + jQuery.inArray( fn, list ) > -1 : + list.length > 0; + }, + + // Remove all callbacks from the list + empty: function() { + if ( list ) { + list = []; + } + return this; + }, + + // Disable .fire and .add + // Abort any current/pending executions + // Clear all callbacks and values + disable: function() { + locked = queue = []; + list = memory = ""; + return this; + }, + disabled: function() { + return !list; + }, + + // Disable .fire + // Also disable .add unless we have memory (since it would have no effect) + // Abort any pending executions + lock: function() { + locked = queue = []; + if ( !memory && !firing ) { + list = memory = ""; + } + return this; + }, + locked: function() { + return !!locked; + }, + + // Call all callbacks with the given context and arguments + fireWith: function( context, args ) { + if ( !locked ) { + args = args || []; + args = [ context, args.slice ? args.slice() : args ]; + queue.push( args ); + if ( !firing ) { + fire(); + } + } + return this; + }, + + // Call all the callbacks with the given arguments + fire: function() { + self.fireWith( this, arguments ); + return this; + }, + + // To know if the callbacks have already been called at least once + fired: function() { + return !!fired; + } + }; + + return self; +}; + + +function Identity( v ) { + return v; +} +function Thrower( ex ) { + throw ex; +} + +function adoptValue( value, resolve, reject, noValue ) { + var method; + + try { + + // Check for promise aspect first to privilege synchronous behavior + if ( value && isFunction( ( method = value.promise ) ) ) { + method.call( value ).done( resolve ).fail( reject ); + + // Other thenables + } else if ( value && isFunction( ( method = value.then ) ) ) { + method.call( value, resolve, reject ); + + // Other non-thenables + } else { + + // Control `resolve` arguments by letting Array#slice cast boolean `noValue` to integer: + // * false: [ value ].slice( 0 ) => resolve( value ) + // * true: [ value ].slice( 1 ) => resolve() + resolve.apply( undefined, [ value ].slice( noValue ) ); + } + + // For Promises/A+, convert exceptions into rejections + // Since jQuery.when doesn't unwrap thenables, we can skip the extra checks appearing in + // Deferred#then to conditionally suppress rejection. + } catch ( value ) { + + // Support: Android 4.0 only + // Strict mode functions invoked without .call/.apply get global-object context + reject.apply( undefined, [ value ] ); + } +} + +jQuery.extend( { + + Deferred: function( func ) { + var tuples = [ + + // action, add listener, callbacks, + // ... .then handlers, argument index, [final state] + [ "notify", "progress", jQuery.Callbacks( "memory" ), + jQuery.Callbacks( "memory" ), 2 ], + [ "resolve", "done", jQuery.Callbacks( "once memory" ), + jQuery.Callbacks( "once memory" ), 0, "resolved" ], + [ "reject", "fail", jQuery.Callbacks( "once memory" ), + jQuery.Callbacks( "once memory" ), 1, "rejected" ] + ], + state = "pending", + promise = { + state: function() { + return state; + }, + always: function() { + deferred.done( arguments ).fail( arguments ); + return this; + }, + "catch": function( fn ) { + return promise.then( null, fn ); + }, + + // Keep pipe for back-compat + pipe: function( /* fnDone, fnFail, fnProgress */ ) { + var fns = arguments; + + return jQuery.Deferred( function( newDefer ) { + jQuery.each( tuples, function( _i, tuple ) { + + // Map tuples (progress, done, fail) to arguments (done, fail, progress) + var fn = isFunction( fns[ tuple[ 4 ] ] ) && fns[ tuple[ 4 ] ]; + + // deferred.progress(function() { bind to newDefer or newDefer.notify }) + // deferred.done(function() { bind to newDefer or newDefer.resolve }) + // deferred.fail(function() { bind to newDefer or newDefer.reject }) + deferred[ tuple[ 1 ] ]( function() { + var returned = fn && fn.apply( this, arguments ); + if ( returned && isFunction( returned.promise ) ) { + returned.promise() + .progress( newDefer.notify ) + .done( newDefer.resolve ) + .fail( newDefer.reject ); + } else { + newDefer[ tuple[ 0 ] + "With" ]( + this, + fn ? [ returned ] : arguments + ); + } + } ); + } ); + fns = null; + } ).promise(); + }, + then: function( onFulfilled, onRejected, onProgress ) { + var maxDepth = 0; + function resolve( depth, deferred, handler, special ) { + return function() { + var that = this, + args = arguments, + mightThrow = function() { + var returned, then; + + // Support: Promises/A+ section 2.3.3.3.3 + // https://promisesaplus.com/#point-59 + // Ignore double-resolution attempts + if ( depth < maxDepth ) { + return; + } + + returned = handler.apply( that, args ); + + // Support: Promises/A+ section 2.3.1 + // https://promisesaplus.com/#point-48 + if ( returned === deferred.promise() ) { + throw new TypeError( "Thenable self-resolution" ); + } + + // Support: Promises/A+ sections 2.3.3.1, 3.5 + // https://promisesaplus.com/#point-54 + // https://promisesaplus.com/#point-75 + // Retrieve `then` only once + then = returned && + + // Support: Promises/A+ section 2.3.4 + // https://promisesaplus.com/#point-64 + // Only check objects and functions for thenability + ( typeof returned === "object" || + typeof returned === "function" ) && + returned.then; + + // Handle a returned thenable + if ( isFunction( then ) ) { + + // Special processors (notify) just wait for resolution + if ( special ) { + then.call( + returned, + resolve( maxDepth, deferred, Identity, special ), + resolve( maxDepth, deferred, Thrower, special ) + ); + + // Normal processors (resolve) also hook into progress + } else { + + // ...and disregard older resolution values + maxDepth++; + + then.call( + returned, + resolve( maxDepth, deferred, Identity, special ), + resolve( maxDepth, deferred, Thrower, special ), + resolve( maxDepth, deferred, Identity, + deferred.notifyWith ) + ); + } + + // Handle all other returned values + } else { + + // Only substitute handlers pass on context + // and multiple values (non-spec behavior) + if ( handler !== Identity ) { + that = undefined; + args = [ returned ]; + } + + // Process the value(s) + // Default process is resolve + ( special || deferred.resolveWith )( that, args ); + } + }, + + // Only normal processors (resolve) catch and reject exceptions + process = special ? + mightThrow : + function() { + try { + mightThrow(); + } catch ( e ) { + + if ( jQuery.Deferred.exceptionHook ) { + jQuery.Deferred.exceptionHook( e, + process.stackTrace ); + } + + // Support: Promises/A+ section 2.3.3.3.4.1 + // https://promisesaplus.com/#point-61 + // Ignore post-resolution exceptions + if ( depth + 1 >= maxDepth ) { + + // Only substitute handlers pass on context + // and multiple values (non-spec behavior) + if ( handler !== Thrower ) { + that = undefined; + args = [ e ]; + } + + deferred.rejectWith( that, args ); + } + } + }; + + // Support: Promises/A+ section 2.3.3.3.1 + // https://promisesaplus.com/#point-57 + // Re-resolve promises immediately to dodge false rejection from + // subsequent errors + if ( depth ) { + process(); + } else { + + // Call an optional hook to record the stack, in case of exception + // since it's otherwise lost when execution goes async + if ( jQuery.Deferred.getStackHook ) { + process.stackTrace = jQuery.Deferred.getStackHook(); + } + window.setTimeout( process ); + } + }; + } + + return jQuery.Deferred( function( newDefer ) { + + // progress_handlers.add( ... ) + tuples[ 0 ][ 3 ].add( + resolve( + 0, + newDefer, + isFunction( onProgress ) ? + onProgress : + Identity, + newDefer.notifyWith + ) + ); + + // fulfilled_handlers.add( ... ) + tuples[ 1 ][ 3 ].add( + resolve( + 0, + newDefer, + isFunction( onFulfilled ) ? + onFulfilled : + Identity + ) + ); + + // rejected_handlers.add( ... ) + tuples[ 2 ][ 3 ].add( + resolve( + 0, + newDefer, + isFunction( onRejected ) ? + onRejected : + Thrower + ) + ); + } ).promise(); + }, + + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + return obj != null ? jQuery.extend( obj, promise ) : promise; + } + }, + deferred = {}; + + // Add list-specific methods + jQuery.each( tuples, function( i, tuple ) { + var list = tuple[ 2 ], + stateString = tuple[ 5 ]; + + // promise.progress = list.add + // promise.done = list.add + // promise.fail = list.add + promise[ tuple[ 1 ] ] = list.add; + + // Handle state + if ( stateString ) { + list.add( + function() { + + // state = "resolved" (i.e., fulfilled) + // state = "rejected" + state = stateString; + }, + + // rejected_callbacks.disable + // fulfilled_callbacks.disable + tuples[ 3 - i ][ 2 ].disable, + + // rejected_handlers.disable + // fulfilled_handlers.disable + tuples[ 3 - i ][ 3 ].disable, + + // progress_callbacks.lock + tuples[ 0 ][ 2 ].lock, + + // progress_handlers.lock + tuples[ 0 ][ 3 ].lock + ); + } + + // progress_handlers.fire + // fulfilled_handlers.fire + // rejected_handlers.fire + list.add( tuple[ 3 ].fire ); + + // deferred.notify = function() { deferred.notifyWith(...) } + // deferred.resolve = function() { deferred.resolveWith(...) } + // deferred.reject = function() { deferred.rejectWith(...) } + deferred[ tuple[ 0 ] ] = function() { + deferred[ tuple[ 0 ] + "With" ]( this === deferred ? undefined : this, arguments ); + return this; + }; + + // deferred.notifyWith = list.fireWith + // deferred.resolveWith = list.fireWith + // deferred.rejectWith = list.fireWith + deferred[ tuple[ 0 ] + "With" ] = list.fireWith; + } ); + + // Make the deferred a promise + promise.promise( deferred ); + + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + + // All done! + return deferred; + }, + + // Deferred helper + when: function( singleValue ) { + var + + // count of uncompleted subordinates + remaining = arguments.length, + + // count of unprocessed arguments + i = remaining, + + // subordinate fulfillment data + resolveContexts = Array( i ), + resolveValues = slice.call( arguments ), + + // the primary Deferred + primary = jQuery.Deferred(), + + // subordinate callback factory + updateFunc = function( i ) { + return function( value ) { + resolveContexts[ i ] = this; + resolveValues[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; + if ( !( --remaining ) ) { + primary.resolveWith( resolveContexts, resolveValues ); + } + }; + }; + + // Single- and empty arguments are adopted like Promise.resolve + if ( remaining <= 1 ) { + adoptValue( singleValue, primary.done( updateFunc( i ) ).resolve, primary.reject, + !remaining ); + + // Use .then() to unwrap secondary thenables (cf. gh-3000) + if ( primary.state() === "pending" || + isFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) { + + return primary.then(); + } + } + + // Multiple arguments are aggregated like Promise.all array elements + while ( i-- ) { + adoptValue( resolveValues[ i ], updateFunc( i ), primary.reject ); + } + + return primary.promise(); + } +} ); + + +// These usually indicate a programmer mistake during development, +// warn about them ASAP rather than swallowing them by default. +var rerrorNames = /^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/; + +jQuery.Deferred.exceptionHook = function( error, stack ) { + + // Support: IE 8 - 9 only + // Console exists when dev tools are open, which can happen at any time + if ( window.console && window.console.warn && error && rerrorNames.test( error.name ) ) { + window.console.warn( "jQuery.Deferred exception: " + error.message, error.stack, stack ); + } +}; + + + + +jQuery.readyException = function( error ) { + window.setTimeout( function() { + throw error; + } ); +}; + + + + +// The deferred used on DOM ready +var readyList = jQuery.Deferred(); + +jQuery.fn.ready = function( fn ) { + + readyList + .then( fn ) + + // Wrap jQuery.readyException in a function so that the lookup + // happens at the time of error handling instead of callback + // registration. + .catch( function( error ) { + jQuery.readyException( error ); + } ); + + return this; +}; + +jQuery.extend( { + + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, + + // A counter to track how many items to wait for before + // the ready event fires. See #6781 + readyWait: 1, + + // Handle when the DOM is ready + ready: function( wait ) { + + // Abort if there are pending holds or we're already ready + if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { + return; + } + + // Remember that the DOM is ready + jQuery.isReady = true; + + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; + } + + // If there are functions bound, to execute + readyList.resolveWith( document, [ jQuery ] ); + } +} ); + +jQuery.ready.then = readyList.then; + +// The ready event handler and self cleanup method +function completed() { + document.removeEventListener( "DOMContentLoaded", completed ); + window.removeEventListener( "load", completed ); + jQuery.ready(); +} + +// Catch cases where $(document).ready() is called +// after the browser event has already occurred. +// Support: IE <=9 - 10 only +// Older IE sometimes signals "interactive" too soon +if ( document.readyState === "complete" || + ( document.readyState !== "loading" && !document.documentElement.doScroll ) ) { + + // Handle it asynchronously to allow scripts the opportunity to delay ready + window.setTimeout( jQuery.ready ); + +} else { + + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", completed ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", completed ); +} + + + + +// Multifunctional method to get and set values of a collection +// The value/s can optionally be executed if it's a function +var access = function( elems, fn, key, value, chainable, emptyGet, raw ) { + var i = 0, + len = elems.length, + bulk = key == null; + + // Sets many values + if ( toType( key ) === "object" ) { + chainable = true; + for ( i in key ) { + access( elems, fn, i, key[ i ], true, emptyGet, raw ); + } + + // Sets one value + } else if ( value !== undefined ) { + chainable = true; + + if ( !isFunction( value ) ) { + raw = true; + } + + if ( bulk ) { + + // Bulk operations run against the entire set + if ( raw ) { + fn.call( elems, value ); + fn = null; + + // ...except when executing function values + } else { + bulk = fn; + fn = function( elem, _key, value ) { + return bulk.call( jQuery( elem ), value ); + }; + } + } + + if ( fn ) { + for ( ; i < len; i++ ) { + fn( + elems[ i ], key, raw ? + value : + value.call( elems[ i ], i, fn( elems[ i ], key ) ) + ); + } + } + } + + if ( chainable ) { + return elems; + } + + // Gets + if ( bulk ) { + return fn.call( elems ); + } + + return len ? fn( elems[ 0 ], key ) : emptyGet; +}; + + +// Matches dashed string for camelizing +var rmsPrefix = /^-ms-/, + rdashAlpha = /-([a-z])/g; + +// Used by camelCase as callback to replace() +function fcamelCase( _all, letter ) { + return letter.toUpperCase(); +} + +// Convert dashed to camelCase; used by the css and data modules +// Support: IE <=9 - 11, Edge 12 - 15 +// Microsoft forgot to hump their vendor prefix (#9572) +function camelCase( string ) { + return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); +} +var acceptData = function( owner ) { + + // Accepts only: + // - Node + // - Node.ELEMENT_NODE + // - Node.DOCUMENT_NODE + // - Object + // - Any + return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType ); +}; + + + + +function Data() { + this.expando = jQuery.expando + Data.uid++; +} + +Data.uid = 1; + +Data.prototype = { + + cache: function( owner ) { + + // Check if the owner object already has a cache + var value = owner[ this.expando ]; + + // If not, create one + if ( !value ) { + value = {}; + + // We can accept data for non-element nodes in modern browsers, + // but we should not, see #8335. + // Always return an empty object. + if ( acceptData( owner ) ) { + + // If it is a node unlikely to be stringify-ed or looped over + // use plain assignment + if ( owner.nodeType ) { + owner[ this.expando ] = value; + + // Otherwise secure it in a non-enumerable property + // configurable must be true to allow the property to be + // deleted when data is removed + } else { + Object.defineProperty( owner, this.expando, { + value: value, + configurable: true + } ); + } + } + } + + return value; + }, + set: function( owner, data, value ) { + var prop, + cache = this.cache( owner ); + + // Handle: [ owner, key, value ] args + // Always use camelCase key (gh-2257) + if ( typeof data === "string" ) { + cache[ camelCase( data ) ] = value; + + // Handle: [ owner, { properties } ] args + } else { + + // Copy the properties one-by-one to the cache object + for ( prop in data ) { + cache[ camelCase( prop ) ] = data[ prop ]; + } + } + return cache; + }, + get: function( owner, key ) { + return key === undefined ? + this.cache( owner ) : + + // Always use camelCase key (gh-2257) + owner[ this.expando ] && owner[ this.expando ][ camelCase( key ) ]; + }, + access: function( owner, key, value ) { + + // In cases where either: + // + // 1. No key was specified + // 2. A string key was specified, but no value provided + // + // Take the "read" path and allow the get method to determine + // which value to return, respectively either: + // + // 1. The entire cache object + // 2. The data stored at the key + // + if ( key === undefined || + ( ( key && typeof key === "string" ) && value === undefined ) ) { + + return this.get( owner, key ); + } + + // When the key is not a string, or both a key and value + // are specified, set or extend (existing objects) with either: + // + // 1. An object of properties + // 2. A key and value + // + this.set( owner, key, value ); + + // Since the "set" path can have two possible entry points + // return the expected data based on which path was taken[*] + return value !== undefined ? value : key; + }, + remove: function( owner, key ) { + var i, + cache = owner[ this.expando ]; + + if ( cache === undefined ) { + return; + } + + if ( key !== undefined ) { + + // Support array or space separated string of keys + if ( Array.isArray( key ) ) { + + // If key is an array of keys... + // We always set camelCase keys, so remove that. + key = key.map( camelCase ); + } else { + key = camelCase( key ); + + // If a key with the spaces exists, use it. + // Otherwise, create an array by matching non-whitespace + key = key in cache ? + [ key ] : + ( key.match( rnothtmlwhite ) || [] ); + } + + i = key.length; + + while ( i-- ) { + delete cache[ key[ i ] ]; + } + } + + // Remove the expando if there's no more data + if ( key === undefined || jQuery.isEmptyObject( cache ) ) { + + // Support: Chrome <=35 - 45 + // Webkit & Blink performance suffers when deleting properties + // from DOM nodes, so set to undefined instead + // https://bugs.chromium.org/p/chromium/issues/detail?id=378607 (bug restricted) + if ( owner.nodeType ) { + owner[ this.expando ] = undefined; + } else { + delete owner[ this.expando ]; + } + } + }, + hasData: function( owner ) { + var cache = owner[ this.expando ]; + return cache !== undefined && !jQuery.isEmptyObject( cache ); + } +}; +var dataPriv = new Data(); + +var dataUser = new Data(); + + + +// Implementation Summary +// +// 1. Enforce API surface and semantic compatibility with 1.9.x branch +// 2. Improve the module's maintainability by reducing the storage +// paths to a single mechanism. +// 3. Use the same single mechanism to support "private" and "user" data. +// 4. _Never_ expose "private" data to user code (TODO: Drop _data, _removeData) +// 5. Avoid exposing implementation details on user objects (eg. expando properties) +// 6. Provide a clear path for implementation upgrade to WeakMap in 2014 + +var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/, + rmultiDash = /[A-Z]/g; + +function getData( data ) { + if ( data === "true" ) { + return true; + } + + if ( data === "false" ) { + return false; + } + + if ( data === "null" ) { + return null; + } + + // Only convert to a number if it doesn't change the string + if ( data === +data + "" ) { + return +data; + } + + if ( rbrace.test( data ) ) { + return JSON.parse( data ); + } + + return data; +} + +function dataAttr( elem, key, data ) { + var name; + + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { + name = "data-" + key.replace( rmultiDash, "-$&" ).toLowerCase(); + data = elem.getAttribute( name ); + + if ( typeof data === "string" ) { + try { + data = getData( data ); + } catch ( e ) {} + + // Make sure we set the data so it isn't changed later + dataUser.set( elem, key, data ); + } else { + data = undefined; + } + } + return data; +} + +jQuery.extend( { + hasData: function( elem ) { + return dataUser.hasData( elem ) || dataPriv.hasData( elem ); + }, + + data: function( elem, name, data ) { + return dataUser.access( elem, name, data ); + }, + + removeData: function( elem, name ) { + dataUser.remove( elem, name ); + }, + + // TODO: Now that all calls to _data and _removeData have been replaced + // with direct calls to dataPriv methods, these can be deprecated. + _data: function( elem, name, data ) { + return dataPriv.access( elem, name, data ); + }, + + _removeData: function( elem, name ) { + dataPriv.remove( elem, name ); + } +} ); + +jQuery.fn.extend( { + data: function( key, value ) { + var i, name, data, + elem = this[ 0 ], + attrs = elem && elem.attributes; + + // Gets all values + if ( key === undefined ) { + if ( this.length ) { + data = dataUser.get( elem ); + + if ( elem.nodeType === 1 && !dataPriv.get( elem, "hasDataAttrs" ) ) { + i = attrs.length; + while ( i-- ) { + + // Support: IE 11 only + // The attrs elements can be null (#14894) + if ( attrs[ i ] ) { + name = attrs[ i ].name; + if ( name.indexOf( "data-" ) === 0 ) { + name = camelCase( name.slice( 5 ) ); + dataAttr( elem, name, data[ name ] ); + } + } + } + dataPriv.set( elem, "hasDataAttrs", true ); + } + } + + return data; + } + + // Sets multiple values + if ( typeof key === "object" ) { + return this.each( function() { + dataUser.set( this, key ); + } ); + } + + return access( this, function( value ) { + var data; + + // The calling jQuery object (element matches) is not empty + // (and therefore has an element appears at this[ 0 ]) and the + // `value` parameter was not undefined. An empty jQuery object + // will result in `undefined` for elem = this[ 0 ] which will + // throw an exception if an attempt to read a data cache is made. + if ( elem && value === undefined ) { + + // Attempt to get data from the cache + // The key will always be camelCased in Data + data = dataUser.get( elem, key ); + if ( data !== undefined ) { + return data; + } + + // Attempt to "discover" the data in + // HTML5 custom data-* attrs + data = dataAttr( elem, key ); + if ( data !== undefined ) { + return data; + } + + // We tried really hard, but the data doesn't exist. + return; + } + + // Set the data... + this.each( function() { + + // We always store the camelCased key + dataUser.set( this, key, value ); + } ); + }, null, value, arguments.length > 1, null, true ); + }, + + removeData: function( key ) { + return this.each( function() { + dataUser.remove( this, key ); + } ); + } +} ); + + +jQuery.extend( { + queue: function( elem, type, data ) { + var queue; + + if ( elem ) { + type = ( type || "fx" ) + "queue"; + queue = dataPriv.get( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if ( data ) { + if ( !queue || Array.isArray( data ) ) { + queue = dataPriv.access( elem, type, jQuery.makeArray( data ) ); + } else { + queue.push( data ); + } + } + return queue || []; + } + }, + + dequeue: function( elem, type ) { + type = type || "fx"; + + var queue = jQuery.queue( elem, type ), + startLength = queue.length, + fn = queue.shift(), + hooks = jQuery._queueHooks( elem, type ), + next = function() { + jQuery.dequeue( elem, type ); + }; + + // If the fx queue is dequeued, always remove the progress sentinel + if ( fn === "inprogress" ) { + fn = queue.shift(); + startLength--; + } + + if ( fn ) { + + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if ( type === "fx" ) { + queue.unshift( "inprogress" ); + } + + // Clear up the last queue stop function + delete hooks.stop; + fn.call( elem, next, hooks ); + } + + if ( !startLength && hooks ) { + hooks.empty.fire(); + } + }, + + // Not public - generate a queueHooks object, or return the current one + _queueHooks: function( elem, type ) { + var key = type + "queueHooks"; + return dataPriv.get( elem, key ) || dataPriv.access( elem, key, { + empty: jQuery.Callbacks( "once memory" ).add( function() { + dataPriv.remove( elem, [ type + "queue", key ] ); + } ) + } ); + } +} ); + +jQuery.fn.extend( { + queue: function( type, data ) { + var setter = 2; + + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + setter--; + } + + if ( arguments.length < setter ) { + return jQuery.queue( this[ 0 ], type ); + } + + return data === undefined ? + this : + this.each( function() { + var queue = jQuery.queue( this, type, data ); + + // Ensure a hooks for this queue + jQuery._queueHooks( this, type ); + + if ( type === "fx" && queue[ 0 ] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + } ); + }, + dequeue: function( type ) { + return this.each( function() { + jQuery.dequeue( this, type ); + } ); + }, + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); + }, + + // Get a promise resolved when queues of a certain type + // are emptied (fx is the type by default) + promise: function( type, obj ) { + var tmp, + count = 1, + defer = jQuery.Deferred(), + elements = this, + i = this.length, + resolve = function() { + if ( !( --count ) ) { + defer.resolveWith( elements, [ elements ] ); + } + }; + + if ( typeof type !== "string" ) { + obj = type; + type = undefined; + } + type = type || "fx"; + + while ( i-- ) { + tmp = dataPriv.get( elements[ i ], type + "queueHooks" ); + if ( tmp && tmp.empty ) { + count++; + tmp.empty.add( resolve ); + } + } + resolve(); + return defer.promise( obj ); + } +} ); +var pnum = ( /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/ ).source; + +var rcssNum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" ); + + +var cssExpand = [ "Top", "Right", "Bottom", "Left" ]; + +var documentElement = document.documentElement; + + + + var isAttached = function( elem ) { + return jQuery.contains( elem.ownerDocument, elem ); + }, + composed = { composed: true }; + + // Support: IE 9 - 11+, Edge 12 - 18+, iOS 10.0 - 10.2 only + // Check attachment across shadow DOM boundaries when possible (gh-3504) + // Support: iOS 10.0-10.2 only + // Early iOS 10 versions support `attachShadow` but not `getRootNode`, + // leading to errors. We need to check for `getRootNode`. + if ( documentElement.getRootNode ) { + isAttached = function( elem ) { + return jQuery.contains( elem.ownerDocument, elem ) || + elem.getRootNode( composed ) === elem.ownerDocument; + }; + } +var isHiddenWithinTree = function( elem, el ) { + + // isHiddenWithinTree might be called from jQuery#filter function; + // in that case, element will be second argument + elem = el || elem; + + // Inline style trumps all + return elem.style.display === "none" || + elem.style.display === "" && + + // Otherwise, check computed style + // Support: Firefox <=43 - 45 + // Disconnected elements can have computed display: none, so first confirm that elem is + // in the document. + isAttached( elem ) && + + jQuery.css( elem, "display" ) === "none"; + }; + + + +function adjustCSS( elem, prop, valueParts, tween ) { + var adjusted, scale, + maxIterations = 20, + currentValue = tween ? + function() { + return tween.cur(); + } : + function() { + return jQuery.css( elem, prop, "" ); + }, + initial = currentValue(), + unit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ), + + // Starting value computation is required for potential unit mismatches + initialInUnit = elem.nodeType && + ( jQuery.cssNumber[ prop ] || unit !== "px" && +initial ) && + rcssNum.exec( jQuery.css( elem, prop ) ); + + if ( initialInUnit && initialInUnit[ 3 ] !== unit ) { + + // Support: Firefox <=54 + // Halve the iteration target value to prevent interference from CSS upper bounds (gh-2144) + initial = initial / 2; + + // Trust units reported by jQuery.css + unit = unit || initialInUnit[ 3 ]; + + // Iteratively approximate from a nonzero starting point + initialInUnit = +initial || 1; + + while ( maxIterations-- ) { + + // Evaluate and update our best guess (doubling guesses that zero out). + // Finish if the scale equals or crosses 1 (making the old*new product non-positive). + jQuery.style( elem, prop, initialInUnit + unit ); + if ( ( 1 - scale ) * ( 1 - ( scale = currentValue() / initial || 0.5 ) ) <= 0 ) { + maxIterations = 0; + } + initialInUnit = initialInUnit / scale; + + } + + initialInUnit = initialInUnit * 2; + jQuery.style( elem, prop, initialInUnit + unit ); + + // Make sure we update the tween properties later on + valueParts = valueParts || []; + } + + if ( valueParts ) { + initialInUnit = +initialInUnit || +initial || 0; + + // Apply relative offset (+=/-=) if specified + adjusted = valueParts[ 1 ] ? + initialInUnit + ( valueParts[ 1 ] + 1 ) * valueParts[ 2 ] : + +valueParts[ 2 ]; + if ( tween ) { + tween.unit = unit; + tween.start = initialInUnit; + tween.end = adjusted; + } + } + return adjusted; +} + + +var defaultDisplayMap = {}; + +function getDefaultDisplay( elem ) { + var temp, + doc = elem.ownerDocument, + nodeName = elem.nodeName, + display = defaultDisplayMap[ nodeName ]; + + if ( display ) { + return display; + } + + temp = doc.body.appendChild( doc.createElement( nodeName ) ); + display = jQuery.css( temp, "display" ); + + temp.parentNode.removeChild( temp ); + + if ( display === "none" ) { + display = "block"; + } + defaultDisplayMap[ nodeName ] = display; + + return display; +} + +function showHide( elements, show ) { + var display, elem, + values = [], + index = 0, + length = elements.length; + + // Determine new display value for elements that need to change + for ( ; index < length; index++ ) { + elem = elements[ index ]; + if ( !elem.style ) { + continue; + } + + display = elem.style.display; + if ( show ) { + + // Since we force visibility upon cascade-hidden elements, an immediate (and slow) + // check is required in this first loop unless we have a nonempty display value (either + // inline or about-to-be-restored) + if ( display === "none" ) { + values[ index ] = dataPriv.get( elem, "display" ) || null; + if ( !values[ index ] ) { + elem.style.display = ""; + } + } + if ( elem.style.display === "" && isHiddenWithinTree( elem ) ) { + values[ index ] = getDefaultDisplay( elem ); + } + } else { + if ( display !== "none" ) { + values[ index ] = "none"; + + // Remember what we're overwriting + dataPriv.set( elem, "display", display ); + } + } + } + + // Set the display of the elements in a second loop to avoid constant reflow + for ( index = 0; index < length; index++ ) { + if ( values[ index ] != null ) { + elements[ index ].style.display = values[ index ]; + } + } + + return elements; +} + +jQuery.fn.extend( { + show: function() { + return showHide( this, true ); + }, + hide: function() { + return showHide( this ); + }, + toggle: function( state ) { + if ( typeof state === "boolean" ) { + return state ? this.show() : this.hide(); + } + + return this.each( function() { + if ( isHiddenWithinTree( this ) ) { + jQuery( this ).show(); + } else { + jQuery( this ).hide(); + } + } ); + } +} ); +var rcheckableType = ( /^(?:checkbox|radio)$/i ); + +var rtagName = ( /<([a-z][^\/\0>\x20\t\r\n\f]*)/i ); + +var rscriptType = ( /^$|^module$|\/(?:java|ecma)script/i ); + + + +( function() { + var fragment = document.createDocumentFragment(), + div = fragment.appendChild( document.createElement( "div" ) ), + input = document.createElement( "input" ); + + // Support: Android 4.0 - 4.3 only + // Check state lost if the name is set (#11217) + // Support: Windows Web Apps (WWA) + // `name` and `type` must use .setAttribute for WWA (#14901) + input.setAttribute( "type", "radio" ); + input.setAttribute( "checked", "checked" ); + input.setAttribute( "name", "t" ); + + div.appendChild( input ); + + // Support: Android <=4.1 only + // Older WebKit doesn't clone checked state correctly in fragments + support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked; + + // Support: IE <=11 only + // Make sure textarea (and checkbox) defaultValue is properly cloned + div.innerHTML = ""; + support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; + + // Support: IE <=9 only + // IE <=9 replaces "; + support.option = !!div.lastChild; +} )(); + + +// We have to close these tags to support XHTML (#13200) +var wrapMap = { + + // XHTML parsers do not magically insert elements in the + // same way that tag soup parsers do. So we cannot shorten + // this by omitting or other required elements. + thead: [ 1, "", "
      " ], + col: [ 2, "", "
      " ], + tr: [ 2, "", "
      " ], + td: [ 3, "", "
      " ], + + _default: [ 0, "", "" ] +}; + +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + +// Support: IE <=9 only +if ( !support.option ) { + wrapMap.optgroup = wrapMap.option = [ 1, "" ]; +} + + +function getAll( context, tag ) { + + // Support: IE <=9 - 11 only + // Use typeof to avoid zero-argument method invocation on host objects (#15151) + var ret; + + if ( typeof context.getElementsByTagName !== "undefined" ) { + ret = context.getElementsByTagName( tag || "*" ); + + } else if ( typeof context.querySelectorAll !== "undefined" ) { + ret = context.querySelectorAll( tag || "*" ); + + } else { + ret = []; + } + + if ( tag === undefined || tag && nodeName( context, tag ) ) { + return jQuery.merge( [ context ], ret ); + } + + return ret; +} + + +// Mark scripts as having already been evaluated +function setGlobalEval( elems, refElements ) { + var i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + dataPriv.set( + elems[ i ], + "globalEval", + !refElements || dataPriv.get( refElements[ i ], "globalEval" ) + ); + } +} + + +var rhtml = /<|&#?\w+;/; + +function buildFragment( elems, context, scripts, selection, ignored ) { + var elem, tmp, tag, wrap, attached, j, + fragment = context.createDocumentFragment(), + nodes = [], + i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + elem = elems[ i ]; + + if ( elem || elem === 0 ) { + + // Add nodes directly + if ( toType( elem ) === "object" ) { + + // Support: Android <=4.0 only, PhantomJS 1 only + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); + + // Convert non-html into a text node + } else if ( !rhtml.test( elem ) ) { + nodes.push( context.createTextNode( elem ) ); + + // Convert html into DOM nodes + } else { + tmp = tmp || fragment.appendChild( context.createElement( "div" ) ); + + // Deserialize a standard representation + tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase(); + wrap = wrapMap[ tag ] || wrapMap._default; + tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ]; + + // Descend through wrappers to the right content + j = wrap[ 0 ]; + while ( j-- ) { + tmp = tmp.lastChild; + } + + // Support: Android <=4.0 only, PhantomJS 1 only + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( nodes, tmp.childNodes ); + + // Remember the top-level container + tmp = fragment.firstChild; + + // Ensure the created nodes are orphaned (#12392) + tmp.textContent = ""; + } + } + } + + // Remove wrapper from fragment + fragment.textContent = ""; + + i = 0; + while ( ( elem = nodes[ i++ ] ) ) { + + // Skip elements already in the context collection (trac-4087) + if ( selection && jQuery.inArray( elem, selection ) > -1 ) { + if ( ignored ) { + ignored.push( elem ); + } + continue; + } + + attached = isAttached( elem ); + + // Append to fragment + tmp = getAll( fragment.appendChild( elem ), "script" ); + + // Preserve script evaluation history + if ( attached ) { + setGlobalEval( tmp ); + } + + // Capture executables + if ( scripts ) { + j = 0; + while ( ( elem = tmp[ j++ ] ) ) { + if ( rscriptType.test( elem.type || "" ) ) { + scripts.push( elem ); + } + } + } + } + + return fragment; +} + + +var rtypenamespace = /^([^.]*)(?:\.(.+)|)/; + +function returnTrue() { + return true; +} + +function returnFalse() { + return false; +} + +// Support: IE <=9 - 11+ +// focus() and blur() are asynchronous, except when they are no-op. +// So expect focus to be synchronous when the element is already active, +// and blur to be synchronous when the element is not already active. +// (focus and blur are always synchronous in other supported browsers, +// this just defines when we can count on it). +function expectSync( elem, type ) { + return ( elem === safeActiveElement() ) === ( type === "focus" ); +} + +// Support: IE <=9 only +// Accessing document.activeElement can throw unexpectedly +// https://bugs.jquery.com/ticket/13393 +function safeActiveElement() { + try { + return document.activeElement; + } catch ( err ) { } +} + +function on( elem, types, selector, data, fn, one ) { + var origFn, type; + + // Types can be a map of types/handlers + if ( typeof types === "object" ) { + + // ( types-Object, selector, data ) + if ( typeof selector !== "string" ) { + + // ( types-Object, data ) + data = data || selector; + selector = undefined; + } + for ( type in types ) { + on( elem, type, selector, data, types[ type ], one ); + } + return elem; + } + + if ( data == null && fn == null ) { + + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; + } + } + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return elem; + } + + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + + // Can use an empty set, since event contains the info + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return elem.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + } ); +} + +/* + * Helper functions for managing events -- not part of the public interface. + * Props to Dean Edwards' addEvent library for many of the ideas. + */ +jQuery.event = { + + global: {}, + + add: function( elem, types, handler, data, selector ) { + + var handleObjIn, eventHandle, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = dataPriv.get( elem ); + + // Only attach events to objects that accept data + if ( !acceptData( elem ) ) { + return; + } + + // Caller can pass in an object of custom data in lieu of the handler + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + selector = handleObjIn.selector; + } + + // Ensure that invalid selectors throw exceptions at attach time + // Evaluate against documentElement in case elem is a non-element node (e.g., document) + if ( selector ) { + jQuery.find.matchesSelector( documentElement, selector ); + } + + // Make sure that the handler has a unique ID, used to find/remove it later + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + // Init the element's event structure and main handler, if this is the first + if ( !( events = elemData.events ) ) { + events = elemData.events = Object.create( null ); + } + if ( !( eventHandle = elemData.handle ) ) { + eventHandle = elemData.handle = function( e ) { + + // Discard the second event of a jQuery.event.trigger() and + // when an event is called after a page has unloaded + return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ? + jQuery.event.dispatch.apply( elem, arguments ) : undefined; + }; + } + + // Handle multiple events separated by a space + types = ( types || "" ).match( rnothtmlwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[ t ] ) || []; + type = origType = tmp[ 1 ]; + namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); + + // There *must* be a type, no attaching namespace-only handlers + if ( !type ) { + continue; + } + + // If event changes its type, use the special event handlers for the changed type + special = jQuery.event.special[ type ] || {}; + + // If selector defined, determine special event api type, otherwise given type + type = ( selector ? special.delegateType : special.bindType ) || type; + + // Update special based on newly reset type + special = jQuery.event.special[ type ] || {}; + + // handleObj is passed to all event handlers + handleObj = jQuery.extend( { + type: type, + origType: origType, + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + needsContext: selector && jQuery.expr.match.needsContext.test( selector ), + namespace: namespaces.join( "." ) + }, handleObjIn ); + + // Init the event handler queue if we're the first + if ( !( handlers = events[ type ] ) ) { + handlers = events[ type ] = []; + handlers.delegateCount = 0; + + // Only use addEventListener if the special events handler returns false + if ( !special.setup || + special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle ); + } + } + } + + if ( special.add ) { + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + + // Add to the element's handler list, delegates in front + if ( selector ) { + handlers.splice( handlers.delegateCount++, 0, handleObj ); + } else { + handlers.push( handleObj ); + } + + // Keep track of which events have ever been used, for event optimization + jQuery.event.global[ type ] = true; + } + + }, + + // Detach an event or set of events from an element + remove: function( elem, types, handler, selector, mappedTypes ) { + + var j, origCount, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = dataPriv.hasData( elem ) && dataPriv.get( elem ); + + if ( !elemData || !( events = elemData.events ) ) { + return; + } + + // Once for each type.namespace in types; type may be omitted + types = ( types || "" ).match( rnothtmlwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[ t ] ) || []; + type = origType = tmp[ 1 ]; + namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); + + // Unbind all events (on this namespace, if provided) for the element + if ( !type ) { + for ( type in events ) { + jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); + } + continue; + } + + special = jQuery.event.special[ type ] || {}; + type = ( selector ? special.delegateType : special.bindType ) || type; + handlers = events[ type ] || []; + tmp = tmp[ 2 ] && + new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ); + + // Remove matching events + origCount = j = handlers.length; + while ( j-- ) { + handleObj = handlers[ j ]; + + if ( ( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !tmp || tmp.test( handleObj.namespace ) ) && + ( !selector || selector === handleObj.selector || + selector === "**" && handleObj.selector ) ) { + handlers.splice( j, 1 ); + + if ( handleObj.selector ) { + handlers.delegateCount--; + } + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + } + + // Remove generic event handler if we removed something and no more handlers exist + // (avoids potential for endless recursion during removal of special event handlers) + if ( origCount && !handlers.length ) { + if ( !special.teardown || + special.teardown.call( elem, namespaces, elemData.handle ) === false ) { + + jQuery.removeEvent( elem, type, elemData.handle ); + } + + delete events[ type ]; + } + } + + // Remove data and the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + dataPriv.remove( elem, "handle events" ); + } + }, + + dispatch: function( nativeEvent ) { + + var i, j, ret, matched, handleObj, handlerQueue, + args = new Array( arguments.length ), + + // Make a writable jQuery.Event from the native event object + event = jQuery.event.fix( nativeEvent ), + + handlers = ( + dataPriv.get( this, "events" ) || Object.create( null ) + )[ event.type ] || [], + special = jQuery.event.special[ event.type ] || {}; + + // Use the fix-ed jQuery.Event rather than the (read-only) native event + args[ 0 ] = event; + + for ( i = 1; i < arguments.length; i++ ) { + args[ i ] = arguments[ i ]; + } + + event.delegateTarget = this; + + // Call the preDispatch hook for the mapped type, and let it bail if desired + if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { + return; + } + + // Determine handlers + handlerQueue = jQuery.event.handlers.call( this, event, handlers ); + + // Run delegates first; they may want to stop propagation beneath us + i = 0; + while ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) { + event.currentTarget = matched.elem; + + j = 0; + while ( ( handleObj = matched.handlers[ j++ ] ) && + !event.isImmediatePropagationStopped() ) { + + // If the event is namespaced, then each handler is only invoked if it is + // specially universal or its namespaces are a superset of the event's. + if ( !event.rnamespace || handleObj.namespace === false || + event.rnamespace.test( handleObj.namespace ) ) { + + event.handleObj = handleObj; + event.data = handleObj.data; + + ret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle || + handleObj.handler ).apply( matched.elem, args ); + + if ( ret !== undefined ) { + if ( ( event.result = ret ) === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + } + + // Call the postDispatch hook for the mapped type + if ( special.postDispatch ) { + special.postDispatch.call( this, event ); + } + + return event.result; + }, + + handlers: function( event, handlers ) { + var i, handleObj, sel, matchedHandlers, matchedSelectors, + handlerQueue = [], + delegateCount = handlers.delegateCount, + cur = event.target; + + // Find delegate handlers + if ( delegateCount && + + // Support: IE <=9 + // Black-hole SVG instance trees (trac-13180) + cur.nodeType && + + // Support: Firefox <=42 + // Suppress spec-violating clicks indicating a non-primary pointer button (trac-3861) + // https://www.w3.org/TR/DOM-Level-3-Events/#event-type-click + // Support: IE 11 only + // ...but not arrow key "clicks" of radio inputs, which can have `button` -1 (gh-2343) + !( event.type === "click" && event.button >= 1 ) ) { + + for ( ; cur !== this; cur = cur.parentNode || this ) { + + // Don't check non-elements (#13208) + // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) + if ( cur.nodeType === 1 && !( event.type === "click" && cur.disabled === true ) ) { + matchedHandlers = []; + matchedSelectors = {}; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; + + // Don't conflict with Object.prototype properties (#13203) + sel = handleObj.selector + " "; + + if ( matchedSelectors[ sel ] === undefined ) { + matchedSelectors[ sel ] = handleObj.needsContext ? + jQuery( sel, this ).index( cur ) > -1 : + jQuery.find( sel, this, null, [ cur ] ).length; + } + if ( matchedSelectors[ sel ] ) { + matchedHandlers.push( handleObj ); + } + } + if ( matchedHandlers.length ) { + handlerQueue.push( { elem: cur, handlers: matchedHandlers } ); + } + } + } + } + + // Add the remaining (directly-bound) handlers + cur = this; + if ( delegateCount < handlers.length ) { + handlerQueue.push( { elem: cur, handlers: handlers.slice( delegateCount ) } ); + } + + return handlerQueue; + }, + + addProp: function( name, hook ) { + Object.defineProperty( jQuery.Event.prototype, name, { + enumerable: true, + configurable: true, + + get: isFunction( hook ) ? + function() { + if ( this.originalEvent ) { + return hook( this.originalEvent ); + } + } : + function() { + if ( this.originalEvent ) { + return this.originalEvent[ name ]; + } + }, + + set: function( value ) { + Object.defineProperty( this, name, { + enumerable: true, + configurable: true, + writable: true, + value: value + } ); + } + } ); + }, + + fix: function( originalEvent ) { + return originalEvent[ jQuery.expando ] ? + originalEvent : + new jQuery.Event( originalEvent ); + }, + + special: { + load: { + + // Prevent triggered image.load events from bubbling to window.load + noBubble: true + }, + click: { + + // Utilize native event to ensure correct state for checkable inputs + setup: function( data ) { + + // For mutual compressibility with _default, replace `this` access with a local var. + // `|| data` is dead code meant only to preserve the variable through minification. + var el = this || data; + + // Claim the first handler + if ( rcheckableType.test( el.type ) && + el.click && nodeName( el, "input" ) ) { + + // dataPriv.set( el, "click", ... ) + leverageNative( el, "click", returnTrue ); + } + + // Return false to allow normal processing in the caller + return false; + }, + trigger: function( data ) { + + // For mutual compressibility with _default, replace `this` access with a local var. + // `|| data` is dead code meant only to preserve the variable through minification. + var el = this || data; + + // Force setup before triggering a click + if ( rcheckableType.test( el.type ) && + el.click && nodeName( el, "input" ) ) { + + leverageNative( el, "click" ); + } + + // Return non-false to allow normal event-path propagation + return true; + }, + + // For cross-browser consistency, suppress native .click() on links + // Also prevent it if we're currently inside a leveraged native-event stack + _default: function( event ) { + var target = event.target; + return rcheckableType.test( target.type ) && + target.click && nodeName( target, "input" ) && + dataPriv.get( target, "click" ) || + nodeName( target, "a" ); + } + }, + + beforeunload: { + postDispatch: function( event ) { + + // Support: Firefox 20+ + // Firefox doesn't alert if the returnValue field is not set. + if ( event.result !== undefined && event.originalEvent ) { + event.originalEvent.returnValue = event.result; + } + } + } + } +}; + +// Ensure the presence of an event listener that handles manually-triggered +// synthetic events by interrupting progress until reinvoked in response to +// *native* events that it fires directly, ensuring that state changes have +// already occurred before other listeners are invoked. +function leverageNative( el, type, expectSync ) { + + // Missing expectSync indicates a trigger call, which must force setup through jQuery.event.add + if ( !expectSync ) { + if ( dataPriv.get( el, type ) === undefined ) { + jQuery.event.add( el, type, returnTrue ); + } + return; + } + + // Register the controller as a special universal handler for all event namespaces + dataPriv.set( el, type, false ); + jQuery.event.add( el, type, { + namespace: false, + handler: function( event ) { + var notAsync, result, + saved = dataPriv.get( this, type ); + + if ( ( event.isTrigger & 1 ) && this[ type ] ) { + + // Interrupt processing of the outer synthetic .trigger()ed event + // Saved data should be false in such cases, but might be a leftover capture object + // from an async native handler (gh-4350) + if ( !saved.length ) { + + // Store arguments for use when handling the inner native event + // There will always be at least one argument (an event object), so this array + // will not be confused with a leftover capture object. + saved = slice.call( arguments ); + dataPriv.set( this, type, saved ); + + // Trigger the native event and capture its result + // Support: IE <=9 - 11+ + // focus() and blur() are asynchronous + notAsync = expectSync( this, type ); + this[ type ](); + result = dataPriv.get( this, type ); + if ( saved !== result || notAsync ) { + dataPriv.set( this, type, false ); + } else { + result = {}; + } + if ( saved !== result ) { + + // Cancel the outer synthetic event + event.stopImmediatePropagation(); + event.preventDefault(); + + // Support: Chrome 86+ + // In Chrome, if an element having a focusout handler is blurred by + // clicking outside of it, it invokes the handler synchronously. If + // that handler calls `.remove()` on the element, the data is cleared, + // leaving `result` undefined. We need to guard against this. + return result && result.value; + } + + // If this is an inner synthetic event for an event with a bubbling surrogate + // (focus or blur), assume that the surrogate already propagated from triggering the + // native event and prevent that from happening again here. + // This technically gets the ordering wrong w.r.t. to `.trigger()` (in which the + // bubbling surrogate propagates *after* the non-bubbling base), but that seems + // less bad than duplication. + } else if ( ( jQuery.event.special[ type ] || {} ).delegateType ) { + event.stopPropagation(); + } + + // If this is a native event triggered above, everything is now in order + // Fire an inner synthetic event with the original arguments + } else if ( saved.length ) { + + // ...and capture the result + dataPriv.set( this, type, { + value: jQuery.event.trigger( + + // Support: IE <=9 - 11+ + // Extend with the prototype to reset the above stopImmediatePropagation() + jQuery.extend( saved[ 0 ], jQuery.Event.prototype ), + saved.slice( 1 ), + this + ) + } ); + + // Abort handling of the native event + event.stopImmediatePropagation(); + } + } + } ); +} + +jQuery.removeEvent = function( elem, type, handle ) { + + // This "if" is needed for plain objects + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle ); + } +}; + +jQuery.Event = function( src, props ) { + + // Allow instantiation without the 'new' keyword + if ( !( this instanceof jQuery.Event ) ) { + return new jQuery.Event( src, props ); + } + + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + + // Events bubbling up the document may have been marked as prevented + // by a handler lower down the tree; reflect the correct value. + this.isDefaultPrevented = src.defaultPrevented || + src.defaultPrevented === undefined && + + // Support: Android <=2.3 only + src.returnValue === false ? + returnTrue : + returnFalse; + + // Create target properties + // Support: Safari <=6 - 7 only + // Target should not be a text node (#504, #13143) + this.target = ( src.target && src.target.nodeType === 3 ) ? + src.target.parentNode : + src.target; + + this.currentTarget = src.currentTarget; + this.relatedTarget = src.relatedTarget; + + // Event type + } else { + this.type = src; + } + + // Put explicitly provided properties onto the event object + if ( props ) { + jQuery.extend( this, props ); + } + + // Create a timestamp if incoming event doesn't have one + this.timeStamp = src && src.timeStamp || Date.now(); + + // Mark it as fixed + this[ jQuery.expando ] = true; +}; + +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// https://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + constructor: jQuery.Event, + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse, + isSimulated: false, + + preventDefault: function() { + var e = this.originalEvent; + + this.isDefaultPrevented = returnTrue; + + if ( e && !this.isSimulated ) { + e.preventDefault(); + } + }, + stopPropagation: function() { + var e = this.originalEvent; + + this.isPropagationStopped = returnTrue; + + if ( e && !this.isSimulated ) { + e.stopPropagation(); + } + }, + stopImmediatePropagation: function() { + var e = this.originalEvent; + + this.isImmediatePropagationStopped = returnTrue; + + if ( e && !this.isSimulated ) { + e.stopImmediatePropagation(); + } + + this.stopPropagation(); + } +}; + +// Includes all common event props including KeyEvent and MouseEvent specific props +jQuery.each( { + altKey: true, + bubbles: true, + cancelable: true, + changedTouches: true, + ctrlKey: true, + detail: true, + eventPhase: true, + metaKey: true, + pageX: true, + pageY: true, + shiftKey: true, + view: true, + "char": true, + code: true, + charCode: true, + key: true, + keyCode: true, + button: true, + buttons: true, + clientX: true, + clientY: true, + offsetX: true, + offsetY: true, + pointerId: true, + pointerType: true, + screenX: true, + screenY: true, + targetTouches: true, + toElement: true, + touches: true, + which: true +}, jQuery.event.addProp ); + +jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateType ) { + jQuery.event.special[ type ] = { + + // Utilize native event if possible so blur/focus sequence is correct + setup: function() { + + // Claim the first handler + // dataPriv.set( this, "focus", ... ) + // dataPriv.set( this, "blur", ... ) + leverageNative( this, type, expectSync ); + + // Return false to allow normal processing in the caller + return false; + }, + trigger: function() { + + // Force setup before trigger + leverageNative( this, type ); + + // Return non-false to allow normal event-path propagation + return true; + }, + + // Suppress native focus or blur as it's already being fired + // in leverageNative. + _default: function() { + return true; + }, + + delegateType: delegateType + }; +} ); + +// Create mouseenter/leave events using mouseover/out and event-time checks +// so that event delegation works in jQuery. +// Do the same for pointerenter/pointerleave and pointerover/pointerout +// +// Support: Safari 7 only +// Safari sends mouseenter too often; see: +// https://bugs.chromium.org/p/chromium/issues/detail?id=470258 +// for the description of the bug (it existed in older Chrome versions as well). +jQuery.each( { + mouseenter: "mouseover", + mouseleave: "mouseout", + pointerenter: "pointerover", + pointerleave: "pointerout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + delegateType: fix, + bindType: fix, + + handle: function( event ) { + var ret, + target = this, + related = event.relatedTarget, + handleObj = event.handleObj; + + // For mouseenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || ( related !== target && !jQuery.contains( target, related ) ) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; + } + return ret; + } + }; +} ); + +jQuery.fn.extend( { + + on: function( types, selector, data, fn ) { + return on( this, types, selector, data, fn ); + }, + one: function( types, selector, data, fn ) { + return on( this, types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + var handleObj, type; + if ( types && types.preventDefault && types.handleObj ) { + + // ( event ) dispatched jQuery.Event + handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace ? + handleObj.origType + "." + handleObj.namespace : + handleObj.origType, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + + // ( types-object [, selector] ) + for ( type in types ) { + this.off( type, selector, types[ type ] ); + } + return this; + } + if ( selector === false || typeof selector === "function" ) { + + // ( types [, fn] ) + fn = selector; + selector = undefined; + } + if ( fn === false ) { + fn = returnFalse; + } + return this.each( function() { + jQuery.event.remove( this, types, fn, selector ); + } ); + } +} ); + + +var + + // Support: IE <=10 - 11, Edge 12 - 13 only + // In IE/Edge using regex groups here causes severe slowdowns. + // See https://connect.microsoft.com/IE/feedback/details/1736512/ + rnoInnerhtml = /\s*$/g; + +// Prefer a tbody over its parent table for containing new rows +function manipulationTarget( elem, content ) { + if ( nodeName( elem, "table" ) && + nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ) { + + return jQuery( elem ).children( "tbody" )[ 0 ] || elem; + } + + return elem; +} + +// Replace/restore the type attribute of script elements for safe DOM manipulation +function disableScript( elem ) { + elem.type = ( elem.getAttribute( "type" ) !== null ) + "/" + elem.type; + return elem; +} +function restoreScript( elem ) { + if ( ( elem.type || "" ).slice( 0, 5 ) === "true/" ) { + elem.type = elem.type.slice( 5 ); + } else { + elem.removeAttribute( "type" ); + } + + return elem; +} + +function cloneCopyEvent( src, dest ) { + var i, l, type, pdataOld, udataOld, udataCur, events; + + if ( dest.nodeType !== 1 ) { + return; + } + + // 1. Copy private data: events, handlers, etc. + if ( dataPriv.hasData( src ) ) { + pdataOld = dataPriv.get( src ); + events = pdataOld.events; + + if ( events ) { + dataPriv.remove( dest, "handle events" ); + + for ( type in events ) { + for ( i = 0, l = events[ type ].length; i < l; i++ ) { + jQuery.event.add( dest, type, events[ type ][ i ] ); + } + } + } + } + + // 2. Copy user data + if ( dataUser.hasData( src ) ) { + udataOld = dataUser.access( src ); + udataCur = jQuery.extend( {}, udataOld ); + + dataUser.set( dest, udataCur ); + } +} + +// Fix IE bugs, see support tests +function fixInput( src, dest ) { + var nodeName = dest.nodeName.toLowerCase(); + + // Fails to persist the checked state of a cloned checkbox or radio button. + if ( nodeName === "input" && rcheckableType.test( src.type ) ) { + dest.checked = src.checked; + + // Fails to return the selected option to the default selected state when cloning options + } else if ( nodeName === "input" || nodeName === "textarea" ) { + dest.defaultValue = src.defaultValue; + } +} + +function domManip( collection, args, callback, ignored ) { + + // Flatten any nested arrays + args = flat( args ); + + var fragment, first, scripts, hasScripts, node, doc, + i = 0, + l = collection.length, + iNoClone = l - 1, + value = args[ 0 ], + valueIsFunction = isFunction( value ); + + // We can't cloneNode fragments that contain checked, in WebKit + if ( valueIsFunction || + ( l > 1 && typeof value === "string" && + !support.checkClone && rchecked.test( value ) ) ) { + return collection.each( function( index ) { + var self = collection.eq( index ); + if ( valueIsFunction ) { + args[ 0 ] = value.call( this, index, self.html() ); + } + domManip( self, args, callback, ignored ); + } ); + } + + if ( l ) { + fragment = buildFragment( args, collection[ 0 ].ownerDocument, false, collection, ignored ); + first = fragment.firstChild; + + if ( fragment.childNodes.length === 1 ) { + fragment = first; + } + + // Require either new content or an interest in ignored elements to invoke the callback + if ( first || ignored ) { + scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); + hasScripts = scripts.length; + + // Use the original fragment for the last item + // instead of the first because it can end up + // being emptied incorrectly in certain situations (#8070). + for ( ; i < l; i++ ) { + node = fragment; + + if ( i !== iNoClone ) { + node = jQuery.clone( node, true, true ); + + // Keep references to cloned scripts for later restoration + if ( hasScripts ) { + + // Support: Android <=4.0 only, PhantomJS 1 only + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( scripts, getAll( node, "script" ) ); + } + } + + callback.call( collection[ i ], node, i ); + } + + if ( hasScripts ) { + doc = scripts[ scripts.length - 1 ].ownerDocument; + + // Reenable scripts + jQuery.map( scripts, restoreScript ); + + // Evaluate executable scripts on first document insertion + for ( i = 0; i < hasScripts; i++ ) { + node = scripts[ i ]; + if ( rscriptType.test( node.type || "" ) && + !dataPriv.access( node, "globalEval" ) && + jQuery.contains( doc, node ) ) { + + if ( node.src && ( node.type || "" ).toLowerCase() !== "module" ) { + + // Optional AJAX dependency, but won't run scripts if not present + if ( jQuery._evalUrl && !node.noModule ) { + jQuery._evalUrl( node.src, { + nonce: node.nonce || node.getAttribute( "nonce" ) + }, doc ); + } + } else { + DOMEval( node.textContent.replace( rcleanScript, "" ), node, doc ); + } + } + } + } + } + } + + return collection; +} + +function remove( elem, selector, keepData ) { + var node, + nodes = selector ? jQuery.filter( selector, elem ) : elem, + i = 0; + + for ( ; ( node = nodes[ i ] ) != null; i++ ) { + if ( !keepData && node.nodeType === 1 ) { + jQuery.cleanData( getAll( node ) ); + } + + if ( node.parentNode ) { + if ( keepData && isAttached( node ) ) { + setGlobalEval( getAll( node, "script" ) ); + } + node.parentNode.removeChild( node ); + } + } + + return elem; +} + +jQuery.extend( { + htmlPrefilter: function( html ) { + return html; + }, + + clone: function( elem, dataAndEvents, deepDataAndEvents ) { + var i, l, srcElements, destElements, + clone = elem.cloneNode( true ), + inPage = isAttached( elem ); + + // Fix IE cloning issues + if ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) && + !jQuery.isXMLDoc( elem ) ) { + + // We eschew Sizzle here for performance reasons: https://jsperf.com/getall-vs-sizzle/2 + destElements = getAll( clone ); + srcElements = getAll( elem ); + + for ( i = 0, l = srcElements.length; i < l; i++ ) { + fixInput( srcElements[ i ], destElements[ i ] ); + } + } + + // Copy the events from the original to the clone + if ( dataAndEvents ) { + if ( deepDataAndEvents ) { + srcElements = srcElements || getAll( elem ); + destElements = destElements || getAll( clone ); + + for ( i = 0, l = srcElements.length; i < l; i++ ) { + cloneCopyEvent( srcElements[ i ], destElements[ i ] ); + } + } else { + cloneCopyEvent( elem, clone ); + } + } + + // Preserve script evaluation history + destElements = getAll( clone, "script" ); + if ( destElements.length > 0 ) { + setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); + } + + // Return the cloned set + return clone; + }, + + cleanData: function( elems ) { + var data, elem, type, + special = jQuery.event.special, + i = 0; + + for ( ; ( elem = elems[ i ] ) !== undefined; i++ ) { + if ( acceptData( elem ) ) { + if ( ( data = elem[ dataPriv.expando ] ) ) { + if ( data.events ) { + for ( type in data.events ) { + if ( special[ type ] ) { + jQuery.event.remove( elem, type ); + + // This is a shortcut to avoid jQuery.event.remove's overhead + } else { + jQuery.removeEvent( elem, type, data.handle ); + } + } + } + + // Support: Chrome <=35 - 45+ + // Assign undefined instead of using delete, see Data#remove + elem[ dataPriv.expando ] = undefined; + } + if ( elem[ dataUser.expando ] ) { + + // Support: Chrome <=35 - 45+ + // Assign undefined instead of using delete, see Data#remove + elem[ dataUser.expando ] = undefined; + } + } + } + } +} ); + +jQuery.fn.extend( { + detach: function( selector ) { + return remove( this, selector, true ); + }, + + remove: function( selector ) { + return remove( this, selector ); + }, + + text: function( value ) { + return access( this, function( value ) { + return value === undefined ? + jQuery.text( this ) : + this.empty().each( function() { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + this.textContent = value; + } + } ); + }, null, value, arguments.length ); + }, + + append: function() { + return domManip( this, arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.appendChild( elem ); + } + } ); + }, + + prepend: function() { + return domManip( this, arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.insertBefore( elem, target.firstChild ); + } + } ); + }, + + before: function() { + return domManip( this, arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this ); + } + } ); + }, + + after: function() { + return domManip( this, arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this.nextSibling ); + } + } ); + }, + + empty: function() { + var elem, + i = 0; + + for ( ; ( elem = this[ i ] ) != null; i++ ) { + if ( elem.nodeType === 1 ) { + + // Prevent memory leaks + jQuery.cleanData( getAll( elem, false ) ); + + // Remove any remaining nodes + elem.textContent = ""; + } + } + + return this; + }, + + clone: function( dataAndEvents, deepDataAndEvents ) { + dataAndEvents = dataAndEvents == null ? false : dataAndEvents; + deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; + + return this.map( function() { + return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); + } ); + }, + + html: function( value ) { + return access( this, function( value ) { + var elem = this[ 0 ] || {}, + i = 0, + l = this.length; + + if ( value === undefined && elem.nodeType === 1 ) { + return elem.innerHTML; + } + + // See if we can take a shortcut and just use innerHTML + if ( typeof value === "string" && !rnoInnerhtml.test( value ) && + !wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) { + + value = jQuery.htmlPrefilter( value ); + + try { + for ( ; i < l; i++ ) { + elem = this[ i ] || {}; + + // Remove element nodes and prevent memory leaks + if ( elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem, false ) ); + elem.innerHTML = value; + } + } + + elem = 0; + + // If using innerHTML throws an exception, use the fallback method + } catch ( e ) {} + } + + if ( elem ) { + this.empty().append( value ); + } + }, null, value, arguments.length ); + }, + + replaceWith: function() { + var ignored = []; + + // Make the changes, replacing each non-ignored context element with the new content + return domManip( this, arguments, function( elem ) { + var parent = this.parentNode; + + if ( jQuery.inArray( this, ignored ) < 0 ) { + jQuery.cleanData( getAll( this ) ); + if ( parent ) { + parent.replaceChild( elem, this ); + } + } + + // Force callback invocation + }, ignored ); + } +} ); + +jQuery.each( { + appendTo: "append", + prependTo: "prepend", + insertBefore: "before", + insertAfter: "after", + replaceAll: "replaceWith" +}, function( name, original ) { + jQuery.fn[ name ] = function( selector ) { + var elems, + ret = [], + insert = jQuery( selector ), + last = insert.length - 1, + i = 0; + + for ( ; i <= last; i++ ) { + elems = i === last ? this : this.clone( true ); + jQuery( insert[ i ] )[ original ]( elems ); + + // Support: Android <=4.0 only, PhantomJS 1 only + // .get() because push.apply(_, arraylike) throws on ancient WebKit + push.apply( ret, elems.get() ); + } + + return this.pushStack( ret ); + }; +} ); +var rnumnonpx = new RegExp( "^(" + pnum + ")(?!px)[a-z%]+$", "i" ); + +var getStyles = function( elem ) { + + // Support: IE <=11 only, Firefox <=30 (#15098, #14150) + // IE throws on elements created in popups + // FF meanwhile throws on frame elements through "defaultView.getComputedStyle" + var view = elem.ownerDocument.defaultView; + + if ( !view || !view.opener ) { + view = window; + } + + return view.getComputedStyle( elem ); + }; + +var swap = function( elem, options, callback ) { + var ret, name, + old = {}; + + // Remember the old values, and insert the new ones + for ( name in options ) { + old[ name ] = elem.style[ name ]; + elem.style[ name ] = options[ name ]; + } + + ret = callback.call( elem ); + + // Revert the old values + for ( name in options ) { + elem.style[ name ] = old[ name ]; + } + + return ret; +}; + + +var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" ); + + + +( function() { + + // Executing both pixelPosition & boxSizingReliable tests require only one layout + // so they're executed at the same time to save the second computation. + function computeStyleTests() { + + // This is a singleton, we need to execute it only once + if ( !div ) { + return; + } + + container.style.cssText = "position:absolute;left:-11111px;width:60px;" + + "margin-top:1px;padding:0;border:0"; + div.style.cssText = + "position:relative;display:block;box-sizing:border-box;overflow:scroll;" + + "margin:auto;border:1px;padding:1px;" + + "width:60%;top:1%"; + documentElement.appendChild( container ).appendChild( div ); + + var divStyle = window.getComputedStyle( div ); + pixelPositionVal = divStyle.top !== "1%"; + + // Support: Android 4.0 - 4.3 only, Firefox <=3 - 44 + reliableMarginLeftVal = roundPixelMeasures( divStyle.marginLeft ) === 12; + + // Support: Android 4.0 - 4.3 only, Safari <=9.1 - 10.1, iOS <=7.0 - 9.3 + // Some styles come back with percentage values, even though they shouldn't + div.style.right = "60%"; + pixelBoxStylesVal = roundPixelMeasures( divStyle.right ) === 36; + + // Support: IE 9 - 11 only + // Detect misreporting of content dimensions for box-sizing:border-box elements + boxSizingReliableVal = roundPixelMeasures( divStyle.width ) === 36; + + // Support: IE 9 only + // Detect overflow:scroll screwiness (gh-3699) + // Support: Chrome <=64 + // Don't get tricked when zoom affects offsetWidth (gh-4029) + div.style.position = "absolute"; + scrollboxSizeVal = roundPixelMeasures( div.offsetWidth / 3 ) === 12; + + documentElement.removeChild( container ); + + // Nullify the div so it wouldn't be stored in the memory and + // it will also be a sign that checks already performed + div = null; + } + + function roundPixelMeasures( measure ) { + return Math.round( parseFloat( measure ) ); + } + + var pixelPositionVal, boxSizingReliableVal, scrollboxSizeVal, pixelBoxStylesVal, + reliableTrDimensionsVal, reliableMarginLeftVal, + container = document.createElement( "div" ), + div = document.createElement( "div" ); + + // Finish early in limited (non-browser) environments + if ( !div.style ) { + return; + } + + // Support: IE <=9 - 11 only + // Style of cloned element affects source element cloned (#8908) + div.style.backgroundClip = "content-box"; + div.cloneNode( true ).style.backgroundClip = ""; + support.clearCloneStyle = div.style.backgroundClip === "content-box"; + + jQuery.extend( support, { + boxSizingReliable: function() { + computeStyleTests(); + return boxSizingReliableVal; + }, + pixelBoxStyles: function() { + computeStyleTests(); + return pixelBoxStylesVal; + }, + pixelPosition: function() { + computeStyleTests(); + return pixelPositionVal; + }, + reliableMarginLeft: function() { + computeStyleTests(); + return reliableMarginLeftVal; + }, + scrollboxSize: function() { + computeStyleTests(); + return scrollboxSizeVal; + }, + + // Support: IE 9 - 11+, Edge 15 - 18+ + // IE/Edge misreport `getComputedStyle` of table rows with width/height + // set in CSS while `offset*` properties report correct values. + // Behavior in IE 9 is more subtle than in newer versions & it passes + // some versions of this test; make sure not to make it pass there! + // + // Support: Firefox 70+ + // Only Firefox includes border widths + // in computed dimensions. (gh-4529) + reliableTrDimensions: function() { + var table, tr, trChild, trStyle; + if ( reliableTrDimensionsVal == null ) { + table = document.createElement( "table" ); + tr = document.createElement( "tr" ); + trChild = document.createElement( "div" ); + + table.style.cssText = "position:absolute;left:-11111px;border-collapse:separate"; + tr.style.cssText = "border:1px solid"; + + // Support: Chrome 86+ + // Height set through cssText does not get applied. + // Computed height then comes back as 0. + tr.style.height = "1px"; + trChild.style.height = "9px"; + + // Support: Android 8 Chrome 86+ + // In our bodyBackground.html iframe, + // display for all div elements is set to "inline", + // which causes a problem only in Android 8 Chrome 86. + // Ensuring the div is display: block + // gets around this issue. + trChild.style.display = "block"; + + documentElement + .appendChild( table ) + .appendChild( tr ) + .appendChild( trChild ); + + trStyle = window.getComputedStyle( tr ); + reliableTrDimensionsVal = ( parseInt( trStyle.height, 10 ) + + parseInt( trStyle.borderTopWidth, 10 ) + + parseInt( trStyle.borderBottomWidth, 10 ) ) === tr.offsetHeight; + + documentElement.removeChild( table ); + } + return reliableTrDimensionsVal; + } + } ); +} )(); + + +function curCSS( elem, name, computed ) { + var width, minWidth, maxWidth, ret, + + // Support: Firefox 51+ + // Retrieving style before computed somehow + // fixes an issue with getting wrong values + // on detached elements + style = elem.style; + + computed = computed || getStyles( elem ); + + // getPropertyValue is needed for: + // .css('filter') (IE 9 only, #12537) + // .css('--customProperty) (#3144) + if ( computed ) { + ret = computed.getPropertyValue( name ) || computed[ name ]; + + if ( ret === "" && !isAttached( elem ) ) { + ret = jQuery.style( elem, name ); + } + + // A tribute to the "awesome hack by Dean Edwards" + // Android Browser returns percentage for some values, + // but width seems to be reliably pixels. + // This is against the CSSOM draft spec: + // https://drafts.csswg.org/cssom/#resolved-values + if ( !support.pixelBoxStyles() && rnumnonpx.test( ret ) && rboxStyle.test( name ) ) { + + // Remember the original values + width = style.width; + minWidth = style.minWidth; + maxWidth = style.maxWidth; + + // Put in the new values to get a computed value out + style.minWidth = style.maxWidth = style.width = ret; + ret = computed.width; + + // Revert the changed values + style.width = width; + style.minWidth = minWidth; + style.maxWidth = maxWidth; + } + } + + return ret !== undefined ? + + // Support: IE <=9 - 11 only + // IE returns zIndex value as an integer. + ret + "" : + ret; +} + + +function addGetHookIf( conditionFn, hookFn ) { + + // Define the hook, we'll check on the first run if it's really needed. + return { + get: function() { + if ( conditionFn() ) { + + // Hook not needed (or it's not possible to use it due + // to missing dependency), remove it. + delete this.get; + return; + } + + // Hook needed; redefine it so that the support test is not executed again. + return ( this.get = hookFn ).apply( this, arguments ); + } + }; +} + + +var cssPrefixes = [ "Webkit", "Moz", "ms" ], + emptyStyle = document.createElement( "div" ).style, + vendorProps = {}; + +// Return a vendor-prefixed property or undefined +function vendorPropName( name ) { + + // Check for vendor prefixed names + var capName = name[ 0 ].toUpperCase() + name.slice( 1 ), + i = cssPrefixes.length; + + while ( i-- ) { + name = cssPrefixes[ i ] + capName; + if ( name in emptyStyle ) { + return name; + } + } +} + +// Return a potentially-mapped jQuery.cssProps or vendor prefixed property +function finalPropName( name ) { + var final = jQuery.cssProps[ name ] || vendorProps[ name ]; + + if ( final ) { + return final; + } + if ( name in emptyStyle ) { + return name; + } + return vendorProps[ name ] = vendorPropName( name ) || name; +} + + +var + + // Swappable if display is none or starts with table + // except "table", "table-cell", or "table-caption" + // See here for display values: https://developer.mozilla.org/en-US/docs/CSS/display + rdisplayswap = /^(none|table(?!-c[ea]).+)/, + rcustomProp = /^--/, + cssShow = { position: "absolute", visibility: "hidden", display: "block" }, + cssNormalTransform = { + letterSpacing: "0", + fontWeight: "400" + }; + +function setPositiveNumber( _elem, value, subtract ) { + + // Any relative (+/-) values have already been + // normalized at this point + var matches = rcssNum.exec( value ); + return matches ? + + // Guard against undefined "subtract", e.g., when used as in cssHooks + Math.max( 0, matches[ 2 ] - ( subtract || 0 ) ) + ( matches[ 3 ] || "px" ) : + value; +} + +function boxModelAdjustment( elem, dimension, box, isBorderBox, styles, computedVal ) { + var i = dimension === "width" ? 1 : 0, + extra = 0, + delta = 0; + + // Adjustment may not be necessary + if ( box === ( isBorderBox ? "border" : "content" ) ) { + return 0; + } + + for ( ; i < 4; i += 2 ) { + + // Both box models exclude margin + if ( box === "margin" ) { + delta += jQuery.css( elem, box + cssExpand[ i ], true, styles ); + } + + // If we get here with a content-box, we're seeking "padding" or "border" or "margin" + if ( !isBorderBox ) { + + // Add padding + delta += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + + // For "border" or "margin", add border + if ( box !== "padding" ) { + delta += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + + // But still keep track of it otherwise + } else { + extra += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + } + + // If we get here with a border-box (content + padding + border), we're seeking "content" or + // "padding" or "margin" + } else { + + // For "content", subtract padding + if ( box === "content" ) { + delta -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + } + + // For "content" or "padding", subtract border + if ( box !== "margin" ) { + delta -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + } + } + } + + // Account for positive content-box scroll gutter when requested by providing computedVal + if ( !isBorderBox && computedVal >= 0 ) { + + // offsetWidth/offsetHeight is a rounded sum of content, padding, scroll gutter, and border + // Assuming integer scroll gutter, subtract the rest and round down + delta += Math.max( 0, Math.ceil( + elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] - + computedVal - + delta - + extra - + 0.5 + + // If offsetWidth/offsetHeight is unknown, then we can't determine content-box scroll gutter + // Use an explicit zero to avoid NaN (gh-3964) + ) ) || 0; + } + + return delta; +} + +function getWidthOrHeight( elem, dimension, extra ) { + + // Start with computed style + var styles = getStyles( elem ), + + // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-4322). + // Fake content-box until we know it's needed to know the true value. + boxSizingNeeded = !support.boxSizingReliable() || extra, + isBorderBox = boxSizingNeeded && + jQuery.css( elem, "boxSizing", false, styles ) === "border-box", + valueIsBorderBox = isBorderBox, + + val = curCSS( elem, dimension, styles ), + offsetProp = "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ); + + // Support: Firefox <=54 + // Return a confounding non-pixel value or feign ignorance, as appropriate. + if ( rnumnonpx.test( val ) ) { + if ( !extra ) { + return val; + } + val = "auto"; + } + + + // Support: IE 9 - 11 only + // Use offsetWidth/offsetHeight for when box sizing is unreliable. + // In those cases, the computed value can be trusted to be border-box. + if ( ( !support.boxSizingReliable() && isBorderBox || + + // Support: IE 10 - 11+, Edge 15 - 18+ + // IE/Edge misreport `getComputedStyle` of table rows with width/height + // set in CSS while `offset*` properties report correct values. + // Interestingly, in some cases IE 9 doesn't suffer from this issue. + !support.reliableTrDimensions() && nodeName( elem, "tr" ) || + + // Fall back to offsetWidth/offsetHeight when value is "auto" + // This happens for inline elements with no explicit setting (gh-3571) + val === "auto" || + + // Support: Android <=4.1 - 4.3 only + // Also use offsetWidth/offsetHeight for misreported inline dimensions (gh-3602) + !parseFloat( val ) && jQuery.css( elem, "display", false, styles ) === "inline" ) && + + // Make sure the element is visible & connected + elem.getClientRects().length ) { + + isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box"; + + // Where available, offsetWidth/offsetHeight approximate border box dimensions. + // Where not available (e.g., SVG), assume unreliable box-sizing and interpret the + // retrieved value as a content box dimension. + valueIsBorderBox = offsetProp in elem; + if ( valueIsBorderBox ) { + val = elem[ offsetProp ]; + } + } + + // Normalize "" and auto + val = parseFloat( val ) || 0; + + // Adjust for the element's box model + return ( val + + boxModelAdjustment( + elem, + dimension, + extra || ( isBorderBox ? "border" : "content" ), + valueIsBorderBox, + styles, + + // Provide the current computed size to request scroll gutter calculation (gh-3589) + val + ) + ) + "px"; +} + +jQuery.extend( { + + // Add in style property hooks for overriding the default + // behavior of getting and setting a style property + cssHooks: { + opacity: { + get: function( elem, computed ) { + if ( computed ) { + + // We should always get a number back from opacity + var ret = curCSS( elem, "opacity" ); + return ret === "" ? "1" : ret; + } + } + } + }, + + // Don't automatically add "px" to these possibly-unitless properties + cssNumber: { + "animationIterationCount": true, + "columnCount": true, + "fillOpacity": true, + "flexGrow": true, + "flexShrink": true, + "fontWeight": true, + "gridArea": true, + "gridColumn": true, + "gridColumnEnd": true, + "gridColumnStart": true, + "gridRow": true, + "gridRowEnd": true, + "gridRowStart": true, + "lineHeight": true, + "opacity": true, + "order": true, + "orphans": true, + "widows": true, + "zIndex": true, + "zoom": true + }, + + // Add in properties whose names you wish to fix before + // setting or getting the value + cssProps: {}, + + // Get and set the style property on a DOM Node + style: function( elem, name, value, extra ) { + + // Don't set styles on text and comment nodes + if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) { + return; + } + + // Make sure that we're working with the right name + var ret, type, hooks, + origName = camelCase( name ), + isCustomProp = rcustomProp.test( name ), + style = elem.style; + + // Make sure that we're working with the right name. We don't + // want to query the value if it is a CSS custom property + // since they are user-defined. + if ( !isCustomProp ) { + name = finalPropName( origName ); + } + + // Gets hook for the prefixed version, then unprefixed version + hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; + + // Check if we're setting a value + if ( value !== undefined ) { + type = typeof value; + + // Convert "+=" or "-=" to relative numbers (#7345) + if ( type === "string" && ( ret = rcssNum.exec( value ) ) && ret[ 1 ] ) { + value = adjustCSS( elem, name, ret ); + + // Fixes bug #9237 + type = "number"; + } + + // Make sure that null and NaN values aren't set (#7116) + if ( value == null || value !== value ) { + return; + } + + // If a number was passed in, add the unit (except for certain CSS properties) + // The isCustomProp check can be removed in jQuery 4.0 when we only auto-append + // "px" to a few hardcoded values. + if ( type === "number" && !isCustomProp ) { + value += ret && ret[ 3 ] || ( jQuery.cssNumber[ origName ] ? "" : "px" ); + } + + // background-* props affect original clone's values + if ( !support.clearCloneStyle && value === "" && name.indexOf( "background" ) === 0 ) { + style[ name ] = "inherit"; + } + + // If a hook was provided, use that value, otherwise just set the specified value + if ( !hooks || !( "set" in hooks ) || + ( value = hooks.set( elem, value, extra ) ) !== undefined ) { + + if ( isCustomProp ) { + style.setProperty( name, value ); + } else { + style[ name ] = value; + } + } + + } else { + + // If a hook was provided get the non-computed value from there + if ( hooks && "get" in hooks && + ( ret = hooks.get( elem, false, extra ) ) !== undefined ) { + + return ret; + } + + // Otherwise just get the value from the style object + return style[ name ]; + } + }, + + css: function( elem, name, extra, styles ) { + var val, num, hooks, + origName = camelCase( name ), + isCustomProp = rcustomProp.test( name ); + + // Make sure that we're working with the right name. We don't + // want to modify the value if it is a CSS custom property + // since they are user-defined. + if ( !isCustomProp ) { + name = finalPropName( origName ); + } + + // Try prefixed name followed by the unprefixed name + hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; + + // If a hook was provided get the computed value from there + if ( hooks && "get" in hooks ) { + val = hooks.get( elem, true, extra ); + } + + // Otherwise, if a way to get the computed value exists, use that + if ( val === undefined ) { + val = curCSS( elem, name, styles ); + } + + // Convert "normal" to computed value + if ( val === "normal" && name in cssNormalTransform ) { + val = cssNormalTransform[ name ]; + } + + // Make numeric if forced or a qualifier was provided and val looks numeric + if ( extra === "" || extra ) { + num = parseFloat( val ); + return extra === true || isFinite( num ) ? num || 0 : val; + } + + return val; + } +} ); + +jQuery.each( [ "height", "width" ], function( _i, dimension ) { + jQuery.cssHooks[ dimension ] = { + get: function( elem, computed, extra ) { + if ( computed ) { + + // Certain elements can have dimension info if we invisibly show them + // but it must have a current display style that would benefit + return rdisplayswap.test( jQuery.css( elem, "display" ) ) && + + // Support: Safari 8+ + // Table columns in Safari have non-zero offsetWidth & zero + // getBoundingClientRect().width unless display is changed. + // Support: IE <=11 only + // Running getBoundingClientRect on a disconnected node + // in IE throws an error. + ( !elem.getClientRects().length || !elem.getBoundingClientRect().width ) ? + swap( elem, cssShow, function() { + return getWidthOrHeight( elem, dimension, extra ); + } ) : + getWidthOrHeight( elem, dimension, extra ); + } + }, + + set: function( elem, value, extra ) { + var matches, + styles = getStyles( elem ), + + // Only read styles.position if the test has a chance to fail + // to avoid forcing a reflow. + scrollboxSizeBuggy = !support.scrollboxSize() && + styles.position === "absolute", + + // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-3991) + boxSizingNeeded = scrollboxSizeBuggy || extra, + isBorderBox = boxSizingNeeded && + jQuery.css( elem, "boxSizing", false, styles ) === "border-box", + subtract = extra ? + boxModelAdjustment( + elem, + dimension, + extra, + isBorderBox, + styles + ) : + 0; + + // Account for unreliable border-box dimensions by comparing offset* to computed and + // faking a content-box to get border and padding (gh-3699) + if ( isBorderBox && scrollboxSizeBuggy ) { + subtract -= Math.ceil( + elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] - + parseFloat( styles[ dimension ] ) - + boxModelAdjustment( elem, dimension, "border", false, styles ) - + 0.5 + ); + } + + // Convert to pixels if value adjustment is needed + if ( subtract && ( matches = rcssNum.exec( value ) ) && + ( matches[ 3 ] || "px" ) !== "px" ) { + + elem.style[ dimension ] = value; + value = jQuery.css( elem, dimension ); + } + + return setPositiveNumber( elem, value, subtract ); + } + }; +} ); + +jQuery.cssHooks.marginLeft = addGetHookIf( support.reliableMarginLeft, + function( elem, computed ) { + if ( computed ) { + return ( parseFloat( curCSS( elem, "marginLeft" ) ) || + elem.getBoundingClientRect().left - + swap( elem, { marginLeft: 0 }, function() { + return elem.getBoundingClientRect().left; + } ) + ) + "px"; + } + } +); + +// These hooks are used by animate to expand properties +jQuery.each( { + margin: "", + padding: "", + border: "Width" +}, function( prefix, suffix ) { + jQuery.cssHooks[ prefix + suffix ] = { + expand: function( value ) { + var i = 0, + expanded = {}, + + // Assumes a single number if not a string + parts = typeof value === "string" ? value.split( " " ) : [ value ]; + + for ( ; i < 4; i++ ) { + expanded[ prefix + cssExpand[ i ] + suffix ] = + parts[ i ] || parts[ i - 2 ] || parts[ 0 ]; + } + + return expanded; + } + }; + + if ( prefix !== "margin" ) { + jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber; + } +} ); + +jQuery.fn.extend( { + css: function( name, value ) { + return access( this, function( elem, name, value ) { + var styles, len, + map = {}, + i = 0; + + if ( Array.isArray( name ) ) { + styles = getStyles( elem ); + len = name.length; + + for ( ; i < len; i++ ) { + map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles ); + } + + return map; + } + + return value !== undefined ? + jQuery.style( elem, name, value ) : + jQuery.css( elem, name ); + }, name, value, arguments.length > 1 ); + } +} ); + + +function Tween( elem, options, prop, end, easing ) { + return new Tween.prototype.init( elem, options, prop, end, easing ); +} +jQuery.Tween = Tween; + +Tween.prototype = { + constructor: Tween, + init: function( elem, options, prop, end, easing, unit ) { + this.elem = elem; + this.prop = prop; + this.easing = easing || jQuery.easing._default; + this.options = options; + this.start = this.now = this.cur(); + this.end = end; + this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" ); + }, + cur: function() { + var hooks = Tween.propHooks[ this.prop ]; + + return hooks && hooks.get ? + hooks.get( this ) : + Tween.propHooks._default.get( this ); + }, + run: function( percent ) { + var eased, + hooks = Tween.propHooks[ this.prop ]; + + if ( this.options.duration ) { + this.pos = eased = jQuery.easing[ this.easing ]( + percent, this.options.duration * percent, 0, 1, this.options.duration + ); + } else { + this.pos = eased = percent; + } + this.now = ( this.end - this.start ) * eased + this.start; + + if ( this.options.step ) { + this.options.step.call( this.elem, this.now, this ); + } + + if ( hooks && hooks.set ) { + hooks.set( this ); + } else { + Tween.propHooks._default.set( this ); + } + return this; + } +}; + +Tween.prototype.init.prototype = Tween.prototype; + +Tween.propHooks = { + _default: { + get: function( tween ) { + var result; + + // Use a property on the element directly when it is not a DOM element, + // or when there is no matching style property that exists. + if ( tween.elem.nodeType !== 1 || + tween.elem[ tween.prop ] != null && tween.elem.style[ tween.prop ] == null ) { + return tween.elem[ tween.prop ]; + } + + // Passing an empty string as a 3rd parameter to .css will automatically + // attempt a parseFloat and fallback to a string if the parse fails. + // Simple values such as "10px" are parsed to Float; + // complex values such as "rotate(1rad)" are returned as-is. + result = jQuery.css( tween.elem, tween.prop, "" ); + + // Empty strings, null, undefined and "auto" are converted to 0. + return !result || result === "auto" ? 0 : result; + }, + set: function( tween ) { + + // Use step hook for back compat. + // Use cssHook if its there. + // Use .style if available and use plain properties where available. + if ( jQuery.fx.step[ tween.prop ] ) { + jQuery.fx.step[ tween.prop ]( tween ); + } else if ( tween.elem.nodeType === 1 && ( + jQuery.cssHooks[ tween.prop ] || + tween.elem.style[ finalPropName( tween.prop ) ] != null ) ) { + jQuery.style( tween.elem, tween.prop, tween.now + tween.unit ); + } else { + tween.elem[ tween.prop ] = tween.now; + } + } + } +}; + +// Support: IE <=9 only +// Panic based approach to setting things on disconnected nodes +Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = { + set: function( tween ) { + if ( tween.elem.nodeType && tween.elem.parentNode ) { + tween.elem[ tween.prop ] = tween.now; + } + } +}; + +jQuery.easing = { + linear: function( p ) { + return p; + }, + swing: function( p ) { + return 0.5 - Math.cos( p * Math.PI ) / 2; + }, + _default: "swing" +}; + +jQuery.fx = Tween.prototype.init; + +// Back compat <1.8 extension point +jQuery.fx.step = {}; + + + + +var + fxNow, inProgress, + rfxtypes = /^(?:toggle|show|hide)$/, + rrun = /queueHooks$/; + +function schedule() { + if ( inProgress ) { + if ( document.hidden === false && window.requestAnimationFrame ) { + window.requestAnimationFrame( schedule ); + } else { + window.setTimeout( schedule, jQuery.fx.interval ); + } + + jQuery.fx.tick(); + } +} + +// Animations created synchronously will run synchronously +function createFxNow() { + window.setTimeout( function() { + fxNow = undefined; + } ); + return ( fxNow = Date.now() ); +} + +// Generate parameters to create a standard animation +function genFx( type, includeWidth ) { + var which, + i = 0, + attrs = { height: type }; + + // If we include width, step value is 1 to do all cssExpand values, + // otherwise step value is 2 to skip over Left and Right + includeWidth = includeWidth ? 1 : 0; + for ( ; i < 4; i += 2 - includeWidth ) { + which = cssExpand[ i ]; + attrs[ "margin" + which ] = attrs[ "padding" + which ] = type; + } + + if ( includeWidth ) { + attrs.opacity = attrs.width = type; + } + + return attrs; +} + +function createTween( value, prop, animation ) { + var tween, + collection = ( Animation.tweeners[ prop ] || [] ).concat( Animation.tweeners[ "*" ] ), + index = 0, + length = collection.length; + for ( ; index < length; index++ ) { + if ( ( tween = collection[ index ].call( animation, prop, value ) ) ) { + + // We're done with this property + return tween; + } + } +} + +function defaultPrefilter( elem, props, opts ) { + var prop, value, toggle, hooks, oldfire, propTween, restoreDisplay, display, + isBox = "width" in props || "height" in props, + anim = this, + orig = {}, + style = elem.style, + hidden = elem.nodeType && isHiddenWithinTree( elem ), + dataShow = dataPriv.get( elem, "fxshow" ); + + // Queue-skipping animations hijack the fx hooks + if ( !opts.queue ) { + hooks = jQuery._queueHooks( elem, "fx" ); + if ( hooks.unqueued == null ) { + hooks.unqueued = 0; + oldfire = hooks.empty.fire; + hooks.empty.fire = function() { + if ( !hooks.unqueued ) { + oldfire(); + } + }; + } + hooks.unqueued++; + + anim.always( function() { + + // Ensure the complete handler is called before this completes + anim.always( function() { + hooks.unqueued--; + if ( !jQuery.queue( elem, "fx" ).length ) { + hooks.empty.fire(); + } + } ); + } ); + } + + // Detect show/hide animations + for ( prop in props ) { + value = props[ prop ]; + if ( rfxtypes.test( value ) ) { + delete props[ prop ]; + toggle = toggle || value === "toggle"; + if ( value === ( hidden ? "hide" : "show" ) ) { + + // Pretend to be hidden if this is a "show" and + // there is still data from a stopped show/hide + if ( value === "show" && dataShow && dataShow[ prop ] !== undefined ) { + hidden = true; + + // Ignore all other no-op show/hide data + } else { + continue; + } + } + orig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop ); + } + } + + // Bail out if this is a no-op like .hide().hide() + propTween = !jQuery.isEmptyObject( props ); + if ( !propTween && jQuery.isEmptyObject( orig ) ) { + return; + } + + // Restrict "overflow" and "display" styles during box animations + if ( isBox && elem.nodeType === 1 ) { + + // Support: IE <=9 - 11, Edge 12 - 15 + // Record all 3 overflow attributes because IE does not infer the shorthand + // from identically-valued overflowX and overflowY and Edge just mirrors + // the overflowX value there. + opts.overflow = [ style.overflow, style.overflowX, style.overflowY ]; + + // Identify a display type, preferring old show/hide data over the CSS cascade + restoreDisplay = dataShow && dataShow.display; + if ( restoreDisplay == null ) { + restoreDisplay = dataPriv.get( elem, "display" ); + } + display = jQuery.css( elem, "display" ); + if ( display === "none" ) { + if ( restoreDisplay ) { + display = restoreDisplay; + } else { + + // Get nonempty value(s) by temporarily forcing visibility + showHide( [ elem ], true ); + restoreDisplay = elem.style.display || restoreDisplay; + display = jQuery.css( elem, "display" ); + showHide( [ elem ] ); + } + } + + // Animate inline elements as inline-block + if ( display === "inline" || display === "inline-block" && restoreDisplay != null ) { + if ( jQuery.css( elem, "float" ) === "none" ) { + + // Restore the original display value at the end of pure show/hide animations + if ( !propTween ) { + anim.done( function() { + style.display = restoreDisplay; + } ); + if ( restoreDisplay == null ) { + display = style.display; + restoreDisplay = display === "none" ? "" : display; + } + } + style.display = "inline-block"; + } + } + } + + if ( opts.overflow ) { + style.overflow = "hidden"; + anim.always( function() { + style.overflow = opts.overflow[ 0 ]; + style.overflowX = opts.overflow[ 1 ]; + style.overflowY = opts.overflow[ 2 ]; + } ); + } + + // Implement show/hide animations + propTween = false; + for ( prop in orig ) { + + // General show/hide setup for this element animation + if ( !propTween ) { + if ( dataShow ) { + if ( "hidden" in dataShow ) { + hidden = dataShow.hidden; + } + } else { + dataShow = dataPriv.access( elem, "fxshow", { display: restoreDisplay } ); + } + + // Store hidden/visible for toggle so `.stop().toggle()` "reverses" + if ( toggle ) { + dataShow.hidden = !hidden; + } + + // Show elements before animating them + if ( hidden ) { + showHide( [ elem ], true ); + } + + /* eslint-disable no-loop-func */ + + anim.done( function() { + + /* eslint-enable no-loop-func */ + + // The final step of a "hide" animation is actually hiding the element + if ( !hidden ) { + showHide( [ elem ] ); + } + dataPriv.remove( elem, "fxshow" ); + for ( prop in orig ) { + jQuery.style( elem, prop, orig[ prop ] ); + } + } ); + } + + // Per-property setup + propTween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim ); + if ( !( prop in dataShow ) ) { + dataShow[ prop ] = propTween.start; + if ( hidden ) { + propTween.end = propTween.start; + propTween.start = 0; + } + } + } +} + +function propFilter( props, specialEasing ) { + var index, name, easing, value, hooks; + + // camelCase, specialEasing and expand cssHook pass + for ( index in props ) { + name = camelCase( index ); + easing = specialEasing[ name ]; + value = props[ index ]; + if ( Array.isArray( value ) ) { + easing = value[ 1 ]; + value = props[ index ] = value[ 0 ]; + } + + if ( index !== name ) { + props[ name ] = value; + delete props[ index ]; + } + + hooks = jQuery.cssHooks[ name ]; + if ( hooks && "expand" in hooks ) { + value = hooks.expand( value ); + delete props[ name ]; + + // Not quite $.extend, this won't overwrite existing keys. + // Reusing 'index' because we have the correct "name" + for ( index in value ) { + if ( !( index in props ) ) { + props[ index ] = value[ index ]; + specialEasing[ index ] = easing; + } + } + } else { + specialEasing[ name ] = easing; + } + } +} + +function Animation( elem, properties, options ) { + var result, + stopped, + index = 0, + length = Animation.prefilters.length, + deferred = jQuery.Deferred().always( function() { + + // Don't match elem in the :animated selector + delete tick.elem; + } ), + tick = function() { + if ( stopped ) { + return false; + } + var currentTime = fxNow || createFxNow(), + remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ), + + // Support: Android 2.3 only + // Archaic crash bug won't allow us to use `1 - ( 0.5 || 0 )` (#12497) + temp = remaining / animation.duration || 0, + percent = 1 - temp, + index = 0, + length = animation.tweens.length; + + for ( ; index < length; index++ ) { + animation.tweens[ index ].run( percent ); + } + + deferred.notifyWith( elem, [ animation, percent, remaining ] ); + + // If there's more to do, yield + if ( percent < 1 && length ) { + return remaining; + } + + // If this was an empty animation, synthesize a final progress notification + if ( !length ) { + deferred.notifyWith( elem, [ animation, 1, 0 ] ); + } + + // Resolve the animation and report its conclusion + deferred.resolveWith( elem, [ animation ] ); + return false; + }, + animation = deferred.promise( { + elem: elem, + props: jQuery.extend( {}, properties ), + opts: jQuery.extend( true, { + specialEasing: {}, + easing: jQuery.easing._default + }, options ), + originalProperties: properties, + originalOptions: options, + startTime: fxNow || createFxNow(), + duration: options.duration, + tweens: [], + createTween: function( prop, end ) { + var tween = jQuery.Tween( elem, animation.opts, prop, end, + animation.opts.specialEasing[ prop ] || animation.opts.easing ); + animation.tweens.push( tween ); + return tween; + }, + stop: function( gotoEnd ) { + var index = 0, + + // If we are going to the end, we want to run all the tweens + // otherwise we skip this part + length = gotoEnd ? animation.tweens.length : 0; + if ( stopped ) { + return this; + } + stopped = true; + for ( ; index < length; index++ ) { + animation.tweens[ index ].run( 1 ); + } + + // Resolve when we played the last frame; otherwise, reject + if ( gotoEnd ) { + deferred.notifyWith( elem, [ animation, 1, 0 ] ); + deferred.resolveWith( elem, [ animation, gotoEnd ] ); + } else { + deferred.rejectWith( elem, [ animation, gotoEnd ] ); + } + return this; + } + } ), + props = animation.props; + + propFilter( props, animation.opts.specialEasing ); + + for ( ; index < length; index++ ) { + result = Animation.prefilters[ index ].call( animation, elem, props, animation.opts ); + if ( result ) { + if ( isFunction( result.stop ) ) { + jQuery._queueHooks( animation.elem, animation.opts.queue ).stop = + result.stop.bind( result ); + } + return result; + } + } + + jQuery.map( props, createTween, animation ); + + if ( isFunction( animation.opts.start ) ) { + animation.opts.start.call( elem, animation ); + } + + // Attach callbacks from options + animation + .progress( animation.opts.progress ) + .done( animation.opts.done, animation.opts.complete ) + .fail( animation.opts.fail ) + .always( animation.opts.always ); + + jQuery.fx.timer( + jQuery.extend( tick, { + elem: elem, + anim: animation, + queue: animation.opts.queue + } ) + ); + + return animation; +} + +jQuery.Animation = jQuery.extend( Animation, { + + tweeners: { + "*": [ function( prop, value ) { + var tween = this.createTween( prop, value ); + adjustCSS( tween.elem, prop, rcssNum.exec( value ), tween ); + return tween; + } ] + }, + + tweener: function( props, callback ) { + if ( isFunction( props ) ) { + callback = props; + props = [ "*" ]; + } else { + props = props.match( rnothtmlwhite ); + } + + var prop, + index = 0, + length = props.length; + + for ( ; index < length; index++ ) { + prop = props[ index ]; + Animation.tweeners[ prop ] = Animation.tweeners[ prop ] || []; + Animation.tweeners[ prop ].unshift( callback ); + } + }, + + prefilters: [ defaultPrefilter ], + + prefilter: function( callback, prepend ) { + if ( prepend ) { + Animation.prefilters.unshift( callback ); + } else { + Animation.prefilters.push( callback ); + } + } +} ); + +jQuery.speed = function( speed, easing, fn ) { + var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : { + complete: fn || !fn && easing || + isFunction( speed ) && speed, + duration: speed, + easing: fn && easing || easing && !isFunction( easing ) && easing + }; + + // Go to the end state if fx are off + if ( jQuery.fx.off ) { + opt.duration = 0; + + } else { + if ( typeof opt.duration !== "number" ) { + if ( opt.duration in jQuery.fx.speeds ) { + opt.duration = jQuery.fx.speeds[ opt.duration ]; + + } else { + opt.duration = jQuery.fx.speeds._default; + } + } + } + + // Normalize opt.queue - true/undefined/null -> "fx" + if ( opt.queue == null || opt.queue === true ) { + opt.queue = "fx"; + } + + // Queueing + opt.old = opt.complete; + + opt.complete = function() { + if ( isFunction( opt.old ) ) { + opt.old.call( this ); + } + + if ( opt.queue ) { + jQuery.dequeue( this, opt.queue ); + } + }; + + return opt; +}; + +jQuery.fn.extend( { + fadeTo: function( speed, to, easing, callback ) { + + // Show any hidden elements after setting opacity to 0 + return this.filter( isHiddenWithinTree ).css( "opacity", 0 ).show() + + // Animate to the value specified + .end().animate( { opacity: to }, speed, easing, callback ); + }, + animate: function( prop, speed, easing, callback ) { + var empty = jQuery.isEmptyObject( prop ), + optall = jQuery.speed( speed, easing, callback ), + doAnimation = function() { + + // Operate on a copy of prop so per-property easing won't be lost + var anim = Animation( this, jQuery.extend( {}, prop ), optall ); + + // Empty animations, or finishing resolves immediately + if ( empty || dataPriv.get( this, "finish" ) ) { + anim.stop( true ); + } + }; + + doAnimation.finish = doAnimation; + + return empty || optall.queue === false ? + this.each( doAnimation ) : + this.queue( optall.queue, doAnimation ); + }, + stop: function( type, clearQueue, gotoEnd ) { + var stopQueue = function( hooks ) { + var stop = hooks.stop; + delete hooks.stop; + stop( gotoEnd ); + }; + + if ( typeof type !== "string" ) { + gotoEnd = clearQueue; + clearQueue = type; + type = undefined; + } + if ( clearQueue ) { + this.queue( type || "fx", [] ); + } + + return this.each( function() { + var dequeue = true, + index = type != null && type + "queueHooks", + timers = jQuery.timers, + data = dataPriv.get( this ); + + if ( index ) { + if ( data[ index ] && data[ index ].stop ) { + stopQueue( data[ index ] ); + } + } else { + for ( index in data ) { + if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) { + stopQueue( data[ index ] ); + } + } + } + + for ( index = timers.length; index--; ) { + if ( timers[ index ].elem === this && + ( type == null || timers[ index ].queue === type ) ) { + + timers[ index ].anim.stop( gotoEnd ); + dequeue = false; + timers.splice( index, 1 ); + } + } + + // Start the next in the queue if the last step wasn't forced. + // Timers currently will call their complete callbacks, which + // will dequeue but only if they were gotoEnd. + if ( dequeue || !gotoEnd ) { + jQuery.dequeue( this, type ); + } + } ); + }, + finish: function( type ) { + if ( type !== false ) { + type = type || "fx"; + } + return this.each( function() { + var index, + data = dataPriv.get( this ), + queue = data[ type + "queue" ], + hooks = data[ type + "queueHooks" ], + timers = jQuery.timers, + length = queue ? queue.length : 0; + + // Enable finishing flag on private data + data.finish = true; + + // Empty the queue first + jQuery.queue( this, type, [] ); + + if ( hooks && hooks.stop ) { + hooks.stop.call( this, true ); + } + + // Look for any active animations, and finish them + for ( index = timers.length; index--; ) { + if ( timers[ index ].elem === this && timers[ index ].queue === type ) { + timers[ index ].anim.stop( true ); + timers.splice( index, 1 ); + } + } + + // Look for any animations in the old queue and finish them + for ( index = 0; index < length; index++ ) { + if ( queue[ index ] && queue[ index ].finish ) { + queue[ index ].finish.call( this ); + } + } + + // Turn off finishing flag + delete data.finish; + } ); + } +} ); + +jQuery.each( [ "toggle", "show", "hide" ], function( _i, name ) { + var cssFn = jQuery.fn[ name ]; + jQuery.fn[ name ] = function( speed, easing, callback ) { + return speed == null || typeof speed === "boolean" ? + cssFn.apply( this, arguments ) : + this.animate( genFx( name, true ), speed, easing, callback ); + }; +} ); + +// Generate shortcuts for custom animations +jQuery.each( { + slideDown: genFx( "show" ), + slideUp: genFx( "hide" ), + slideToggle: genFx( "toggle" ), + fadeIn: { opacity: "show" }, + fadeOut: { opacity: "hide" }, + fadeToggle: { opacity: "toggle" } +}, function( name, props ) { + jQuery.fn[ name ] = function( speed, easing, callback ) { + return this.animate( props, speed, easing, callback ); + }; +} ); + +jQuery.timers = []; +jQuery.fx.tick = function() { + var timer, + i = 0, + timers = jQuery.timers; + + fxNow = Date.now(); + + for ( ; i < timers.length; i++ ) { + timer = timers[ i ]; + + // Run the timer and safely remove it when done (allowing for external removal) + if ( !timer() && timers[ i ] === timer ) { + timers.splice( i--, 1 ); + } + } + + if ( !timers.length ) { + jQuery.fx.stop(); + } + fxNow = undefined; +}; + +jQuery.fx.timer = function( timer ) { + jQuery.timers.push( timer ); + jQuery.fx.start(); +}; + +jQuery.fx.interval = 13; +jQuery.fx.start = function() { + if ( inProgress ) { + return; + } + + inProgress = true; + schedule(); +}; + +jQuery.fx.stop = function() { + inProgress = null; +}; + +jQuery.fx.speeds = { + slow: 600, + fast: 200, + + // Default speed + _default: 400 +}; + + +// Based off of the plugin by Clint Helfers, with permission. +// https://web.archive.org/web/20100324014747/http://blindsignals.com/index.php/2009/07/jquery-delay/ +jQuery.fn.delay = function( time, type ) { + time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; + type = type || "fx"; + + return this.queue( type, function( next, hooks ) { + var timeout = window.setTimeout( next, time ); + hooks.stop = function() { + window.clearTimeout( timeout ); + }; + } ); +}; + + +( function() { + var input = document.createElement( "input" ), + select = document.createElement( "select" ), + opt = select.appendChild( document.createElement( "option" ) ); + + input.type = "checkbox"; + + // Support: Android <=4.3 only + // Default value for a checkbox should be "on" + support.checkOn = input.value !== ""; + + // Support: IE <=11 only + // Must access selectedIndex to make default options select + support.optSelected = opt.selected; + + // Support: IE <=11 only + // An input loses its value after becoming a radio + input = document.createElement( "input" ); + input.value = "t"; + input.type = "radio"; + support.radioValue = input.value === "t"; +} )(); + + +var boolHook, + attrHandle = jQuery.expr.attrHandle; + +jQuery.fn.extend( { + attr: function( name, value ) { + return access( this, jQuery.attr, name, value, arguments.length > 1 ); + }, + + removeAttr: function( name ) { + return this.each( function() { + jQuery.removeAttr( this, name ); + } ); + } +} ); + +jQuery.extend( { + attr: function( elem, name, value ) { + var ret, hooks, + nType = elem.nodeType; + + // Don't get/set attributes on text, comment and attribute nodes + if ( nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + // Fallback to prop when attributes are not supported + if ( typeof elem.getAttribute === "undefined" ) { + return jQuery.prop( elem, name, value ); + } + + // Attribute hooks are determined by the lowercase version + // Grab necessary hook if one is defined + if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { + hooks = jQuery.attrHooks[ name.toLowerCase() ] || + ( jQuery.expr.match.bool.test( name ) ? boolHook : undefined ); + } + + if ( value !== undefined ) { + if ( value === null ) { + jQuery.removeAttr( elem, name ); + return; + } + + if ( hooks && "set" in hooks && + ( ret = hooks.set( elem, value, name ) ) !== undefined ) { + return ret; + } + + elem.setAttribute( name, value + "" ); + return value; + } + + if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) { + return ret; + } + + ret = jQuery.find.attr( elem, name ); + + // Non-existent attributes return null, we normalize to undefined + return ret == null ? undefined : ret; + }, + + attrHooks: { + type: { + set: function( elem, value ) { + if ( !support.radioValue && value === "radio" && + nodeName( elem, "input" ) ) { + var val = elem.value; + elem.setAttribute( "type", value ); + if ( val ) { + elem.value = val; + } + return value; + } + } + } + }, + + removeAttr: function( elem, value ) { + var name, + i = 0, + + // Attribute names can contain non-HTML whitespace characters + // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 + attrNames = value && value.match( rnothtmlwhite ); + + if ( attrNames && elem.nodeType === 1 ) { + while ( ( name = attrNames[ i++ ] ) ) { + elem.removeAttribute( name ); + } + } + } +} ); + +// Hooks for boolean attributes +boolHook = { + set: function( elem, value, name ) { + if ( value === false ) { + + // Remove boolean attributes when set to false + jQuery.removeAttr( elem, name ); + } else { + elem.setAttribute( name, name ); + } + return name; + } +}; + +jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( _i, name ) { + var getter = attrHandle[ name ] || jQuery.find.attr; + + attrHandle[ name ] = function( elem, name, isXML ) { + var ret, handle, + lowercaseName = name.toLowerCase(); + + if ( !isXML ) { + + // Avoid an infinite loop by temporarily removing this function from the getter + handle = attrHandle[ lowercaseName ]; + attrHandle[ lowercaseName ] = ret; + ret = getter( elem, name, isXML ) != null ? + lowercaseName : + null; + attrHandle[ lowercaseName ] = handle; + } + return ret; + }; +} ); + + + + +var rfocusable = /^(?:input|select|textarea|button)$/i, + rclickable = /^(?:a|area)$/i; + +jQuery.fn.extend( { + prop: function( name, value ) { + return access( this, jQuery.prop, name, value, arguments.length > 1 ); + }, + + removeProp: function( name ) { + return this.each( function() { + delete this[ jQuery.propFix[ name ] || name ]; + } ); + } +} ); + +jQuery.extend( { + prop: function( elem, name, value ) { + var ret, hooks, + nType = elem.nodeType; + + // Don't get/set properties on text, comment and attribute nodes + if ( nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { + + // Fix name and attach hooks + name = jQuery.propFix[ name ] || name; + hooks = jQuery.propHooks[ name ]; + } + + if ( value !== undefined ) { + if ( hooks && "set" in hooks && + ( ret = hooks.set( elem, value, name ) ) !== undefined ) { + return ret; + } + + return ( elem[ name ] = value ); + } + + if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) { + return ret; + } + + return elem[ name ]; + }, + + propHooks: { + tabIndex: { + get: function( elem ) { + + // Support: IE <=9 - 11 only + // elem.tabIndex doesn't always return the + // correct value when it hasn't been explicitly set + // https://web.archive.org/web/20141116233347/http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ + // Use proper attribute retrieval(#12072) + var tabindex = jQuery.find.attr( elem, "tabindex" ); + + if ( tabindex ) { + return parseInt( tabindex, 10 ); + } + + if ( + rfocusable.test( elem.nodeName ) || + rclickable.test( elem.nodeName ) && + elem.href + ) { + return 0; + } + + return -1; + } + } + }, + + propFix: { + "for": "htmlFor", + "class": "className" + } +} ); + +// Support: IE <=11 only +// Accessing the selectedIndex property +// forces the browser to respect setting selected +// on the option +// The getter ensures a default option is selected +// when in an optgroup +// eslint rule "no-unused-expressions" is disabled for this code +// since it considers such accessions noop +if ( !support.optSelected ) { + jQuery.propHooks.selected = { + get: function( elem ) { + + /* eslint no-unused-expressions: "off" */ + + var parent = elem.parentNode; + if ( parent && parent.parentNode ) { + parent.parentNode.selectedIndex; + } + return null; + }, + set: function( elem ) { + + /* eslint no-unused-expressions: "off" */ + + var parent = elem.parentNode; + if ( parent ) { + parent.selectedIndex; + + if ( parent.parentNode ) { + parent.parentNode.selectedIndex; + } + } + } + }; +} + +jQuery.each( [ + "tabIndex", + "readOnly", + "maxLength", + "cellSpacing", + "cellPadding", + "rowSpan", + "colSpan", + "useMap", + "frameBorder", + "contentEditable" +], function() { + jQuery.propFix[ this.toLowerCase() ] = this; +} ); + + + + + // Strip and collapse whitespace according to HTML spec + // https://infra.spec.whatwg.org/#strip-and-collapse-ascii-whitespace + function stripAndCollapse( value ) { + var tokens = value.match( rnothtmlwhite ) || []; + return tokens.join( " " ); + } + + +function getClass( elem ) { + return elem.getAttribute && elem.getAttribute( "class" ) || ""; +} + +function classesToArray( value ) { + if ( Array.isArray( value ) ) { + return value; + } + if ( typeof value === "string" ) { + return value.match( rnothtmlwhite ) || []; + } + return []; +} + +jQuery.fn.extend( { + addClass: function( value ) { + var classes, elem, cur, curValue, clazz, j, finalValue, + i = 0; + + if ( isFunction( value ) ) { + return this.each( function( j ) { + jQuery( this ).addClass( value.call( this, j, getClass( this ) ) ); + } ); + } + + classes = classesToArray( value ); + + if ( classes.length ) { + while ( ( elem = this[ i++ ] ) ) { + curValue = getClass( elem ); + cur = elem.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " ); + + if ( cur ) { + j = 0; + while ( ( clazz = classes[ j++ ] ) ) { + if ( cur.indexOf( " " + clazz + " " ) < 0 ) { + cur += clazz + " "; + } + } + + // Only assign if different to avoid unneeded rendering. + finalValue = stripAndCollapse( cur ); + if ( curValue !== finalValue ) { + elem.setAttribute( "class", finalValue ); + } + } + } + } + + return this; + }, + + removeClass: function( value ) { + var classes, elem, cur, curValue, clazz, j, finalValue, + i = 0; + + if ( isFunction( value ) ) { + return this.each( function( j ) { + jQuery( this ).removeClass( value.call( this, j, getClass( this ) ) ); + } ); + } + + if ( !arguments.length ) { + return this.attr( "class", "" ); + } + + classes = classesToArray( value ); + + if ( classes.length ) { + while ( ( elem = this[ i++ ] ) ) { + curValue = getClass( elem ); + + // This expression is here for better compressibility (see addClass) + cur = elem.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " ); + + if ( cur ) { + j = 0; + while ( ( clazz = classes[ j++ ] ) ) { + + // Remove *all* instances + while ( cur.indexOf( " " + clazz + " " ) > -1 ) { + cur = cur.replace( " " + clazz + " ", " " ); + } + } + + // Only assign if different to avoid unneeded rendering. + finalValue = stripAndCollapse( cur ); + if ( curValue !== finalValue ) { + elem.setAttribute( "class", finalValue ); + } + } + } + } + + return this; + }, + + toggleClass: function( value, stateVal ) { + var type = typeof value, + isValidValue = type === "string" || Array.isArray( value ); + + if ( typeof stateVal === "boolean" && isValidValue ) { + return stateVal ? this.addClass( value ) : this.removeClass( value ); + } + + if ( isFunction( value ) ) { + return this.each( function( i ) { + jQuery( this ).toggleClass( + value.call( this, i, getClass( this ), stateVal ), + stateVal + ); + } ); + } + + return this.each( function() { + var className, i, self, classNames; + + if ( isValidValue ) { + + // Toggle individual class names + i = 0; + self = jQuery( this ); + classNames = classesToArray( value ); + + while ( ( className = classNames[ i++ ] ) ) { + + // Check each className given, space separated list + if ( self.hasClass( className ) ) { + self.removeClass( className ); + } else { + self.addClass( className ); + } + } + + // Toggle whole class name + } else if ( value === undefined || type === "boolean" ) { + className = getClass( this ); + if ( className ) { + + // Store className if set + dataPriv.set( this, "__className__", className ); + } + + // If the element has a class name or if we're passed `false`, + // then remove the whole classname (if there was one, the above saved it). + // Otherwise bring back whatever was previously saved (if anything), + // falling back to the empty string if nothing was stored. + if ( this.setAttribute ) { + this.setAttribute( "class", + className || value === false ? + "" : + dataPriv.get( this, "__className__" ) || "" + ); + } + } + } ); + }, + + hasClass: function( selector ) { + var className, elem, + i = 0; + + className = " " + selector + " "; + while ( ( elem = this[ i++ ] ) ) { + if ( elem.nodeType === 1 && + ( " " + stripAndCollapse( getClass( elem ) ) + " " ).indexOf( className ) > -1 ) { + return true; + } + } + + return false; + } +} ); + + + + +var rreturn = /\r/g; + +jQuery.fn.extend( { + val: function( value ) { + var hooks, ret, valueIsFunction, + elem = this[ 0 ]; + + if ( !arguments.length ) { + if ( elem ) { + hooks = jQuery.valHooks[ elem.type ] || + jQuery.valHooks[ elem.nodeName.toLowerCase() ]; + + if ( hooks && + "get" in hooks && + ( ret = hooks.get( elem, "value" ) ) !== undefined + ) { + return ret; + } + + ret = elem.value; + + // Handle most common string cases + if ( typeof ret === "string" ) { + return ret.replace( rreturn, "" ); + } + + // Handle cases where value is null/undef or number + return ret == null ? "" : ret; + } + + return; + } + + valueIsFunction = isFunction( value ); + + return this.each( function( i ) { + var val; + + if ( this.nodeType !== 1 ) { + return; + } + + if ( valueIsFunction ) { + val = value.call( this, i, jQuery( this ).val() ); + } else { + val = value; + } + + // Treat null/undefined as ""; convert numbers to string + if ( val == null ) { + val = ""; + + } else if ( typeof val === "number" ) { + val += ""; + + } else if ( Array.isArray( val ) ) { + val = jQuery.map( val, function( value ) { + return value == null ? "" : value + ""; + } ); + } + + hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; + + // If set returns undefined, fall back to normal setting + if ( !hooks || !( "set" in hooks ) || hooks.set( this, val, "value" ) === undefined ) { + this.value = val; + } + } ); + } +} ); + +jQuery.extend( { + valHooks: { + option: { + get: function( elem ) { + + var val = jQuery.find.attr( elem, "value" ); + return val != null ? + val : + + // Support: IE <=10 - 11 only + // option.text throws exceptions (#14686, #14858) + // Strip and collapse whitespace + // https://html.spec.whatwg.org/#strip-and-collapse-whitespace + stripAndCollapse( jQuery.text( elem ) ); + } + }, + select: { + get: function( elem ) { + var value, option, i, + options = elem.options, + index = elem.selectedIndex, + one = elem.type === "select-one", + values = one ? null : [], + max = one ? index + 1 : options.length; + + if ( index < 0 ) { + i = max; + + } else { + i = one ? index : 0; + } + + // Loop through all the selected options + for ( ; i < max; i++ ) { + option = options[ i ]; + + // Support: IE <=9 only + // IE8-9 doesn't update selected after form reset (#2551) + if ( ( option.selected || i === index ) && + + // Don't return options that are disabled or in a disabled optgroup + !option.disabled && + ( !option.parentNode.disabled || + !nodeName( option.parentNode, "optgroup" ) ) ) { + + // Get the specific value for the option + value = jQuery( option ).val(); + + // We don't need an array for one selects + if ( one ) { + return value; + } + + // Multi-Selects return an array + values.push( value ); + } + } + + return values; + }, + + set: function( elem, value ) { + var optionSet, option, + options = elem.options, + values = jQuery.makeArray( value ), + i = options.length; + + while ( i-- ) { + option = options[ i ]; + + /* eslint-disable no-cond-assign */ + + if ( option.selected = + jQuery.inArray( jQuery.valHooks.option.get( option ), values ) > -1 + ) { + optionSet = true; + } + + /* eslint-enable no-cond-assign */ + } + + // Force browsers to behave consistently when non-matching value is set + if ( !optionSet ) { + elem.selectedIndex = -1; + } + return values; + } + } + } +} ); + +// Radios and checkboxes getter/setter +jQuery.each( [ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = { + set: function( elem, value ) { + if ( Array.isArray( value ) ) { + return ( elem.checked = jQuery.inArray( jQuery( elem ).val(), value ) > -1 ); + } + } + }; + if ( !support.checkOn ) { + jQuery.valHooks[ this ].get = function( elem ) { + return elem.getAttribute( "value" ) === null ? "on" : elem.value; + }; + } +} ); + + + + +// Return jQuery for attributes-only inclusion + + +support.focusin = "onfocusin" in window; + + +var rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, + stopPropagationCallback = function( e ) { + e.stopPropagation(); + }; + +jQuery.extend( jQuery.event, { + + trigger: function( event, data, elem, onlyHandlers ) { + + var i, cur, tmp, bubbleType, ontype, handle, special, lastElement, + eventPath = [ elem || document ], + type = hasOwn.call( event, "type" ) ? event.type : event, + namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split( "." ) : []; + + cur = lastElement = tmp = elem = elem || document; + + // Don't do events on text and comment nodes + if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + return; + } + + // focus/blur morphs to focusin/out; ensure we're not firing them right now + if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { + return; + } + + if ( type.indexOf( "." ) > -1 ) { + + // Namespaced trigger; create a regexp to match event type in handle() + namespaces = type.split( "." ); + type = namespaces.shift(); + namespaces.sort(); + } + ontype = type.indexOf( ":" ) < 0 && "on" + type; + + // Caller can pass in a jQuery.Event object, Object, or just an event type string + event = event[ jQuery.expando ] ? + event : + new jQuery.Event( type, typeof event === "object" && event ); + + // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true) + event.isTrigger = onlyHandlers ? 2 : 3; + event.namespace = namespaces.join( "." ); + event.rnamespace = event.namespace ? + new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ) : + null; + + // Clean up the event in case it is being reused + event.result = undefined; + if ( !event.target ) { + event.target = elem; + } + + // Clone any incoming data and prepend the event, creating the handler arg list + data = data == null ? + [ event ] : + jQuery.makeArray( data, [ event ] ); + + // Allow special events to draw outside the lines + special = jQuery.event.special[ type ] || {}; + if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { + return; + } + + // Determine event propagation path in advance, per W3C events spec (#9951) + // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) + if ( !onlyHandlers && !special.noBubble && !isWindow( elem ) ) { + + bubbleType = special.delegateType || type; + if ( !rfocusMorph.test( bubbleType + type ) ) { + cur = cur.parentNode; + } + for ( ; cur; cur = cur.parentNode ) { + eventPath.push( cur ); + tmp = cur; + } + + // Only add window if we got to document (e.g., not plain obj or detached DOM) + if ( tmp === ( elem.ownerDocument || document ) ) { + eventPath.push( tmp.defaultView || tmp.parentWindow || window ); + } + } + + // Fire handlers on the event path + i = 0; + while ( ( cur = eventPath[ i++ ] ) && !event.isPropagationStopped() ) { + lastElement = cur; + event.type = i > 1 ? + bubbleType : + special.bindType || type; + + // jQuery handler + handle = ( dataPriv.get( cur, "events" ) || Object.create( null ) )[ event.type ] && + dataPriv.get( cur, "handle" ); + if ( handle ) { + handle.apply( cur, data ); + } + + // Native handler + handle = ontype && cur[ ontype ]; + if ( handle && handle.apply && acceptData( cur ) ) { + event.result = handle.apply( cur, data ); + if ( event.result === false ) { + event.preventDefault(); + } + } + } + event.type = type; + + // If nobody prevented the default action, do it now + if ( !onlyHandlers && !event.isDefaultPrevented() ) { + + if ( ( !special._default || + special._default.apply( eventPath.pop(), data ) === false ) && + acceptData( elem ) ) { + + // Call a native DOM method on the target with the same name as the event. + // Don't do default actions on window, that's where global variables be (#6170) + if ( ontype && isFunction( elem[ type ] ) && !isWindow( elem ) ) { + + // Don't re-trigger an onFOO event when we call its FOO() method + tmp = elem[ ontype ]; + + if ( tmp ) { + elem[ ontype ] = null; + } + + // Prevent re-triggering of the same event, since we already bubbled it above + jQuery.event.triggered = type; + + if ( event.isPropagationStopped() ) { + lastElement.addEventListener( type, stopPropagationCallback ); + } + + elem[ type ](); + + if ( event.isPropagationStopped() ) { + lastElement.removeEventListener( type, stopPropagationCallback ); + } + + jQuery.event.triggered = undefined; + + if ( tmp ) { + elem[ ontype ] = tmp; + } + } + } + } + + return event.result; + }, + + // Piggyback on a donor event to simulate a different one + // Used only for `focus(in | out)` events + simulate: function( type, elem, event ) { + var e = jQuery.extend( + new jQuery.Event(), + event, + { + type: type, + isSimulated: true + } + ); + + jQuery.event.trigger( e, null, elem ); + } + +} ); + +jQuery.fn.extend( { + + trigger: function( type, data ) { + return this.each( function() { + jQuery.event.trigger( type, data, this ); + } ); + }, + triggerHandler: function( type, data ) { + var elem = this[ 0 ]; + if ( elem ) { + return jQuery.event.trigger( type, data, elem, true ); + } + } +} ); + + +// Support: Firefox <=44 +// Firefox doesn't have focus(in | out) events +// Related ticket - https://bugzilla.mozilla.org/show_bug.cgi?id=687787 +// +// Support: Chrome <=48 - 49, Safari <=9.0 - 9.1 +// focus(in | out) events fire after focus & blur events, +// which is spec violation - http://www.w3.org/TR/DOM-Level-3-Events/#events-focusevent-event-order +// Related ticket - https://bugs.chromium.org/p/chromium/issues/detail?id=449857 +if ( !support.focusin ) { + jQuery.each( { focus: "focusin", blur: "focusout" }, function( orig, fix ) { + + // Attach a single capturing handler on the document while someone wants focusin/focusout + var handler = function( event ) { + jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ) ); + }; + + jQuery.event.special[ fix ] = { + setup: function() { + + // Handle: regular nodes (via `this.ownerDocument`), window + // (via `this.document`) & document (via `this`). + var doc = this.ownerDocument || this.document || this, + attaches = dataPriv.access( doc, fix ); + + if ( !attaches ) { + doc.addEventListener( orig, handler, true ); + } + dataPriv.access( doc, fix, ( attaches || 0 ) + 1 ); + }, + teardown: function() { + var doc = this.ownerDocument || this.document || this, + attaches = dataPriv.access( doc, fix ) - 1; + + if ( !attaches ) { + doc.removeEventListener( orig, handler, true ); + dataPriv.remove( doc, fix ); + + } else { + dataPriv.access( doc, fix, attaches ); + } + } + }; + } ); +} +var location = window.location; + +var nonce = { guid: Date.now() }; + +var rquery = ( /\?/ ); + + + +// Cross-browser xml parsing +jQuery.parseXML = function( data ) { + var xml, parserErrorElem; + if ( !data || typeof data !== "string" ) { + return null; + } + + // Support: IE 9 - 11 only + // IE throws on parseFromString with invalid input. + try { + xml = ( new window.DOMParser() ).parseFromString( data, "text/xml" ); + } catch ( e ) {} + + parserErrorElem = xml && xml.getElementsByTagName( "parsererror" )[ 0 ]; + if ( !xml || parserErrorElem ) { + jQuery.error( "Invalid XML: " + ( + parserErrorElem ? + jQuery.map( parserErrorElem.childNodes, function( el ) { + return el.textContent; + } ).join( "\n" ) : + data + ) ); + } + return xml; +}; + + +var + rbracket = /\[\]$/, + rCRLF = /\r?\n/g, + rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i, + rsubmittable = /^(?:input|select|textarea|keygen)/i; + +function buildParams( prefix, obj, traditional, add ) { + var name; + + if ( Array.isArray( obj ) ) { + + // Serialize array item. + jQuery.each( obj, function( i, v ) { + if ( traditional || rbracket.test( prefix ) ) { + + // Treat each array item as a scalar. + add( prefix, v ); + + } else { + + // Item is non-scalar (array or object), encode its numeric index. + buildParams( + prefix + "[" + ( typeof v === "object" && v != null ? i : "" ) + "]", + v, + traditional, + add + ); + } + } ); + + } else if ( !traditional && toType( obj ) === "object" ) { + + // Serialize object item. + for ( name in obj ) { + buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add ); + } + + } else { + + // Serialize scalar item. + add( prefix, obj ); + } +} + +// Serialize an array of form elements or a set of +// key/values into a query string +jQuery.param = function( a, traditional ) { + var prefix, + s = [], + add = function( key, valueOrFunction ) { + + // If value is a function, invoke it and use its return value + var value = isFunction( valueOrFunction ) ? + valueOrFunction() : + valueOrFunction; + + s[ s.length ] = encodeURIComponent( key ) + "=" + + encodeURIComponent( value == null ? "" : value ); + }; + + if ( a == null ) { + return ""; + } + + // If an array was passed in, assume that it is an array of form elements. + if ( Array.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) { + + // Serialize the form elements + jQuery.each( a, function() { + add( this.name, this.value ); + } ); + + } else { + + // If traditional, encode the "old" way (the way 1.3.2 or older + // did it), otherwise encode params recursively. + for ( prefix in a ) { + buildParams( prefix, a[ prefix ], traditional, add ); + } + } + + // Return the resulting serialization + return s.join( "&" ); +}; + +jQuery.fn.extend( { + serialize: function() { + return jQuery.param( this.serializeArray() ); + }, + serializeArray: function() { + return this.map( function() { + + // Can add propHook for "elements" to filter or add form elements + var elements = jQuery.prop( this, "elements" ); + return elements ? jQuery.makeArray( elements ) : this; + } ).filter( function() { + var type = this.type; + + // Use .is( ":disabled" ) so that fieldset[disabled] works + return this.name && !jQuery( this ).is( ":disabled" ) && + rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) && + ( this.checked || !rcheckableType.test( type ) ); + } ).map( function( _i, elem ) { + var val = jQuery( this ).val(); + + if ( val == null ) { + return null; + } + + if ( Array.isArray( val ) ) { + return jQuery.map( val, function( val ) { + return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; + } ); + } + + return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; + } ).get(); + } +} ); + + +var + r20 = /%20/g, + rhash = /#.*$/, + rantiCache = /([?&])_=[^&]*/, + rheaders = /^(.*?):[ \t]*([^\r\n]*)$/mg, + + // #7653, #8125, #8152: local protocol detection + rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/, + rnoContent = /^(?:GET|HEAD)$/, + rprotocol = /^\/\//, + + /* Prefilters + * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example) + * 2) These are called: + * - BEFORE asking for a transport + * - AFTER param serialization (s.data is a string if s.processData is true) + * 3) key is the dataType + * 4) the catchall symbol "*" can be used + * 5) execution will start with transport dataType and THEN continue down to "*" if needed + */ + prefilters = {}, + + /* Transports bindings + * 1) key is the dataType + * 2) the catchall symbol "*" can be used + * 3) selection will start with transport dataType and THEN go to "*" if needed + */ + transports = {}, + + // Avoid comment-prolog char sequence (#10098); must appease lint and evade compression + allTypes = "*/".concat( "*" ), + + // Anchor tag for parsing the document origin + originAnchor = document.createElement( "a" ); + +originAnchor.href = location.href; + +// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport +function addToPrefiltersOrTransports( structure ) { + + // dataTypeExpression is optional and defaults to "*" + return function( dataTypeExpression, func ) { + + if ( typeof dataTypeExpression !== "string" ) { + func = dataTypeExpression; + dataTypeExpression = "*"; + } + + var dataType, + i = 0, + dataTypes = dataTypeExpression.toLowerCase().match( rnothtmlwhite ) || []; + + if ( isFunction( func ) ) { + + // For each dataType in the dataTypeExpression + while ( ( dataType = dataTypes[ i++ ] ) ) { + + // Prepend if requested + if ( dataType[ 0 ] === "+" ) { + dataType = dataType.slice( 1 ) || "*"; + ( structure[ dataType ] = structure[ dataType ] || [] ).unshift( func ); + + // Otherwise append + } else { + ( structure[ dataType ] = structure[ dataType ] || [] ).push( func ); + } + } + } + }; +} + +// Base inspection function for prefilters and transports +function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) { + + var inspected = {}, + seekingTransport = ( structure === transports ); + + function inspect( dataType ) { + var selected; + inspected[ dataType ] = true; + jQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) { + var dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR ); + if ( typeof dataTypeOrTransport === "string" && + !seekingTransport && !inspected[ dataTypeOrTransport ] ) { + + options.dataTypes.unshift( dataTypeOrTransport ); + inspect( dataTypeOrTransport ); + return false; + } else if ( seekingTransport ) { + return !( selected = dataTypeOrTransport ); + } + } ); + return selected; + } + + return inspect( options.dataTypes[ 0 ] ) || !inspected[ "*" ] && inspect( "*" ); +} + +// A special extend for ajax options +// that takes "flat" options (not to be deep extended) +// Fixes #9887 +function ajaxExtend( target, src ) { + var key, deep, + flatOptions = jQuery.ajaxSettings.flatOptions || {}; + + for ( key in src ) { + if ( src[ key ] !== undefined ) { + ( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ]; + } + } + if ( deep ) { + jQuery.extend( true, target, deep ); + } + + return target; +} + +/* Handles responses to an ajax request: + * - finds the right dataType (mediates between content-type and expected dataType) + * - returns the corresponding response + */ +function ajaxHandleResponses( s, jqXHR, responses ) { + + var ct, type, finalDataType, firstDataType, + contents = s.contents, + dataTypes = s.dataTypes; + + // Remove auto dataType and get content-type in the process + while ( dataTypes[ 0 ] === "*" ) { + dataTypes.shift(); + if ( ct === undefined ) { + ct = s.mimeType || jqXHR.getResponseHeader( "Content-Type" ); + } + } + + // Check if we're dealing with a known content-type + if ( ct ) { + for ( type in contents ) { + if ( contents[ type ] && contents[ type ].test( ct ) ) { + dataTypes.unshift( type ); + break; + } + } + } + + // Check to see if we have a response for the expected dataType + if ( dataTypes[ 0 ] in responses ) { + finalDataType = dataTypes[ 0 ]; + } else { + + // Try convertible dataTypes + for ( type in responses ) { + if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[ 0 ] ] ) { + finalDataType = type; + break; + } + if ( !firstDataType ) { + firstDataType = type; + } + } + + // Or just use first one + finalDataType = finalDataType || firstDataType; + } + + // If we found a dataType + // We add the dataType to the list if needed + // and return the corresponding response + if ( finalDataType ) { + if ( finalDataType !== dataTypes[ 0 ] ) { + dataTypes.unshift( finalDataType ); + } + return responses[ finalDataType ]; + } +} + +/* Chain conversions given the request and the original response + * Also sets the responseXXX fields on the jqXHR instance + */ +function ajaxConvert( s, response, jqXHR, isSuccess ) { + var conv2, current, conv, tmp, prev, + converters = {}, + + // Work with a copy of dataTypes in case we need to modify it for conversion + dataTypes = s.dataTypes.slice(); + + // Create converters map with lowercased keys + if ( dataTypes[ 1 ] ) { + for ( conv in s.converters ) { + converters[ conv.toLowerCase() ] = s.converters[ conv ]; + } + } + + current = dataTypes.shift(); + + // Convert to each sequential dataType + while ( current ) { + + if ( s.responseFields[ current ] ) { + jqXHR[ s.responseFields[ current ] ] = response; + } + + // Apply the dataFilter if provided + if ( !prev && isSuccess && s.dataFilter ) { + response = s.dataFilter( response, s.dataType ); + } + + prev = current; + current = dataTypes.shift(); + + if ( current ) { + + // There's only work to do if current dataType is non-auto + if ( current === "*" ) { + + current = prev; + + // Convert response if prev dataType is non-auto and differs from current + } else if ( prev !== "*" && prev !== current ) { + + // Seek a direct converter + conv = converters[ prev + " " + current ] || converters[ "* " + current ]; + + // If none found, seek a pair + if ( !conv ) { + for ( conv2 in converters ) { + + // If conv2 outputs current + tmp = conv2.split( " " ); + if ( tmp[ 1 ] === current ) { + + // If prev can be converted to accepted input + conv = converters[ prev + " " + tmp[ 0 ] ] || + converters[ "* " + tmp[ 0 ] ]; + if ( conv ) { + + // Condense equivalence converters + if ( conv === true ) { + conv = converters[ conv2 ]; + + // Otherwise, insert the intermediate dataType + } else if ( converters[ conv2 ] !== true ) { + current = tmp[ 0 ]; + dataTypes.unshift( tmp[ 1 ] ); + } + break; + } + } + } + } + + // Apply converter (if not an equivalence) + if ( conv !== true ) { + + // Unless errors are allowed to bubble, catch and return them + if ( conv && s.throws ) { + response = conv( response ); + } else { + try { + response = conv( response ); + } catch ( e ) { + return { + state: "parsererror", + error: conv ? e : "No conversion from " + prev + " to " + current + }; + } + } + } + } + } + } + + return { state: "success", data: response }; +} + +jQuery.extend( { + + // Counter for holding the number of active queries + active: 0, + + // Last-Modified header cache for next request + lastModified: {}, + etag: {}, + + ajaxSettings: { + url: location.href, + type: "GET", + isLocal: rlocalProtocol.test( location.protocol ), + global: true, + processData: true, + async: true, + contentType: "application/x-www-form-urlencoded; charset=UTF-8", + + /* + timeout: 0, + data: null, + dataType: null, + username: null, + password: null, + cache: null, + throws: false, + traditional: false, + headers: {}, + */ + + accepts: { + "*": allTypes, + text: "text/plain", + html: "text/html", + xml: "application/xml, text/xml", + json: "application/json, text/javascript" + }, + + contents: { + xml: /\bxml\b/, + html: /\bhtml/, + json: /\bjson\b/ + }, + + responseFields: { + xml: "responseXML", + text: "responseText", + json: "responseJSON" + }, + + // Data converters + // Keys separate source (or catchall "*") and destination types with a single space + converters: { + + // Convert anything to text + "* text": String, + + // Text to html (true = no transformation) + "text html": true, + + // Evaluate text as a json expression + "text json": JSON.parse, + + // Parse text as xml + "text xml": jQuery.parseXML + }, + + // For options that shouldn't be deep extended: + // you can add your own custom options here if + // and when you create one that shouldn't be + // deep extended (see ajaxExtend) + flatOptions: { + url: true, + context: true + } + }, + + // Creates a full fledged settings object into target + // with both ajaxSettings and settings fields. + // If target is omitted, writes into ajaxSettings. + ajaxSetup: function( target, settings ) { + return settings ? + + // Building a settings object + ajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) : + + // Extending ajaxSettings + ajaxExtend( jQuery.ajaxSettings, target ); + }, + + ajaxPrefilter: addToPrefiltersOrTransports( prefilters ), + ajaxTransport: addToPrefiltersOrTransports( transports ), + + // Main method + ajax: function( url, options ) { + + // If url is an object, simulate pre-1.5 signature + if ( typeof url === "object" ) { + options = url; + url = undefined; + } + + // Force options to be an object + options = options || {}; + + var transport, + + // URL without anti-cache param + cacheURL, + + // Response headers + responseHeadersString, + responseHeaders, + + // timeout handle + timeoutTimer, + + // Url cleanup var + urlAnchor, + + // Request state (becomes false upon send and true upon completion) + completed, + + // To know if global events are to be dispatched + fireGlobals, + + // Loop variable + i, + + // uncached part of the url + uncached, + + // Create the final options object + s = jQuery.ajaxSetup( {}, options ), + + // Callbacks context + callbackContext = s.context || s, + + // Context for global events is callbackContext if it is a DOM node or jQuery collection + globalEventContext = s.context && + ( callbackContext.nodeType || callbackContext.jquery ) ? + jQuery( callbackContext ) : + jQuery.event, + + // Deferreds + deferred = jQuery.Deferred(), + completeDeferred = jQuery.Callbacks( "once memory" ), + + // Status-dependent callbacks + statusCode = s.statusCode || {}, + + // Headers (they are sent all at once) + requestHeaders = {}, + requestHeadersNames = {}, + + // Default abort message + strAbort = "canceled", + + // Fake xhr + jqXHR = { + readyState: 0, + + // Builds headers hashtable if needed + getResponseHeader: function( key ) { + var match; + if ( completed ) { + if ( !responseHeaders ) { + responseHeaders = {}; + while ( ( match = rheaders.exec( responseHeadersString ) ) ) { + responseHeaders[ match[ 1 ].toLowerCase() + " " ] = + ( responseHeaders[ match[ 1 ].toLowerCase() + " " ] || [] ) + .concat( match[ 2 ] ); + } + } + match = responseHeaders[ key.toLowerCase() + " " ]; + } + return match == null ? null : match.join( ", " ); + }, + + // Raw string + getAllResponseHeaders: function() { + return completed ? responseHeadersString : null; + }, + + // Caches the header + setRequestHeader: function( name, value ) { + if ( completed == null ) { + name = requestHeadersNames[ name.toLowerCase() ] = + requestHeadersNames[ name.toLowerCase() ] || name; + requestHeaders[ name ] = value; + } + return this; + }, + + // Overrides response content-type header + overrideMimeType: function( type ) { + if ( completed == null ) { + s.mimeType = type; + } + return this; + }, + + // Status-dependent callbacks + statusCode: function( map ) { + var code; + if ( map ) { + if ( completed ) { + + // Execute the appropriate callbacks + jqXHR.always( map[ jqXHR.status ] ); + } else { + + // Lazy-add the new callbacks in a way that preserves old ones + for ( code in map ) { + statusCode[ code ] = [ statusCode[ code ], map[ code ] ]; + } + } + } + return this; + }, + + // Cancel the request + abort: function( statusText ) { + var finalText = statusText || strAbort; + if ( transport ) { + transport.abort( finalText ); + } + done( 0, finalText ); + return this; + } + }; + + // Attach deferreds + deferred.promise( jqXHR ); + + // Add protocol if not provided (prefilters might expect it) + // Handle falsy url in the settings object (#10093: consistency with old signature) + // We also use the url parameter if available + s.url = ( ( url || s.url || location.href ) + "" ) + .replace( rprotocol, location.protocol + "//" ); + + // Alias method option to type as per ticket #12004 + s.type = options.method || options.type || s.method || s.type; + + // Extract dataTypes list + s.dataTypes = ( s.dataType || "*" ).toLowerCase().match( rnothtmlwhite ) || [ "" ]; + + // A cross-domain request is in order when the origin doesn't match the current origin. + if ( s.crossDomain == null ) { + urlAnchor = document.createElement( "a" ); + + // Support: IE <=8 - 11, Edge 12 - 15 + // IE throws exception on accessing the href property if url is malformed, + // e.g. http://example.com:80x/ + try { + urlAnchor.href = s.url; + + // Support: IE <=8 - 11 only + // Anchor's host property isn't correctly set when s.url is relative + urlAnchor.href = urlAnchor.href; + s.crossDomain = originAnchor.protocol + "//" + originAnchor.host !== + urlAnchor.protocol + "//" + urlAnchor.host; + } catch ( e ) { + + // If there is an error parsing the URL, assume it is crossDomain, + // it can be rejected by the transport if it is invalid + s.crossDomain = true; + } + } + + // Convert data if not already a string + if ( s.data && s.processData && typeof s.data !== "string" ) { + s.data = jQuery.param( s.data, s.traditional ); + } + + // Apply prefilters + inspectPrefiltersOrTransports( prefilters, s, options, jqXHR ); + + // If request was aborted inside a prefilter, stop there + if ( completed ) { + return jqXHR; + } + + // We can fire global events as of now if asked to + // Don't fire events if jQuery.event is undefined in an AMD-usage scenario (#15118) + fireGlobals = jQuery.event && s.global; + + // Watch for a new set of requests + if ( fireGlobals && jQuery.active++ === 0 ) { + jQuery.event.trigger( "ajaxStart" ); + } + + // Uppercase the type + s.type = s.type.toUpperCase(); + + // Determine if request has content + s.hasContent = !rnoContent.test( s.type ); + + // Save the URL in case we're toying with the If-Modified-Since + // and/or If-None-Match header later on + // Remove hash to simplify url manipulation + cacheURL = s.url.replace( rhash, "" ); + + // More options handling for requests with no content + if ( !s.hasContent ) { + + // Remember the hash so we can put it back + uncached = s.url.slice( cacheURL.length ); + + // If data is available and should be processed, append data to url + if ( s.data && ( s.processData || typeof s.data === "string" ) ) { + cacheURL += ( rquery.test( cacheURL ) ? "&" : "?" ) + s.data; + + // #9682: remove data so that it's not used in an eventual retry + delete s.data; + } + + // Add or update anti-cache param if needed + if ( s.cache === false ) { + cacheURL = cacheURL.replace( rantiCache, "$1" ); + uncached = ( rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + ( nonce.guid++ ) + + uncached; + } + + // Put hash and anti-cache on the URL that will be requested (gh-1732) + s.url = cacheURL + uncached; + + // Change '%20' to '+' if this is encoded form body content (gh-2658) + } else if ( s.data && s.processData && + ( s.contentType || "" ).indexOf( "application/x-www-form-urlencoded" ) === 0 ) { + s.data = s.data.replace( r20, "+" ); + } + + // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. + if ( s.ifModified ) { + if ( jQuery.lastModified[ cacheURL ] ) { + jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ cacheURL ] ); + } + if ( jQuery.etag[ cacheURL ] ) { + jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ cacheURL ] ); + } + } + + // Set the correct header, if data is being sent + if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) { + jqXHR.setRequestHeader( "Content-Type", s.contentType ); + } + + // Set the Accepts header for the server, depending on the dataType + jqXHR.setRequestHeader( + "Accept", + s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[ 0 ] ] ? + s.accepts[ s.dataTypes[ 0 ] ] + + ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) : + s.accepts[ "*" ] + ); + + // Check for headers option + for ( i in s.headers ) { + jqXHR.setRequestHeader( i, s.headers[ i ] ); + } + + // Allow custom headers/mimetypes and early abort + if ( s.beforeSend && + ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || completed ) ) { + + // Abort if not done already and return + return jqXHR.abort(); + } + + // Aborting is no longer a cancellation + strAbort = "abort"; + + // Install callbacks on deferreds + completeDeferred.add( s.complete ); + jqXHR.done( s.success ); + jqXHR.fail( s.error ); + + // Get transport + transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR ); + + // If no transport, we auto-abort + if ( !transport ) { + done( -1, "No Transport" ); + } else { + jqXHR.readyState = 1; + + // Send global event + if ( fireGlobals ) { + globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] ); + } + + // If request was aborted inside ajaxSend, stop there + if ( completed ) { + return jqXHR; + } + + // Timeout + if ( s.async && s.timeout > 0 ) { + timeoutTimer = window.setTimeout( function() { + jqXHR.abort( "timeout" ); + }, s.timeout ); + } + + try { + completed = false; + transport.send( requestHeaders, done ); + } catch ( e ) { + + // Rethrow post-completion exceptions + if ( completed ) { + throw e; + } + + // Propagate others as results + done( -1, e ); + } + } + + // Callback for when everything is done + function done( status, nativeStatusText, responses, headers ) { + var isSuccess, success, error, response, modified, + statusText = nativeStatusText; + + // Ignore repeat invocations + if ( completed ) { + return; + } + + completed = true; + + // Clear timeout if it exists + if ( timeoutTimer ) { + window.clearTimeout( timeoutTimer ); + } + + // Dereference transport for early garbage collection + // (no matter how long the jqXHR object will be used) + transport = undefined; + + // Cache response headers + responseHeadersString = headers || ""; + + // Set readyState + jqXHR.readyState = status > 0 ? 4 : 0; + + // Determine if successful + isSuccess = status >= 200 && status < 300 || status === 304; + + // Get response data + if ( responses ) { + response = ajaxHandleResponses( s, jqXHR, responses ); + } + + // Use a noop converter for missing script but not if jsonp + if ( !isSuccess && + jQuery.inArray( "script", s.dataTypes ) > -1 && + jQuery.inArray( "json", s.dataTypes ) < 0 ) { + s.converters[ "text script" ] = function() {}; + } + + // Convert no matter what (that way responseXXX fields are always set) + response = ajaxConvert( s, response, jqXHR, isSuccess ); + + // If successful, handle type chaining + if ( isSuccess ) { + + // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. + if ( s.ifModified ) { + modified = jqXHR.getResponseHeader( "Last-Modified" ); + if ( modified ) { + jQuery.lastModified[ cacheURL ] = modified; + } + modified = jqXHR.getResponseHeader( "etag" ); + if ( modified ) { + jQuery.etag[ cacheURL ] = modified; + } + } + + // if no content + if ( status === 204 || s.type === "HEAD" ) { + statusText = "nocontent"; + + // if not modified + } else if ( status === 304 ) { + statusText = "notmodified"; + + // If we have data, let's convert it + } else { + statusText = response.state; + success = response.data; + error = response.error; + isSuccess = !error; + } + } else { + + // Extract error from statusText and normalize for non-aborts + error = statusText; + if ( status || !statusText ) { + statusText = "error"; + if ( status < 0 ) { + status = 0; + } + } + } + + // Set data for the fake xhr object + jqXHR.status = status; + jqXHR.statusText = ( nativeStatusText || statusText ) + ""; + + // Success/Error + if ( isSuccess ) { + deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] ); + } else { + deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] ); + } + + // Status-dependent callbacks + jqXHR.statusCode( statusCode ); + statusCode = undefined; + + if ( fireGlobals ) { + globalEventContext.trigger( isSuccess ? "ajaxSuccess" : "ajaxError", + [ jqXHR, s, isSuccess ? success : error ] ); + } + + // Complete + completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] ); + + if ( fireGlobals ) { + globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] ); + + // Handle the global AJAX counter + if ( !( --jQuery.active ) ) { + jQuery.event.trigger( "ajaxStop" ); + } + } + } + + return jqXHR; + }, + + getJSON: function( url, data, callback ) { + return jQuery.get( url, data, callback, "json" ); + }, + + getScript: function( url, callback ) { + return jQuery.get( url, undefined, callback, "script" ); + } +} ); + +jQuery.each( [ "get", "post" ], function( _i, method ) { + jQuery[ method ] = function( url, data, callback, type ) { + + // Shift arguments if data argument was omitted + if ( isFunction( data ) ) { + type = type || callback; + callback = data; + data = undefined; + } + + // The url can be an options object (which then must have .url) + return jQuery.ajax( jQuery.extend( { + url: url, + type: method, + dataType: type, + data: data, + success: callback + }, jQuery.isPlainObject( url ) && url ) ); + }; +} ); + +jQuery.ajaxPrefilter( function( s ) { + var i; + for ( i in s.headers ) { + if ( i.toLowerCase() === "content-type" ) { + s.contentType = s.headers[ i ] || ""; + } + } +} ); + + +jQuery._evalUrl = function( url, options, doc ) { + return jQuery.ajax( { + url: url, + + // Make this explicit, since user can override this through ajaxSetup (#11264) + type: "GET", + dataType: "script", + cache: true, + async: false, + global: false, + + // Only evaluate the response if it is successful (gh-4126) + // dataFilter is not invoked for failure responses, so using it instead + // of the default converter is kludgy but it works. + converters: { + "text script": function() {} + }, + dataFilter: function( response ) { + jQuery.globalEval( response, options, doc ); + } + } ); +}; + + +jQuery.fn.extend( { + wrapAll: function( html ) { + var wrap; + + if ( this[ 0 ] ) { + if ( isFunction( html ) ) { + html = html.call( this[ 0 ] ); + } + + // The elements to wrap the target around + wrap = jQuery( html, this[ 0 ].ownerDocument ).eq( 0 ).clone( true ); + + if ( this[ 0 ].parentNode ) { + wrap.insertBefore( this[ 0 ] ); + } + + wrap.map( function() { + var elem = this; + + while ( elem.firstElementChild ) { + elem = elem.firstElementChild; + } + + return elem; + } ).append( this ); + } + + return this; + }, + + wrapInner: function( html ) { + if ( isFunction( html ) ) { + return this.each( function( i ) { + jQuery( this ).wrapInner( html.call( this, i ) ); + } ); + } + + return this.each( function() { + var self = jQuery( this ), + contents = self.contents(); + + if ( contents.length ) { + contents.wrapAll( html ); + + } else { + self.append( html ); + } + } ); + }, + + wrap: function( html ) { + var htmlIsFunction = isFunction( html ); + + return this.each( function( i ) { + jQuery( this ).wrapAll( htmlIsFunction ? html.call( this, i ) : html ); + } ); + }, + + unwrap: function( selector ) { + this.parent( selector ).not( "body" ).each( function() { + jQuery( this ).replaceWith( this.childNodes ); + } ); + return this; + } +} ); + + +jQuery.expr.pseudos.hidden = function( elem ) { + return !jQuery.expr.pseudos.visible( elem ); +}; +jQuery.expr.pseudos.visible = function( elem ) { + return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length ); +}; + + + + +jQuery.ajaxSettings.xhr = function() { + try { + return new window.XMLHttpRequest(); + } catch ( e ) {} +}; + +var xhrSuccessStatus = { + + // File protocol always yields status code 0, assume 200 + 0: 200, + + // Support: IE <=9 only + // #1450: sometimes IE returns 1223 when it should be 204 + 1223: 204 + }, + xhrSupported = jQuery.ajaxSettings.xhr(); + +support.cors = !!xhrSupported && ( "withCredentials" in xhrSupported ); +support.ajax = xhrSupported = !!xhrSupported; + +jQuery.ajaxTransport( function( options ) { + var callback, errorCallback; + + // Cross domain only allowed if supported through XMLHttpRequest + if ( support.cors || xhrSupported && !options.crossDomain ) { + return { + send: function( headers, complete ) { + var i, + xhr = options.xhr(); + + xhr.open( + options.type, + options.url, + options.async, + options.username, + options.password + ); + + // Apply custom fields if provided + if ( options.xhrFields ) { + for ( i in options.xhrFields ) { + xhr[ i ] = options.xhrFields[ i ]; + } + } + + // Override mime type if needed + if ( options.mimeType && xhr.overrideMimeType ) { + xhr.overrideMimeType( options.mimeType ); + } + + // X-Requested-With header + // For cross-domain requests, seeing as conditions for a preflight are + // akin to a jigsaw puzzle, we simply never set it to be sure. + // (it can always be set on a per-request basis or even using ajaxSetup) + // For same-domain requests, won't change header if already provided. + if ( !options.crossDomain && !headers[ "X-Requested-With" ] ) { + headers[ "X-Requested-With" ] = "XMLHttpRequest"; + } + + // Set headers + for ( i in headers ) { + xhr.setRequestHeader( i, headers[ i ] ); + } + + // Callback + callback = function( type ) { + return function() { + if ( callback ) { + callback = errorCallback = xhr.onload = + xhr.onerror = xhr.onabort = xhr.ontimeout = + xhr.onreadystatechange = null; + + if ( type === "abort" ) { + xhr.abort(); + } else if ( type === "error" ) { + + // Support: IE <=9 only + // On a manual native abort, IE9 throws + // errors on any property access that is not readyState + if ( typeof xhr.status !== "number" ) { + complete( 0, "error" ); + } else { + complete( + + // File: protocol always yields status 0; see #8605, #14207 + xhr.status, + xhr.statusText + ); + } + } else { + complete( + xhrSuccessStatus[ xhr.status ] || xhr.status, + xhr.statusText, + + // Support: IE <=9 only + // IE9 has no XHR2 but throws on binary (trac-11426) + // For XHR2 non-text, let the caller handle it (gh-2498) + ( xhr.responseType || "text" ) !== "text" || + typeof xhr.responseText !== "string" ? + { binary: xhr.response } : + { text: xhr.responseText }, + xhr.getAllResponseHeaders() + ); + } + } + }; + }; + + // Listen to events + xhr.onload = callback(); + errorCallback = xhr.onerror = xhr.ontimeout = callback( "error" ); + + // Support: IE 9 only + // Use onreadystatechange to replace onabort + // to handle uncaught aborts + if ( xhr.onabort !== undefined ) { + xhr.onabort = errorCallback; + } else { + xhr.onreadystatechange = function() { + + // Check readyState before timeout as it changes + if ( xhr.readyState === 4 ) { + + // Allow onerror to be called first, + // but that will not handle a native abort + // Also, save errorCallback to a variable + // as xhr.onerror cannot be accessed + window.setTimeout( function() { + if ( callback ) { + errorCallback(); + } + } ); + } + }; + } + + // Create the abort callback + callback = callback( "abort" ); + + try { + + // Do send the request (this may raise an exception) + xhr.send( options.hasContent && options.data || null ); + } catch ( e ) { + + // #14683: Only rethrow if this hasn't been notified as an error yet + if ( callback ) { + throw e; + } + } + }, + + abort: function() { + if ( callback ) { + callback(); + } + } + }; + } +} ); + + + + +// Prevent auto-execution of scripts when no explicit dataType was provided (See gh-2432) +jQuery.ajaxPrefilter( function( s ) { + if ( s.crossDomain ) { + s.contents.script = false; + } +} ); + +// Install script dataType +jQuery.ajaxSetup( { + accepts: { + script: "text/javascript, application/javascript, " + + "application/ecmascript, application/x-ecmascript" + }, + contents: { + script: /\b(?:java|ecma)script\b/ + }, + converters: { + "text script": function( text ) { + jQuery.globalEval( text ); + return text; + } + } +} ); + +// Handle cache's special case and crossDomain +jQuery.ajaxPrefilter( "script", function( s ) { + if ( s.cache === undefined ) { + s.cache = false; + } + if ( s.crossDomain ) { + s.type = "GET"; + } +} ); + +// Bind script tag hack transport +jQuery.ajaxTransport( "script", function( s ) { + + // This transport only deals with cross domain or forced-by-attrs requests + if ( s.crossDomain || s.scriptAttrs ) { + var script, callback; + return { + send: function( _, complete ) { + script = jQuery( " + <% end %> + + + + + + NAV + <%= image_tag('navbar.png') %> + + +
      + <%= image_tag "logo.png", class: 'logo' %> + <% if language_tabs.any? %> +
      + <% language_tabs.each do |lang| %> + <% if lang.is_a? Hash %> + <%= lang.values.first %> + <% else %> + <%= lang %> + <% end %> + <% end %> +
      + <% end %> + <% if current_page.data.search %> + +
        + <% end %> +
          + <% toc_data(page_content).each do |h1| %> +
        • + <%= h1[:content] %> + <% if h1[:children].length > 0 %> + + <% end %> +
        • + <% end %> +
        + <% if current_page.data.toc_footers %> + + <% end %> +
        +
        +
        +
        + <%= page_content %> +
        +
        + <% if language_tabs.any? %> +
        + <% language_tabs.each do |lang| %> + <% if lang.is_a? Hash %> + <%= lang.values.first %> + <% else %> + <%= lang %> + <% end %> + <% end %> +
        + <% end %> +
        +
        + + diff --git a/docs/api/source/stylesheets/_icon-font.scss b/docs/api/source/stylesheets/_icon-font.scss new file mode 100644 index 00000000..b5994839 --- /dev/null +++ b/docs/api/source/stylesheets/_icon-font.scss @@ -0,0 +1,38 @@ +@font-face { + font-family: 'slate'; + src:font-url('slate.eot?-syv14m'); + src:font-url('slate.eot?#iefix-syv14m') format('embedded-opentype'), + font-url('slate.woff2?-syv14m') format('woff2'), + font-url('slate.woff?-syv14m') format('woff'), + font-url('slate.ttf?-syv14m') format('truetype'), + font-url('slate.svg?-syv14m#slate') format('svg'); + font-weight: normal; + font-style: normal; +} + +%icon { + font-family: 'slate'; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; +} + +%icon-exclamation-sign { + @extend %icon; + content: "\e600"; +} +%icon-info-sign { + @extend %icon; + content: "\e602"; +} +%icon-ok-sign { + @extend %icon; + content: "\e606"; +} +%icon-search { + @extend %icon; + content: "\e607"; +} diff --git a/docs/api/source/stylesheets/_normalize.scss b/docs/api/source/stylesheets/_normalize.scss new file mode 100644 index 00000000..46f646a5 --- /dev/null +++ b/docs/api/source/stylesheets/_normalize.scss @@ -0,0 +1,427 @@ +/*! normalize.css v3.0.2 | MIT License | git.io/normalize */ + +/** + * 1. Set default font family to sans-serif. + * 2. Prevent iOS text size adjust after orientation change, without disabling + * user zoom. + */ + +html { + font-family: sans-serif; /* 1 */ + -ms-text-size-adjust: 100%; /* 2 */ + -webkit-text-size-adjust: 100%; /* 2 */ +} + +/** + * Remove default margin. + */ + +body { + margin: 0; +} + +/* HTML5 display definitions + ========================================================================== */ + +/** + * Correct `block` display not defined for any HTML5 element in IE 8/9. + * Correct `block` display not defined for `details` or `summary` in IE 10/11 + * and Firefox. + * Correct `block` display not defined for `main` in IE 11. + */ + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +menu, +nav, +section, +summary { + display: block; +} + +/** + * 1. Correct `inline-block` display not defined in IE 8/9. + * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. + */ + +audio, +canvas, +progress, +video { + display: inline-block; /* 1 */ + vertical-align: baseline; /* 2 */ +} + +/** + * Prevent modern browsers from displaying `audio` without controls. + * Remove excess height in iOS 5 devices. + */ + +audio:not([controls]) { + display: none; + height: 0; +} + +/** + * Address `[hidden]` styling not present in IE 8/9/10. + * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. + */ + +[hidden], +template { + display: none; +} + +/* Links + ========================================================================== */ + +/** + * Remove the gray background color from active links in IE 10. + */ + +a { + background-color: transparent; +} + +/** + * Improve readability when focused and also mouse hovered in all browsers. + */ + +a:active, +a:hover { + outline: 0; +} + +/* Text-level semantics + ========================================================================== */ + +/** + * Address styling not present in IE 8/9/10/11, Safari, and Chrome. + */ + +abbr[title] { + border-bottom: 1px dotted; +} + +/** + * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. + */ + +b, +strong { + font-weight: bold; +} + +/** + * Address styling not present in Safari and Chrome. + */ + +dfn { + font-style: italic; +} + +/** + * Address variable `h1` font-size and margin within `section` and `article` + * contexts in Firefox 4+, Safari, and Chrome. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +/** + * Address styling not present in IE 8/9. + */ + +mark { + background: #ff0; + color: #000; +} + +/** + * Address inconsistent and variable font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` affecting `line-height` in all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +/* Embedded content + ========================================================================== */ + +/** + * Remove border when inside `a` element in IE 8/9/10. + */ + +img { + border: 0; +} + +/** + * Correct overflow not hidden in IE 9/10/11. + */ + +svg:not(:root) { + overflow: hidden; +} + +/* Grouping content + ========================================================================== */ + +/** + * Address margin not present in IE 8/9 and Safari. + */ + +figure { + margin: 1em 40px; +} + +/** + * Address differences between Firefox and other browsers. + */ + +hr { + -moz-box-sizing: content-box; + box-sizing: content-box; + height: 0; +} + +/** + * Contain overflow in all browsers. + */ + +pre { + overflow: auto; +} + +/** + * Address odd `em`-unit font size rendering in all browsers. + */ + +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} + +/* Forms + ========================================================================== */ + +/** + * Known limitation: by default, Chrome and Safari on OS X allow very limited + * styling of `select`, unless a `border` property is set. + */ + +/** + * 1. Correct color not being inherited. + * Known issue: affects color of disabled elements. + * 2. Correct font properties not being inherited. + * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. + */ + +button, +input, +optgroup, +select, +textarea { + color: inherit; /* 1 */ + font: inherit; /* 2 */ + margin: 0; /* 3 */ +} + +/** + * Address `overflow` set to `hidden` in IE 8/9/10/11. + */ + +button { + overflow: visible; +} + +/** + * Address inconsistent `text-transform` inheritance for `button` and `select`. + * All other form control elements do not inherit `text-transform` values. + * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. + * Correct `select` style inheritance in Firefox. + */ + +button, +select { + text-transform: none; +} + +/** + * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` + * and `video` controls. + * 2. Correct inability to style clickable `input` types in iOS. + * 3. Improve usability and consistency of cursor style between image-type + * `input` and others. + */ + +button, +html input[type="button"], /* 1 */ +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; /* 2 */ + cursor: pointer; /* 3 */ +} + +/** + * Re-set default cursor for disabled elements. + */ + +button[disabled], +html input[disabled] { + cursor: default; +} + +/** + * Remove inner padding and border in Firefox 4+. + */ + +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} + +/** + * Address Firefox 4+ setting `line-height` on `input` using `!important` in + * the UA stylesheet. + */ + +input { + line-height: normal; +} + +/** + * It's recommended that you don't attempt to style these elements. + * Firefox's implementation doesn't respect box-sizing, padding, or width. + * + * 1. Address box sizing set to `content-box` in IE 8/9/10. + * 2. Remove excess padding in IE 8/9/10. + */ + +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Fix the cursor style for Chrome's increment/decrement buttons. For certain + * `font-size` values of the `input`, it causes the cursor style of the + * decrement button to change from `default` to `text`. + */ + +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Address `appearance` set to `searchfield` in Safari and Chrome. + * 2. Address `box-sizing` set to `border-box` in Safari and Chrome + * (include `-moz` to future-proof). + */ + +input[type="search"] { + -webkit-appearance: textfield; /* 1 */ + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; /* 2 */ + box-sizing: content-box; +} + +/** + * Remove inner padding and search cancel button in Safari and Chrome on OS X. + * Safari (but not Chrome) clips the cancel button when the search input has + * padding (and `textfield` appearance). + */ + +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * Define consistent border, margin, and padding. + */ + +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +/** + * 1. Correct `color` not being inherited in IE 8/9/10/11. + * 2. Remove padding so people aren't caught out if they zero out fieldsets. + */ + +legend { + border: 0; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Remove default vertical scrollbar in IE 8/9/10/11. + */ + +textarea { + overflow: auto; +} + +/** + * Don't inherit the `font-weight` (applied by a rule above). + * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. + */ + +optgroup { + font-weight: bold; +} + +/* Tables + ========================================================================== */ + +/** + * Remove most spacing between table cells. + */ + +table { + border-collapse: collapse; + border-spacing: 0; +} + +td, +th { + padding: 0; +} diff --git a/docs/api/source/stylesheets/_rtl.scss b/docs/api/source/stylesheets/_rtl.scss new file mode 100644 index 00000000..720719a0 --- /dev/null +++ b/docs/api/source/stylesheets/_rtl.scss @@ -0,0 +1,140 @@ +//////////////////////////////////////////////////////////////////////////////// +// RTL Styles Variables +//////////////////////////////////////////////////////////////////////////////// + +$default: auto; + +//////////////////////////////////////////////////////////////////////////////// +// TABLE OF CONTENTS +//////////////////////////////////////////////////////////////////////////////// + +#toc>ul>li>a>span { + float: left; +} + +.toc-wrapper { + transition: right 0.3s ease-in-out !important; + left: $default !important; + #{right}: 0; +} + +.toc-h2 { + padding-#{right}: $nav-padding + $nav-indent; +} + +#nav-button { + #{right}: 0; + transition: right 0.3s ease-in-out; + &.open { + right: $nav-width + } +} + +//////////////////////////////////////////////////////////////////////////////// +// PAGE LAYOUT AND CODE SAMPLE BACKGROUND +//////////////////////////////////////////////////////////////////////////////// +.page-wrapper { + margin-#{left}: $default !important; + margin-#{right}: $nav-width; + .dark-box { + #{right}: $default; + #{left}: 0; + } +} + +.lang-selector { + width: $default !important; + a { + float: right; + } +} + +//////////////////////////////////////////////////////////////////////////////// +// CODE SAMPLE STYLES +//////////////////////////////////////////////////////////////////////////////// +.content { + &>h1, + &>h2, + &>h3, + &>h4, + &>h5, + &>h6, + &>p, + &>table, + &>ul, + &>ol, + &>aside, + &>dl { + margin-#{left}: $examples-width; + margin-#{right}: $default !important; + } + &>ul, + &>ol { + padding-#{right}: $main-padding + 15px; + } + table { + th, + td { + text-align: right; + } + } + dd { + margin-#{right}: 15px; + } + aside { + aside:before { + padding-#{left}: 0.5em; + } + .search-highlight { + background: linear-gradient(to top right, #F7E633 0%, #F1D32F 100%); + } + } + pre, + blockquote { + float: left !important; + clear: left !important; + } +} + +//////////////////////////////////////////////////////////////////////////////// +// TYPOGRAPHY +//////////////////////////////////////////////////////////////////////////////// +h1, +h2, +h3, +h4, +h5, +h6, +p, +aside { + text-align: right; + direction: rtl; +} + +.toc-wrapper { + text-align: right; + direction: rtl; + font-weight: 100 !important; +} + + +//////////////////////////////////////////////////////////////////////////////// +// RESPONSIVE DESIGN +//////////////////////////////////////////////////////////////////////////////// +@media (max-width: $tablet-width) { + .toc-wrapper { + #{right}: -$nav-width; + &.open { + #{right}: 0; + } + } + .page-wrapper { + margin-#{right}: 0; + } +} + +@media (max-width: $phone-width) { + %left-col { + margin-#{left}: 0; + } +} diff --git a/docs/api/source/stylesheets/_variables.scss b/docs/api/source/stylesheets/_variables.scss new file mode 100644 index 00000000..76932618 --- /dev/null +++ b/docs/api/source/stylesheets/_variables.scss @@ -0,0 +1,103 @@ +/* +Copyright 2008-2013 Concur Technologies, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. You may obtain +a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +License for the specific language governing permissions and limitations +under the License. +*/ + + +//////////////////////////////////////////////////////////////////////////////// +// CUSTOMIZE SLATE +//////////////////////////////////////////////////////////////////////////////// +// Use these settings to help adjust the appearance of Slate + + +// BACKGROUND COLORS +//////////////////// +$nav-bg: #2E3336 !default; +$examples-bg: #2E3336 !default; +$code-bg: #1E2224 !default; +$code-annotation-bg: #191D1F !default; +$nav-subitem-bg: #1E2224 !default; +$nav-active-bg: #0F75D4 !default; +$nav-active-parent-bg: #1E2224 !default; // parent links of the current section +$lang-select-border: #000 !default; +$lang-select-bg: #1E2224 !default; +$lang-select-active-bg: $examples-bg !default; // feel free to change this to blue or something +$lang-select-pressed-bg: #111 !default; // color of language tab bg when mouse is pressed +$main-bg: #F3F7F9 !default; +$aside-notice-bg: #8fbcd4 !default; +$aside-warning-bg: #c97a7e !default; +$aside-success-bg: #6ac174 !default; +$search-notice-bg: #c97a7e !default; + + +// TEXT COLORS +//////////////////// +$main-text: #333 !default; // main content text color +$nav-text: #fff !default; +$nav-active-text: #fff !default; +$nav-active-parent-text: #fff !default; // parent links of the current section +$lang-select-text: #fff !default; // color of unselected language tab text +$lang-select-active-text: #fff !default; // color of selected language tab text +$lang-select-pressed-text: #fff !default; // color of language tab text when mouse is pressed + + +// SIZES +//////////////////// +$nav-width: 230px !default; // width of the navbar +$examples-width: 50% !default; // portion of the screen taken up by code examples +$logo-margin: 0px !default; // margin below logo +$main-padding: 28px !default; // padding to left and right of content & examples +$nav-padding: 15px !default; // padding to left and right of navbar +$nav-v-padding: 10px !default; // padding used vertically around search boxes and results +$nav-indent: 10px !default; // extra padding for ToC subitems +$code-annotation-padding: 13px !default; // padding inside code annotations +$h1-margin-bottom: 21px !default; // padding under the largest header tags +$tablet-width: 930px !default; // min width before reverting to tablet size +$phone-width: $tablet-width - $nav-width !default; // min width before reverting to mobile size + + +// FONTS +//////////////////// +%default-font { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + font-size: 14px; +} + +%header-font { + @extend %default-font; + font-weight: bold; +} + +%code-font { + font-family: Consolas, Menlo, Monaco, "Lucida Console", "Liberation Mono", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Courier New", monospace, serif; + font-size: 12px; + line-height: 1.5; +} + + +// OTHER +//////////////////// +$nav-footer-border-color: #666 !default; +$search-box-border-color: #666 !default; + + +//////////////////////////////////////////////////////////////////////////////// +// INTERNAL +//////////////////////////////////////////////////////////////////////////////// +// These settings are probably best left alone. + +%break-words { + word-break: break-all; + hyphens: auto; +} diff --git a/docs/api/source/stylesheets/print.css.scss b/docs/api/source/stylesheets/print.css.scss new file mode 100644 index 00000000..aea88c30 --- /dev/null +++ b/docs/api/source/stylesheets/print.css.scss @@ -0,0 +1,153 @@ +@charset "utf-8"; +@import 'normalize'; +@import 'variables'; +@import 'icon-font'; + +/* +Copyright 2008-2013 Concur Technologies, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. You may obtain +a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +License for the specific language governing permissions and limitations +under the License. +*/ + +$print-color: #999; +$print-color-light: #ccc; +$print-font-size: 12px; + +body { + @extend %default-font; +} + +.tocify, .toc-footer, .lang-selector, .search, #nav-button { + display: none; +} + +.tocify-wrapper>img { + margin: 0 auto; + display: block; +} + +.content { + font-size: 12px; + + pre, code { + @extend %code-font; + @extend %break-words; + border: 1px solid $print-color; + border-radius: 5px; + font-size: 0.8em; + } + + pre { + code { + border: 0; + } + } + + pre { + padding: 1.3em; + } + + code { + padding: 0.2em; + } + + table { + border: 1px solid $print-color; + tr { + border-bottom: 1px solid $print-color; + } + td,th { + padding: 0.7em; + } + } + + p { + line-height: 1.5; + } + + a { + text-decoration: none; + color: #000; + } + + h1 { + @extend %header-font; + font-size: 2.5em; + padding-top: 0.5em; + padding-bottom: 0.5em; + margin-top: 1em; + margin-bottom: $h1-margin-bottom; + border: 2px solid $print-color-light; + border-width: 2px 0; + text-align: center; + } + + h2 { + @extend %header-font; + font-size: 1.8em; + margin-top: 2em; + border-top: 2px solid $print-color-light; + padding-top: 0.8em; + } + + h1+h2, h1+div+h2 { + border-top: none; + padding-top: 0; + margin-top: 0; + } + + h3, h4 { + @extend %header-font; + font-size: 0.8em; + margin-top: 1.5em; + margin-bottom: 0.8em; + text-transform: uppercase; + } + + h5, h6 { + text-transform: uppercase; + } + + aside { + padding: 1em; + border: 1px solid $print-color-light; + border-radius: 5px; + margin-top: 1.5em; + margin-bottom: 1.5em; + line-height: 1.6; + } + + aside:before { + vertical-align: middle; + padding-right: 0.5em; + font-size: 14px; + } + + aside.notice:before { + @extend %icon-info-sign; + } + + aside.warning:before { + @extend %icon-exclamation-sign; + } + + aside.success:before { + @extend %icon-ok-sign; + } +} + +.copy-clipboard { + @media print { + display: none + } +} diff --git a/docs/api/source/stylesheets/screen.css.scss b/docs/api/source/stylesheets/screen.css.scss new file mode 100644 index 00000000..70e3527d --- /dev/null +++ b/docs/api/source/stylesheets/screen.css.scss @@ -0,0 +1,633 @@ +@charset "utf-8"; +@import 'normalize'; +@import 'variables'; +@import 'icon-font'; +// @import 'rtl'; // uncomment to switch to RTL format + +/* +Copyright 2008-2013 Concur Technologies, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. You may obtain +a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +License for the specific language governing permissions and limitations +under the License. +*/ + +//////////////////////////////////////////////////////////////////////////////// +// GENERAL STUFF +//////////////////////////////////////////////////////////////////////////////// + +html, body { + color: $main-text; + padding: 0; + margin: 0; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + @extend %default-font; + background-color: $main-bg; + height: 100%; + -webkit-text-size-adjust: none; /* Never autoresize text */ +} + +//////////////////////////////////////////////////////////////////////////////// +// TABLE OF CONTENTS +//////////////////////////////////////////////////////////////////////////////// + +#toc > ul > li > a > span { + float: right; + background-color: #2484FF; + border-radius: 40px; + width: 20px; +} + +.toc-wrapper { + transition: left 0.3s ease-in-out; + + overflow-y: auto; + overflow-x: hidden; + position: fixed; + z-index: 30; + top: 0; + left: 0; + bottom: 0; + width: $nav-width; + background-color: $nav-bg; + font-size: 13px; + font-weight: bold; + + // language selector for mobile devices + .lang-selector { + display: none; + a { + padding-top: 0.5em; + padding-bottom: 0.5em; + } + } + + // This is the logo at the top of the ToC + .logo { + display: block; + max-width: 100%; + margin-bottom: $logo-margin; + } + + &>.search { + position: relative; + + input { + background: $nav-bg; + border-width: 0 0 1px 0; + border-color: $search-box-border-color; + padding: 6px 0 6px 20px; + box-sizing: border-box; + margin: $nav-v-padding $nav-padding; + width: $nav-width - ($nav-padding*2); + outline: none; + color: $nav-text; + border-radius: 0; /* ios has a default border radius */ + } + + &:before { + position: absolute; + top: 17px; + left: $nav-padding; + color: $nav-text; + @extend %icon-search; + } + } + + .search-results { + margin-top: 0; + box-sizing: border-box; + height: 0; + overflow-y: auto; + overflow-x: hidden; + transition-property: height, margin; + transition-duration: 180ms; + transition-timing-function: ease-in-out; + background: $nav-subitem-bg; + &.visible { + height: 30%; + margin-bottom: 1em; + } + + li { + margin: 1em $nav-padding; + line-height: 1; + } + + a { + color: $nav-text; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + } + + + // The Table of Contents is composed of multiple nested + // unordered lists. These styles remove the default + // styling of an unordered list because it is ugly. + ul, li { + list-style: none; + margin: 0; + padding: 0; + line-height: 28px; + } + + li { + color: $nav-text; + transition-property: background; + transition-timing-function: linear; + transition-duration: 200ms; + } + + // This is the currently selected ToC entry + .toc-link.active { + background-color: $nav-active-bg; + color: $nav-active-text; + } + + // this is parent links of the currently selected ToC entry + .toc-link.active-parent { + background-color: $nav-active-parent-bg; + color: $nav-active-parent-text; + } + + .toc-list-h2 { + display: none; + background-color: $nav-subitem-bg; + font-weight: 500; + } + + .toc-h2 { + padding-left: $nav-padding + $nav-indent; + font-size: 12px; + } + + .toc-footer { + padding: 1em 0; + margin-top: 1em; + border-top: 1px dashed $nav-footer-border-color; + + li,a { + color: $nav-text; + text-decoration: none; + } + + a:hover { + text-decoration: underline; + } + + li { + font-size: 0.8em; + line-height: 1.7; + text-decoration: none; + } + } +} + +.toc-link, .toc-footer li { + padding: 0 $nav-padding 0 $nav-padding; + display: block; + overflow-x: hidden; + white-space: nowrap; + text-overflow: ellipsis; + text-decoration: none; + color: $nav-text; + transition-property: background; + transition-timing-function: linear; + transition-duration: 130ms; +} + +// button to show navigation on mobile devices +#nav-button { + span { + display: block; + $side-pad: $main-padding / 2 - 8px; + padding: $side-pad $side-pad $side-pad; + background-color: rgba($main-bg, 0.7); + transform-origin: 0 0; + transform: rotate(-90deg) translate(-100%, 0); + border-radius: 0 0 0 5px; + } + padding: 0 1.5em 5em 0; // increase touch size area + display: none; + position: fixed; + top: 0; + left: 0; + z-index: 100; + color: #000; + text-decoration: none; + font-weight: bold; + opacity: 0.7; + line-height: 16px; + img { + height: 16px; + vertical-align: bottom; + } + + transition: left 0.3s ease-in-out; + + &:hover { opacity: 1; } + &.open {left: $nav-width} +} + + +//////////////////////////////////////////////////////////////////////////////// +// PAGE LAYOUT AND CODE SAMPLE BACKGROUND +//////////////////////////////////////////////////////////////////////////////// + +.page-wrapper { + margin-left: $nav-width; + position: relative; + z-index: 10; + background-color: $main-bg; + min-height: 100%; + + padding-bottom: 1px; // prevent margin overflow + + // The dark box is what gives the code samples their dark background. + // It sits essentially under the actual content block, which has a + // transparent background. + // I know, it's hackish, but it's the simplist way to make the left + // half of the content always this background color. + .dark-box { + width: $examples-width; + background-color: $examples-bg; + position: absolute; + right: 0; + top: 0; + bottom: 0; + } + + .lang-selector { + position: fixed; + z-index: 50; + border-bottom: 5px solid $lang-select-active-bg; + } +} + +.lang-selector { + display: flex; + background-color: $lang-select-bg; + width: 100%; + font-weight: bold; + overflow-x: auto; + a { + display: inline; + color: $lang-select-text; + text-decoration: none; + padding: 0 10px; + line-height: 30px; + outline: 0; + + &:active, &:focus { + background-color: $lang-select-pressed-bg; + color: $lang-select-pressed-text; + } + + &.active { + background-color: $lang-select-active-bg; + color: $lang-select-active-text; + } + } + + &:after { + content: ''; + clear: both; + display: block; + } +} + +//////////////////////////////////////////////////////////////////////////////// +// CONTENT STYLES +//////////////////////////////////////////////////////////////////////////////// +// This is all the stuff with the light background in the left half of the page + +.content { + // fixes webkit rendering bug for some: see #538 + -webkit-transform: translateZ(0); + // to place content above the dark box + position: relative; + z-index: 30; + + &:after { + content: ''; + display: block; + clear: both; + } + + &>h1, &>h2, &>h3, &>h4, &>h5, &>h6, &>p, &>table, &>ul, &>ol, &>aside, &>dl { + margin-right: $examples-width; + padding: 0 $main-padding; + box-sizing: border-box; + display: block; + + @extend %left-col; + } + + &>ul, &>ol { + padding-left: $main-padding + 15px; + } + + // the div is the tocify hidden div for placeholding stuff + &>h1, &>h2, &>div { + clear:both; + } + + h1 { + @extend %header-font; + font-size: 25px; + padding-top: 0.5em; + padding-bottom: 0.5em; + margin-bottom: $h1-margin-bottom; + margin-top: 2em; + border-top: 1px solid #ccc; + border-bottom: 1px solid #ccc; + background-color: #fdfdfd; + } + + h1:first-child, div:first-child + h1 { + border-top-width: 0; + margin-top: 0; + } + + h2 { + @extend %header-font; + font-size: 19px; + margin-top: 4em; + margin-bottom: 0; + border-top: 1px solid #ccc; + padding-top: 1.2em; + padding-bottom: 1.2em; + background-image: linear-gradient(to bottom, rgba(#fff, 0.2), rgba(#fff, 0)); + } + + // h2s right after h1s should bump right up + // against the h1s. + h1 + h2, h1 + div + h2 { + margin-top: $h1-margin-bottom * -1; + border-top: none; + } + + h3, h4, h5, h6 { + @extend %header-font; + font-size: 15px; + margin-top: 2.5em; + margin-bottom: 0.8em; + } + + h4, h5, h6 { + font-size: 10px; + } + + hr { + margin: 2em 0; + border-top: 2px solid $examples-bg; + border-bottom: 2px solid $main-bg; + } + + table { + margin-bottom: 1em; + overflow: auto; + th,td { + text-align: left; + vertical-align: top; + line-height: 1.6; + code { + white-space: nowrap; + } + } + + th { + padding: 5px 10px; + border-bottom: 1px solid #ccc; + vertical-align: bottom; + } + + td { + padding: 10px; + } + + tr:last-child { + border-bottom: 1px solid #ccc; + } + + tr:nth-child(odd)>td { + background-color: lighten($main-bg,4.2%); + } + + tr:nth-child(even)>td { + background-color: lighten($main-bg,2.4%); + } + } + + dt { + font-weight: bold; + } + + dd { + margin-left: 15px; + } + + p, li, dt, dd { + line-height: 1.6; + margin-top: 0; + } + + img { + max-width: 100%; + } + + code { + background-color: rgba(0,0,0,0.05); + padding: 3px; + border-radius: 3px; + @extend %break-words; + @extend %code-font; + } + + pre>code { + background-color: transparent; + padding: 0; + } + + aside { + padding-top: 1em; + padding-bottom: 1em; + margin-top: 1.5em; + margin-bottom: 1.5em; + background: $aside-notice-bg; + line-height: 1.6; + + &.warning { + background-color: $aside-warning-bg; + } + + &.success { + background-color: $aside-success-bg; + } + } + + aside:before { + vertical-align: middle; + padding-right: 0.5em; + font-size: 14px; + } + + aside.notice:before { + @extend %icon-info-sign; + } + + aside.warning:before { + @extend %icon-exclamation-sign; + } + + aside.success:before { + @extend %icon-ok-sign; + } + + .search-highlight { + padding: 2px; + margin: -3px; + border-radius: 4px; + border: 1px solid #F7E633; + background: linear-gradient(to top left, #F7E633 0%, #F1D32F 100%); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// CODE SAMPLE STYLES +//////////////////////////////////////////////////////////////////////////////// +// This is all the stuff that appears in the right half of the page + +.content { + &>div.highlight { + clear:none; + } + + pre, blockquote { + background-color: $code-bg; + color: #fff; + + margin: 0; + width: $examples-width; + + float:right; + clear:right; + + box-sizing: border-box; + + @extend %right-col; + + &>p { margin: 0; } + + a { + color: #fff; + text-decoration: none; + border-bottom: dashed 1px #ccc; + } + } + + pre { + @extend %code-font; + padding-top: 2em; + padding-bottom: 2em; + padding: 2em $main-padding; + } + + blockquote { + &>p { + background-color: $code-annotation-bg; + padding: $code-annotation-padding 2em; + color: #eee; + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +// RESPONSIVE DESIGN +//////////////////////////////////////////////////////////////////////////////// +// These are the styles for phones and tablets +// There are also a couple styles disperesed + +@media (max-width: $tablet-width) { + .toc-wrapper { + left: -$nav-width; + + &.open { + left: 0; + } + } + + .page-wrapper { + margin-left: 0; + } + + #nav-button { + display: block; + } + + .toc-link { + padding-top: 0.3em; + padding-bottom: 0.3em; + } +} + +@media (max-width: $phone-width) { + .dark-box { + display: none; + } + + %left-col { + margin-right: 0; + } + + .toc-wrapper .lang-selector { + display: block; + } + + .page-wrapper .lang-selector { + display: none; + } + + %right-col { + width: auto; + float: none; + } + + %right-col + %left-col { + margin-top: $main-padding; + } +} + +.highlight .c, .highlight .cm, .highlight .c1, .highlight .cs { + color: #909090; +} + +.highlight, .highlight .w { + background-color: $code-bg; +} + +.copy-clipboard { + float: right; + fill: #9DAAB6; + cursor: pointer; + opacity: 0.4; + height: 18px; + width: 18px; +} + +.copy-clipboard:hover { + opacity: 0.8; +} From a02b70e9a5cffcb98a8ef86561a4b66f5b06e365 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Fri, 3 Jun 2022 17:32:32 +0200 Subject: [PATCH 051/236] ci: added steps to build & deploy slate docs --- .woodpecker/.docs.yml | 32 ++++++++++++++++------------- docs/content/{api.md => api-old.md} | 0 2 files changed, 18 insertions(+), 14 deletions(-) rename docs/content/{api.md => api-old.md} (100%) diff --git a/.woodpecker/.docs.yml b/.woodpecker/.docs.yml index fc525e27..f4567596 100644 --- a/.woodpecker/.docs.yml +++ b/.woodpecker/.docs.yml @@ -9,7 +9,6 @@ pipeline: commands: - apk add git - make docs - - 'cd docs/public && tar czvf ../../docs.tar.gz *' api-docs: image: 'chewingbever/vlang:latest' @@ -17,25 +16,30 @@ pipeline: group: 'generate' commands: - make api-docs - - 'cd src/_docs && tar czvf ../../api-docs.tar.gz *' - deploy-docs: + slate-docs: + image: 'slatedocs/slate' + group: 'generate' + commands: + - cd docs/api + - bundle exec middleman build --clean + + archive: + image: 'alpine' + commands: + - cp -r docs/api/build docs/public/api + - 'cd docs/public && tar czvf ../../docs.tar.gz *' + - 'cd src/_docs && tar czvf ../../api-docs.tar.gz *' + when: + event: push + branch: dev + + deploy: image: 'curlimages/curl' - group: 'deploy' secrets: - 'site_api_key' commands: - 'curl -XPOST --fail -s -H "Authorization: Bearer $SITE_API_KEY" -T docs.tar.gz https://rustybever.be/api/deploy?dir=docs-vieter' - when: - event: push - branch: dev - - deploy-api-docs: - image: 'curlimages/curl' - group: 'deploy' - secrets: - - 'site_api_key' - commands: - 'curl -XPOST --fail -s -H "Authorization: Bearer $SITE_API_KEY" -T api-docs.tar.gz https://rustybever.be/api/deploy?dir=api-docs-vieter' when: event: push diff --git a/docs/content/api.md b/docs/content/api-old.md similarity index 100% rename from docs/content/api.md rename to docs/content/api-old.md From 4870edde515cc2484be16bb2b52aa7d4c411b8bb Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Fri, 3 Jun 2022 19:48:15 +0200 Subject: [PATCH 052/236] docs(slate): add Git Repositories API docs --- README.md | 34 ++++++ docs/api/source/images/logo.png | Bin 22317 -> 0 bytes docs/api/source/includes/_errors.md | 22 ---- docs/api/source/includes/_git.md | 148 ++++++++++++++++++++++++ docs/api/source/includes/_repository.md | 6 +- docs/api/source/index.html.md | 1 - docs/config.toml | 14 ++- docs/content/api-old.md | 89 -------------- 8 files changed, 195 insertions(+), 119 deletions(-) delete mode 100644 docs/api/source/images/logo.png delete mode 100644 docs/api/source/includes/_errors.md delete mode 100644 docs/content/api-old.md diff --git a/README.md b/README.md index 138282a2..34e6fd3a 100644 --- a/README.md +++ b/README.md @@ -65,3 +65,37 @@ If you wish to contribute to the project, please take note of the following: * Please follow the [Conventional Commits](https://www.conventionalcommits.org/) style for your commit messages. + +### Writing documentation + +The `docs` directory contains a Hugo site consisting of all user & +administrator documentation. `docs/api` on the other hand is a +[slate](https://github.com/slatedocs/slate) project describing the HTTP web +API. + +To modify the Hugo documentation, you'll need to install Hugo. Afterwards, you +can use the following commands inside the `docs` directory: + +```sh +# Build the documentation +hugo + +# Host an auto-refreshing web server with the documentation. Important to note +is that the files will be at `http://localhost:1313/docs/vieter` instead of +just `http://localhost:1313/docs/vieter` +hugo server +``` + +For the Slate docs, I personally just start a docker container: + +```sh +docker run \ + --rm \ + -p 4567:4567 \ + --name slate \ + -v $(pwd)/docs/api/source:/srv/slate/source slatedocs/slate serve +``` + +This'll make the slate docs available at http://localhost:4567. Sadly, this +server doesn't auto-refresh, so you'll have to manually refresh your browser +every time you make a change. diff --git a/docs/api/source/images/logo.png b/docs/api/source/images/logo.png deleted file mode 100644 index 68a478fae3611d878af18c575cf8218b818f41db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22317 zcmeFZWo%qs(JSi6L=X^=@bIwUZ?M&v zZNPsZoz-O|AgacRjvye!Amk*)H9a8DIuLv`=hi^}_8jqv6)`_*XrLt_m#|ma7;>~> z60F2sY|2HXs2Xt=4xSo(XIVhSu0bq0-%2%$C}hRqOXwn)V7LbJ0s@%GnF5iQh6Lo! zK`)u9oX+O^<7uf(`_4wyoDb{Ho#x&ff)73yy3`1;&|(n(ef{5Wft(o(Rct95!LU;^ z;K0y(>Zz@1zM$dPn6Ky%kpH>@qoe|`!#@K#he&A30W7gQA*Q-TDg(p9P9LDiApUht z<|4#g$0*-R0cn7NDZW&%aKMH5$kWMS$^HL;WH|(a9^um<3zfb~W5Yg$)E6qjARDWf z1o+bL5by#2x?Da&6`=p3ZHux&T5?FC33nk}0?~K_Lg@K8p#FIkx)GG-)yWXS;5qfl z7gUete{nB0dh`L06qLbSwb+L8M0+UL7X zf;G($bI@tNsZM+)`3ixCOrR*uh$Wz{DA@Wxt5%i_U~~owWj3T$u*R!Eg}q}(?|vIx zmrBxSQjWK+Uvu5PBlDhos$~lBzk&Rp1EFK)$n#|}MF(%riFKr6 z?LJ`v?;AJ{g`!S{P4Qy)t_ashA+KLW|2pWh7@le%B2CK>@MA8m;&4WRJ&pa`YFEK)l5}mDXvc0U%zY1zLBD#1#I{kZUoALn+>UPAB zkh&xqQ+hp>cwLl&KP!w)C}gV$b?wGHLYSKDJUA)dYpHhaUssu_THVIIBUV*J*DnBB zj4}p6|A5W{Jx881jj7t#F~)`uIXcWE{S+JM$J{UFB*b?o09?)N*%4^Rj{;qoeh4#O zYpC1M-xYa9wImD1`ZsC=0>CD+L<4q_6NXp&lHLK=(#s9!!V8BwryMp30J-5;2W zQjV-KX8jU)U+>(;dB)jqBx0vzr-v9=V1zn*?Vz5TMO z5PkZA{O@6Wsu5I!ODMr%3b{0H>Oq=_HY6g!)3cE-3FJ4;rW(=E9W(u86-6TK<+lU` zR6nMZ4hBNN|A-DS&hP6<7WxJYiH;6kQk1EDz#4z&O;8HI@+S&$??3_gWCPHBU(SA2 zd-7bm6&zHiexU@4cVwpxoo;l2Mvv#pReT=S+~*SByEeQpI^5|+KB7Yex={t7#4clq z#KA&}2QaJ*9KI?!t_?+3&>5K2brl0M^PD5D#Rn$jAT0{=leX~LlVdvqZGI0+8yXt+ z_`ly|cXM0MXS91<G9^$dF@5suLel7ORKBx9kF7l=!ghZ zH=&_yl^_sh^s!{T$Z{ye5^YslUjuA(o>;GqXdjlD(0=Y&mieiKQ0unwJ8u85e0#nd zXtG_={^mZ*m&;Y3fQ}-^_t%8+Vmh8f!SUqVuz~aFo2;PoDvS&f-(_qZoWhC0!A(aL zNO#ue{sr-B&$_071Qdd-sq4E18{lIZewV9L0fj39_e}oez{p9)HEf z#liA^StrhM@HqHjjBbGiWq7?eM`Icb5C2HxfD?A>fWyeh*hba4-r`iagezquw-h52 zO|te@R$evSydBQSp{}9vwj4Xh{XXtusHf+3bTc6;+G)4eIz!TL{-{AjNA#}x9U1;d zDg<^E+Xv}evd!RteXF#W`!m5}oTA!VwWS8@1*yhee>U)Y-j}Vbl}LGPK1+k%-`Zs2 zrVtkp))ICGqSHlQPV2Dro9ylcM0&D;Q@=IA;|Bac#{G?>=NE!X%rNKV?#jr-R6*_? zx2n?L*C%DAKAp?ISSg^qPoi4Kb!+l!TsHm*8Eh^1DSOV_EkblA)_=O+O?s7Q!D@)j$ z=E$WLb>;cbj2^G;vde3Jw1~i`O1`ZWHK^d8_kvAm=;dhe`tMDm6U4=w#}Ejb&<}HD zdE;i~)%%i!IwQiu!Zzp82tZ%)U#8UbL`z)zZ-;2JiO{5)!U!SE;pm9Qg&bg@&LqIV zYx^RTNA`t$b3SfrcD9$%IRqOGt#K6y7=dXdx(a~&u-$SOWZrqX7!A)st~9A&%4~{C z#OILIsuV>ku#=_GZdGL=xF)||P5(=N6+pa60Ph(|B zbXUUh@gV_;dV(&zs}7$8=*c=PmTjXC4-ZQMC@wXcb94W^_Td?4Y`-amM?{>wAP*H8Xg`_V5Q41KBE`GV%UnkTttW_ zRx(FpH(emNq+{&sDyp6{yQaON9y4d^`Kg7N2HpwB8*Df<_iPGrS7|RDnZ* zhPY=;@eIgeZ#dDo`g3hwO$Yg;0I6F2;pe|ggGC`oz#Xh#$;-=APn-A3fU8w!m0yhL z=KOBoaW}t?MXO9SS*}xUs#8RO{{>K{-b1Vm=BTd}o40vL8U&%Z&|<$oK&lJe$1gZE zJhV6;Fl2e|r&d}u-HRMlC9kfoX3bi~@H=e&c0sE%O6Une%QkMXnwzrkd@536xLI6W zl(iZ=bRS`E+NGkUy|Jr%Pa{(2Q+{Mb26KMkJXS#Wl-JKtNa*i0Vu%9@*pL2(lpf&h z={|#uItKIG0I>3$#`sigGH{=Bp3Ft?FeS{)%p3^&J?rJ63-m^udfYguA}=5AcRX9V z#Pv>{w z8?ERqlyp6oZS_%{&Sr{LPK6&<<3Ay)h*|+0zwMjji%_;C%PlQ#77O))OF;=qu%|=C zCPe-7fCQhXy<{3alJ=>M8e+q!;sfOrw%moYrVWuv zo*k+jCXEN>53V=u%7Odwd5%0eztO?`2TusL-q%kNq2j*S$J8xKF(subdM)15jpH<0rELS{t4Qk95005xa0{p*g zv%lF*#~O&PkV?(uic6(%_&pj@`Lf6YAbn1@oy)kcxuEi942im#TtT$VVS}#?=0pyg!`E z&oWX{FkCx79YqLgq4{E_E;823mjGtwtp)8WpNsa;QUN`h`0+c=#nZTnZoxB zn{<2p2^?-#Cg~t$rP;S`L$w)8ZTSwjA_owuEeSON4 zRz(dYSy^UaJT7_sEe=rs;tH-6e^7pbOw3iJn!+pJ+Oia88@VzGU%^_bKwWB;2Behb zH8K)2GhSH)25#98*u0;%Xn1-Q!mh&!^ab|F+xWDQiP3OF(>(XI3l+K=hcZY?Nc2P+ zhELlB&|*2h2++61_8S!xuIPua(6UR1N`-t%^G96+E7KEM!Y^3so4*FQE>Wvyp<=`n zoncXmY{mkn19BW6kQ)3FE>Dv)1>Mu=M1>mawf6f7?G+D7D}BK=-Li#TkZ{AKL*{g>VLyH9*ck@dIniuim6Kr~(yLY{;+0n|h z=JavxspU%-PLt5I2U$;?ueHGmdG~ND`&a8C$cyF>9W#{O@2 zx(EmewUE86Q^@gVdRli(O4h>4l}we(@xj=rc3T!Rwe*1rI6$&V90YJohr1I4?0CVc z*+)KX<2WslHp^o>9tZ>m;9kfpD$ZWrUs1|LjW(YGM>s67>gX0cdR)hXcr8z5@W=dG zWY<^=X9=bytSwB@NJZM+$yOGf-;a-v2})jyE_={KnUD)``t+AG~y(h=;LP4J`V$893&+AWssPd^Tt~!8U;91*AXti8!P{WHG3B3 z>88&){(i3)Yf&MEukZApWY*d{^?N33c6+kUAM`GC(#LG5b;%iPNk zD>m~HbVw)@7F(8GLLx!9r&e>1$QZ>P&TubXuE1m9b_siCv^9mEHEPUE}dU{IU=|2EIKN%_2nF{pnI~wqo0_zRDfDxjs zru{o6+Hu=gtazdZ;9ONt4#_3hNNz&~1BT2mT0}xax!0>`|5bTIRGg}FTTi9(jAKgy zYuE`dgvOI#p2)gi6FFMqJv<_x*pB8a^{E=nZo8iL)8SN`)G{0%94e@Sd$jPc$PmrJ zsl@?3pc@0VEEEe`2%RiAsHwobuiH{bb6K;}VZGf`D5&t$$Emw-`VOPk3rNuuIim>d5G*_LrI|pD8by2c~wO|k(mbO zLhDip_fjPBHG1ay>BFRX7LURx&V+-!6To84SA4MX*FIu2G&G>pV_J}3*khL6aklSy zUT*#gbKmfNfH;0X>N)a`zuUx9l9v&)&h!X{8wZ0<{O{bMxu7AzZ3KAkc<)a7?)hb9 zW%q^2M$1M91_m2xk3GamZ5$Sr(by|i`CmyrBuo^&CXJXrz#)J9o!$v!B9bpw9Sf`# zN%;*AmV3|%{&HjwMs0wY7_{us<4-7LXO!ML0aRiEm#hcYe^u+P=W!7_j>+S;O>6V% zHp*UJUcbF-(RJ*GJfcre3M|;H|JBX?hX>N2Z7PXg`_8UJ2h)qE$P|qPUZHG>?elKZ zE(Dp4sxlwG4ULTvqaPYE@$eM7_{5)X$WHNrd=G-J1I5fKHm?8+sfdB?-^I+S#N-DI zh9BbJ_cPlJEFQG6#?08y);m@-jg0K>d2M8p^N%XnzmZvJYHG5bd>}657^i$uQRlJ} z^zw`qML!G&hm=cSTNH;oJQzfmiitR;v@mJph~<-?qvZ)q^?>#pB3Ng@vKDXaoU6tL zp-33H0^GHCQyy!=P;9>_Hq7X=VWaw5<1-zFw5xS=S z5^l&#Iq&i9-qv5gex;8Dv8g>M41sGik3Z$8)^_bz{VvOfJhb3ehdEU7TK$qVW-lN&M@fv3VUoU(2aN(RP#k|5y(+5B*y1&$@FlpY zvhl@5^_U^n|1ya5HkHrun{rIsk~ZeArlI#>?8q7MKyk9aWK0PrP4k=GB2%Q*%PoblEa3BaX-Wbw-!^47EjB5(!QLLT z7-H(_(;t)wTK%RRjt>!lXJ!;J z<~#SRT7A0~>Ye))d(y>f5)Edy7{ z24DwD0!1laSah+LR8x_#r$BKiX53R%9*nAH|Gw>z09uwml+0Ur2}JpVvh-zC9PyTJ z=}W(x3(6!N^?#JEAP9OCUDIr`7lb7*;QFZft|y!>va~x{FaAHbh>=0d#vV$$F)T@$ zPQ(Ld#N61IwgP)$bpM$T7;16>*vP4;AAzYN!Q}rXMF02a|8WaUrosV=^wEBN5?~1Dh&6q-7)(?b zdLlUg>R6_pF!C)uZGHa8MQa?vpkSn)9>}D2)j`NGVj6dNZpdJ<@uAnG3H~fN9UbBJ zqYNaEDuK)g6nIZCJQ%^e>VYYilL(!L18^%H;@SXN92eHtcg~bv>zy->;%Peit5qCb(H0tteH^?jEtPnOovP0|r|)q?s%tu54LyHQGB0rX>><2^)5kb8 zMFy)Q47i$M4O|PybT288L{MDXTr{DIAF8h?C|tJj(2^QX9j&XG!0Jj=37;! zmk&>xrkv)`$u_1K?;sXV?9>0~|AuL@ty~J|iA}O5IKFiN?^bN4gEQyGFEU7|b7*WS zWv)~!ujUu%&2MT#jr9AJ`cUw*`+3xHw=(u&+OrQs>Isw`9Ya4>CRS%-Zhd7bR{>(L zWRqE7(y6EQEm*Te@L`s-Uxu@+Z^YgouF**nH&;`0!Kax~+}JuXzGD7>0M7qair~6e zr}Kr*TNV0U$`D!jt?b8J*n1pRUsT63>Ck=5lYAU9yC0PeN8zX{HvMvXN!1XMbf5eM z^HI#>95;7};sCPTPjm!$7Y?XaiL)! zO?BEeN@I@5$xF(jwfQ(RH_4HSV_erib=f_%dOC)&DI5XjBhX)`kAvWUkb(Wf1J1{n zbOB-k&=hiklc>{@I6J&yK%w78xpC7|V!(|U-++W@(gpeHYHP(JJU$;iy(@KHlCRmr zLw3YNL^=tKSc)`Vb*3ELmx{#Eh;X%=Krf~XV{E|tTQU^^M;?G@JZ^8uPla|qJa7%u zL|$dcDQW_rJFB|~+?IT*C`{jf!hS^y_^MI-gSAmHYzF4ds4hO5iI9;%ziuKiIxyF z_<#T^@vYw)5K!cllab#91c!873Oo{Mk2&qG&1;#A9fL>w8}g`3EnV!nCcm2%uqn}B z<#?o&6RsvCIP76ufeU08mcLB3@Frm=2oZ=Y7ygid5--;!&7N$Rdya*+#*!uB#`F*v zXL@w+IH46RiKx-ZPkp6x?iXj8Nzq2DG*V63eB=t;Z>1q7w;%if@3VpM1$p4%4K?3~ zoK|e)#_k@sw6vXHCr(jDUVeOvFLE@P>V;?Nlct*YdE^!DSN)eMj&x?PciE~U+I&H_F+%acuxcL?>EVa_HdG!%8tTPHjh z?qHA@9tE3T+`Yruj(1c+ln!>LT%HZJo^AqmOy|VZ(Q8zN&!pz!OSALf+NEh)ezoqZ zoF@Yhqy9@$x@?OR4~+(mO6;4XOs4Orhu#qzyaV7R4y&bd2(~+EGnmVvB_IqL0w=#S z!Lvc?+F2GkA_w-=R4_erXnAmlt3Tsv4HK(x6_?hu4`rmwow%8GF>Tmp2gX;#dxV$& z+@c41Gs`FaG%t4Uj@!>XPa^LjP5{d%s9q*9l~GEYhy(+wiAiQ}%V-qkG#WRG@uj^L zbgtqP$M6)o+2R%qrvP6exxV9fg|oy@YsluasAbOa^c(?Wr<#z0o0j@a>FbaYkxYSN zgW%p6o?l&ZQ41b9Vb9#6U)rr91fTHTfi1PHiGH{RHQz;(OSeDW!t@Q9`}CKa{-;lJ z3h7nkipoM#ZYm*$;#z_e4lrO@iZ&2+seo`CiqZ}po*I%MLQVFn@KUUoqpTvGyPxY( ze!{d>Qa%J*&VPCx_ zY!bxLDtwGLIy@ve|B?rU(uw`%iP)kkOL!9POFS*Owz^SCJ7xs>%xrBHdz(+KqD7ax zJV)iea5CGjpMa2<5}~67 zw7y)45kR-{fZt;H@}7T7P9qjBgC#X&h-PC<1@l86+AO0;SgzgvJ8o%92z;#Q;F16};-#>7 zq}I~XGb@ixhj(p%sG51Ts}L98&K#kEJ+*c0RJP8srrxvUf^GW)Bna%6%WNDf@!w&; zze;&F0#C(SDhd0BR8Ao|!3KQOH&d>Ts6y8A^2VVq$2*I~EPFKAVNcz4 zH&|Of5lC2FE=QKdgBD-}FX6Dl9rfw2aAHs}wQaaUgkMuZ_pA$8q9=%1wC896g|w<+ zhjiVYLN$a==IQBHo%>X(gbL*BoezzzV#k_V>xSJ@?fmITz3$&pJS{4edoF(K-uqGX z#zIZ|^39lK{dnX!&g!NExKFku!X$MPGxB43PdweMj+t?q zh@=!&M-AAwAx)>zq3)7iYv5aKFnA9$KoC5ObHz<4C=vvlY99aMLYrpTlIGdb8QC-A znRSo_4z|xA%>rMy<5{aB?^*hF#z8yaO(;g_RU%1=SBD$-e%4~I#x8BXwrCJmohM1k zT`{MZ3=Lcau>I^wg{u<#%@|0CdHbnc81?m*U^d@ePw%zTw&5^&hGUHjhv$#!bty`#Q`pGpo(C=y#qLE*N>wB^ADHMSsf)hHjJShQGd8P2rM zujc^X(liW!cz-qAq{$D>rJW2aYMu@lPD!b+5B}4)E~!6PyhbvsgJZDk7hJ|% z{Vb+`iOFx=slGB1!ibW@?Y%l1C76%8aX|Sq#v>$NUzQ_ORxAez6Ffl}dx3Bw0;t*L zsTjJE=-BNX+Ls#4naW9>(W%x}#JJ43to4ZRBP+f`GRK86O8$HGy?j5`xIWL$XfD>* zE6g5K@Q;~ZC8{)lMC6`>27W74YsqZ8jMr+x^pel6@(DgKHd7SaY(asklnlyA zZ2GT$ekmVNR0hP%mgE_u`K&yUWjZowWJ=w)ANAp%KH;Bj{Qo*Z^538TA1vS^0p&zH zdh@_CLP{_1qAva?-_gsr(t68kY`-}pVQ>cjgN#4Rh*Lv3=%>aV7UxLk+05bbmJ2a8 zz)etN*?IEp>GYIYSgtHlI*RBumFa=+1RKUMwBBm8(9xt{(dSbVb)Q!M!rjZsLRVr@ zM5%H#bEm`J-9y*5!uobK9Y#7T&YF(r>*l9erMs{OW@foCwc#V{>ccuVz^3a79Vzp% zhos1duX(7f|2A}>!W%nQLte!{2V93<9#HFm($DFIh%We=ByP^yyTD~CJjfQr) z)Pg3jMx=z|@(x8<=~7B*Lrw-XZDaPrWX}8=Y2M|U3H!pf3j)f+q*zI#uso3_gsFjU&}E@ zx3LwU40i;v@$AHSt2OMim8T$}RI#&fu2t zxVat+p(6!0#<}*}{x%X%G3JNk7e*Dc6l>tDo!)^O#sOoI=%Wqd%Tv5jduOe~@hwRY z;K{BnZbx>62_HFHvQ_(5@2G5Te+Eg^vBNRlf%7AUI^gM#lVhc;fwz3>=gzsu)TC?& z&fTI_hKKBWaRb3?dV~6CD;hn_cL((l&X32iBiEz(QMba**Hrn+&IVo1tbmQ%*NA&A zS^QLct~9JO)GAtT7h`EPlB%o6ZoeR-+qGZ)obxewg&`zqJNKv_5ed1?d;QhAG}MhY zpn?}a{%P*Q1vG}4>HCk#wJ)JAGu>NFqIVy&CkhX0L-ssf64unTlP>LMA9^vyF_Nt< z{7ofOc<<-m>Q@4BEIj*PKKCLk#(OxwOh*pF$4TF4KHTUo%vSV~e zId{?uBI@Ge`Jdu&86f6fAz;P zNY(oIeEu#lZH21_<6`q@Bm|f@-p=_pZB_d}9`I8RdhB zcb106P@~S-_61bx7v*_PtngfEUcc}9nA23`$F5COJA>1zB2+4pneo$+~$CPVP$fH@OP&;rBkx zOw$xNl$k61_EA*AN5%^w8l&TQjjYzi;RhbN$H--)ey7jegXAE2r5m{?Y44vJ-w5wF zTrcvQRFkDzB$VDms7panfN~xg-OeKGf?oDyW+&w-FC1hn3^bhAgl}vG%T?*})bx%x z4XCOxWPK;)r7(wATP|hqXlm5#8qjEGu||#b(k4mV>UKlTf`&<)N~7E9(w2)=I3rE{ zja)Q@$dOBE!n?wj{N}ID(^pDK)n{ejkMXI_4moljzx|SStHXa@7vTZ%$PGKwMIy>J&1*-zlt8DZ_U%YYM6l@w3f-~#6;uhf`CEW-QCE=3u{so=hBoKz`5G!eza zY594&XNZ!vOJTwUX)P#p4yM>#i*)ImM$TQqqtrEMvhq&$2ipDNvwxw-tDh)zI!y?Y z)RXnPz8{ZLVwkr~v2rxA(*Yvkr?nnE_N=PxK5ghAp*JI_QR6li6;j?1XfligFO zlO5FjMbYiQ-UF0Ot!*1$*AXbQN@YnKCox!3^$iV?{lx%A1#BoJ75g4Y3%C7fN}ty) z*pP8!{ncho?fKE|{tyvfH+9i>OG!#Db*|s4YyC-2<95;LtQ8wPD9~okblLEaEO63o zH9jmcARh^iUI+D@hWaIUnX=vH4?m2c48HnxUPWnyidsR=k!x9F6= z>MFw&X6SSXd+DLKL2yez<3U2kVKiH{ZG0R>(M+9F_4kXvDQjjEnyJ)m28fI%z z87`rZpr2qqofz=$zt_*<{0Mwnw&I}LoBbZP&7di>0Ah5$xp+hOL6uPRi=vFg{$|Zm zfxPo`y}_Wb=4fA>{pkqy)2}}2}DU6Ele*Fj|NLYkH5-km#hBvOwrdJkUuyVg-$6YfeO`kzXino
        hg5Z+!{Sa)O9QNxi$tfwtDZos~b$g zf~%$5p41?m+pKz)p$R;(OqAz%NpBzC-Bs|s=5}Fzx{?20LS6w?=8B26nsri7pgQJ;3C5bj0$M1LtS^c{;>_#l@s`gohuYN|zamk(kUh|tL7}g=G2^;)T zUeU-G^*(GZXbEnp8=VhipQGRpEcT%zP$hEGm7S_jENi};dXF4YE~JMgi|Exc7>E5T zb4#?cV^K*FT4Iwbzn+t~$3Qv%rDtC+iB%YXhLdUC{%$m}ruUffNRcHVvPJ%0=IQ&k z9?VV+SK>;krY?^g89zh1#h|K&9TQ zYDRz|t5`ubY6z4BGJK1jhHI0bmu*XuwbnjvGRn%l8qbMM1tsl`tetlhb0vsl`B4); zE#{rhT`06C@8aH^Voznp;rm-!_ikp7iME;3qaYW;HsrzZDomfEQ03=W5i6|cQz+s9 zUo~5bxg-uq1fnQZB4>(Gi~Z0P9(Ixsrn8Ei(}Co24=Cl{pIr)Td?YnY;bD5rhS-JEa3%c%9-tHAd4-hKh&vo)8l%bBQ7v$7%XmKi0jYZXS@ zWG5JWdM#@s)Ic!Ae#4vW`iIFT^raF1Nx$Rf`Ict6!AUcf~GCWX8>1OEAjAA z`4e8So@KJO(KVab`gGRsv3@&dGMN#R!xOD7NV`|2Lv0)37@e;5$M2#363V?ojwG|O z47H#0L!+;=-jO1{eeJZ}<&2BPL6rkDFF=G?UGlX-aAJ+vx-E2it{dVUgGL~rG zoqj=Yk8m!7&==0dFS4R4l%8{68LdZKjEVSq?lWyql9-6y4?e;A>uC}6yBJfb^$n&z zKFSJjT=Z3!w`$NJv!t`4g&DMXL{rrQo3G0EHzvRK_{6(Z%KCTbgc`@{r>Ceo?IMe+ ztO?z8wx=wt6}f~-IOxcqmheaPack{kaFx@J_?^8-VyxM5?=Lc0;fG%y_&r9=GfbsD z1a(>R4F%`D-Yz?SJ)Avmy6@QYc3)}JCn}v0yKFD0Rbe@_c=#RrDFa6SufB$fRObW8 z6xx2g+F?b~*HSM~=kZ|JpkohZr{d=~z!aU4HTDAccuBK(XLXE(aV~03UQe*G+pi|m zRJp>Dvi3lqrAsowwVQE2vp~HgFvkbPC-eD|1bHUrkLSsjD9AnCgg3mXpA3IE*2Xy6 z5TeV{|8cDg$){51%Cm3(TCT2ZW4Q)ouve(#TJS{2Sd3!VQT8mj|{J@P@B%=9sYjut=gZE|TDiBJ<7@;(bJ_jQL$R zz{mWyO6!xn`^s)!f~bz(^O>pv`_pgeh&ZS=}l+4QRzk`0knS(nO zv_G#7s8Ua3%E%js)N)bcxm*#J5P+bQyrM{{X{|njlk*=GXnxo@6kllqaKQ(JQMsN+ z)LEH)Lp#4MX&B}7*_~I~pv>42+p> zU08qLH2D*qSwnLpP-*#v*f}S*B0}RU;2Ny9Ge%{S78l{D#ay?}VhHv3NUi#LrnCml zejqp_Ns9lH$~S=dic{?GD$MxNYUGL=27|uZr_2wQZ(Na}#3F^m(MhYz+8B{*cpya9 znt9_C+hHUY>t@GxY+0i^{4F^2q?CNxioEXqcc43_Er~|;ui6F|_#p?9Pa|rX8U)tl zB-}$94~EY2_pliK7EB@(;|)<5v%+MnGJyVfjSR+Tx(KL&g%F;(sbALabvd%Fx-L2> zhvc@d$uR|+8)$m9PY1Z+*D}<%?-B)#R;f$Vl?+u!{QLOwG$K{!v}btd$0l^wIZi}`SLI`KAoH@odD&^$rO}l;`p)v~%>u)=^@wgP9jpTj1rJ&4C*zVhaEY)m{6hrPlLzedM7u3~+H}=A z$DGFW6?~3xP6ckG+_6^x@#Wtafi z?WoIE9#(F>oJ-^te$A^BL@BTccaxu(Gky_}*1NlSc&IWsY8@_Bwz8yGYJFJ$W&JMG zO37l#o&NRoM@ZJxF9kK+X{T|sci@T<_7hac55JM1(?gh>Q=grz;y%0vN9!GP#tr|b z%WFvf>Wr!gSymB^+mY})>6P|(?4!|n(Kl8U7@sNr$jubKfhqQ?ciD1q6`+ZH2XQJg z!~}c+u&@o>a}bO8y~?%h)>L}`G(Ksde&5&Q-LBe@EwigmSiFAg-LO=OUu1U|rYvRX zNm!KU#rK~%NpLC%;3d*5I8yGtVKCDg(KV8@?JwX|7(czwXZgZ13EGtr>sWA&=gi}#=l0D$Pt-P*!Rv^?g8Wnm z@e$FRlV=pW(6Psr%)G+f6n0)`$Pd%nTKftW0NLo|ZT;aF)$2toqs2Czshtn<}<;k!zWe-PP1CJGTA%>V{Qe+Ov3%>Eh$4HPI-L zcjHB#);{O#)+b2;vindYsHG84H6XPW*#jJ0&tuJ`V4}21P zFORC8Pr!==fxDCfX8`x@Xsb#d$FB6$x`<~~+DuXy^s`4vaWq`+=Y&b*{5VRPL(y^` zAtQi8YjLfHouW@W=`4Hs#N3q!h&B62Wh?4u-fLa;#asB*$TopG0~6@$xQd$Gnc@(K z@cNZct#@`oe}k`6Hi$VJ2`gMEwAsOi6uk3kNqSDAp+s8ZuD@yR=I{AQ%W7QRI!E?c zkN!zj5*-6WgMPe&l#silVR{CeN(MBOw*K*}cUa6dx!jwX{Vg_ao6M@`8f!49)!rJE z&3CZ>s$2iv?D$Zy*t(QQ4I>ig=g^XgT`I>2$D-zs8`11#MM)t$nK_Fmy3BP-_gFto zN#;t8E$c1TsYx59$YIL~O^J$cn4ZlUW{GSOB29#LHuEXDnAP?yT#6(3>n5iaFciOx zWz-+7wmEHgVCe4r>O|d$m=^c41Zlfn;k_*bS z@|PCG?Y|`~4hah_00XJt#m~!8Q+eZD zAy-VNpOZ?3`&#>32?jT^uz`tAWTY)%drA|N&;Na}#z2RS2lrdcM+TtcME;8a@A_v| zA|cr{Mwhe^ks{RnrY9RRAdvx;SBLSqQk3s6E8M?xRlB;w)xMV5k9smX2>tqIt zo4I@Vor{Oor#Iifr?yYe+?kZ|3|{xQN>u~lsaxi?oS$FKx`vfc1!c_ZZK+-7BcQhr;-a}8(@@73qbT3kqqb-yE zjQ{Z72Ni^&H7^GP6WxDwXQ$x3kT&MXPaj*%2^;mEac#&vJtH)or}zJ^lN;!WVZWX! za9Bwb>?PSadtMp`HKANe+H0RPw+8|x1-luLYMwve_AINXd(jx<5==(T{tM&Zo zeoObG=bgU?G&PsVdYP#ornW4drPls%e9P@ya3f*Nc6nbhBj&1!#(w#X*2+#UQ=-ynh31gdV%&hvbKV3NY)9@-$v`;?_|E zdVP$fT{8KXZd`tknyZwn9Ilkxe^*Y?)sDADkt3hLBWGj~v6iP%4`=`dM_r?%E)WSFj^?b37B0aT&T#TW2vctPUCo2;7yw{|f-~D|fVu z=(^F}(SAxAX0oq1(X<+dE6xl_^D~DoiLG&!VWxjb!AmM*Q%<&m*6dAFh7WtK#S2o! zB3Qrgt?Ay1Y+xu^ITBExRD9}(D$hIIS3<29mA7LO=3G|Da)w`jYEC$hJ>a7{Y9)Ge z{!rZo!l}dtrwNSev>*p(;rX z{=3*&okSZ{ef{acKi7TS0?W+Tr1h1r*_avKClr~(`@09tHOW$>^v)MSeI@t^yp<+j zQ~mmvajMUp?_O5=gQ}m^dH8-gT0fa58jq(lcb7jQf$#Bg>7{3O-{Nt%-m2Cm-_;j3 zebwmN=5$RpxJVaR``Z%?m%_|)?0!3hAt_YnG4NSqF}7E&!XAe8#$71bV;I(Nhi)qJ zjL`cHR=3ZTmNo+-nQMoncao`(WoMss+S;p4>qvBqEAu0v&Q8%M&OT`jod;{LqwCdd^}0Gm<|X;n@JhI>a@OSs33;0#KhMLN zkvYeAz*1RHJO6jK_@C6m8A49iZP<;$-O?2-e8T0=gGo=?{vD=V($nX5xuiA8`2Mka zs#zIgjx!=SxL6%dGYeg1!Ve9F_#N#mzCZHXr}UFZo48OwiJj{4*Of}AOqrM~HJ~bQ z(vIM3L*=I=GzE1K@rbr<6vu+;6>4sn35&jI*xcU0my{G1IpG#Xy`L^33fJd;ng}%b zk+*%!ay`?GB}f#PSJ3^H&l4q3Ib&T!mD1Jw_Qpy$p&Qae#!bDteCiCV%eO==9fzlO z9c7;OPfJ{h10DGq#RI4<&cSDXCyhkb&G`-saMq6_YhP5KJ(gkSjP3r2B$qMCJ2#Qk z(YuXJ19q}&=-jOQ{U>z!BXsS2bO#4ctdGk5_dheV72<@n!8Ee!H>^F6%1&oF%c;x^ z{C;t|&To@SX`+j2-kX0Jw`=5+zHuH{xIHao1_vY}aRUi}TB|BCL}csMA4!UYk65Mu z&cFU?vQSRSTg1T3HY_Z5{49f4CN68%#uedK7`x1TNJDpX{r&IBK9Df{C)8$&>fNO8 zIc=lIN;2n4z}*VnR-uP2Q2bs9PG8A$~ zpwNOo}sg^=@7h`m*!TZRzUoHI<3?aWz5+ka9%T6({?YiUA$cAIF>G9zhgFwl5R&MT<@7)e=i z-kzP*pttn5-J$2`Ds;Mft0P=_+t1}P_0V_i+;A_w;H{e|W`|^VmnD3eX{Kz;rVf1@ z@VEOTlzeac_#Vv)guV ze7UL5vm`1xjj2twRcax@?@3u_fcH>gb}CYp($L*}J*bLf`}t3LfBT^=I)H5$R_FWG z0IzOmLP(*G2K7gu5zViF2RZqk6CEeN^d!PA+^744&-X{2qKN$3B=T@7wQKnvass#g zzTGNBnHTOI124X`C@k7&PPkihGX5yj`7*eD=hm?90L5FO^lQamw$HuP~$f#RD!1d zYWECz{{3AKs&v`Zplg`Ydou7Z4unfW=#}i=xjrKMO5Vzhv`(mYdJkMTDA?UsGi+U) zedONW7exYLj(8xMECKzQSvFd(@^uo!g>{fb@7VK7r7$G(n1YQ0gn(NAiX4nK@Uve-OMyYi58 zW<^nFJ8kFf#0`b3kon#4iWx+srw+b9%);GBle^$rXun|9>w4m;1}NFIvYNJ#-T|5% z`0%;meMrVZ-_BZayOV@3O>3}Tt~(={(o3n!n{5HzH0m`coh`BVp6#!5UUbN_>BB@u?iD#Qe^=3xVo2*B? z#Gjfm&VQEBK548hXpdHYXo-4qB;L4qX_WW%g(ef_!6|14t75LK6GPVZZ-EG_zO|5r z`D31JWSS=yfBGmp%TX{*eCJ`rw=?PuuX0J~9fZ&Gf-974&eSXeiQ1%6TIhy=CT5=f0kZ+GreHr*Q`X#_I^ zAzBNX(vIrOsdz?Ai#(IEoK_8>MwY3#Vo-r&xs3&}aqg=U67qY8D!Z(jVsXQoXAdjh zQe>!^Qw7-JvoUH zud0$NIqIBO{GR<*ZzyPo-KvI%EAVTwu@av|d|s8_RWU6%WZC)D%<3$tD?N!x>1ML2 z6YKVi2OXqDl&AM_!j-oX?*yl?EL7?*fDDf%)gDQ5V_#`66K)DS7SXc$N+-G=rMgMw zeTFebq7p)7=*c35{n^NfR&< z)rl>OH3xHyUV1n7x{!8fS@zxXeBkwjPWuDQ4~#x6QAW?)@}9_FO9O*2PC5(NSK(z& z>mbQ-Ve?D4=@@_88X2_?67i?tI^*1yWZP%dH)!HYOLri%zj&@UtLJ-K(Qf0hl;I`}mk`cS_iTM7rv!79T$VK<^#>EDT3vBM4wt+m?%xNFUYL+BeY;z^jtfEdiglxO`qP;3D%0fGrjXE zg!D+t;{q@3Y_PUUtEqTwZTvoi&pR!~KmZt?Sz@&QS$E)6x|#U7FgF7$oa`h5s31M- z*um<^Y+SK%ban&x!!nhvz#PyK|8NHnd5ZqFQZH+;sD% z_hf)BFAOM3sSiOWtJaxIUOb5*_mazzNkcG8CLe)Gs)3EmRB@U!%bZDnxFt`P zLYYr3yn)tewRuw8r&=i|ns>zUF?3j_CdpQvboGX4s%|iRN`G8(p*B7Yd2PQjpBtdb z3(YfHFTI-NZ*__?Rg(oS@|kD4y&c2Pao#Cn2jO@dN>P~OSgSTFo?6*(Un#A*bZp9Y zxn$-u>0>2~I1guGKySX|pW(rz&!Q5pzl(Hau$&{5tMu~m!(UsG zaoeA=_h0Q-R}26rxt=M9p6|}Ndp?O9#=qYkw?IbGh zy1u!Xx=3wLx6FV#WHPRDxzfAh&$G|pWIF>{T!@P_)GAE+PPGfYywR@gT7Dj>huP>= zvYLS_25*0F!QpUAJay-&GgPFC@a7tWt1ZXXQIZ$8lBP*Ia51^`Z&cn%&3w71wwuln z6LmiMG7x|e?$oQ@#AfmAL?5+4(P)37JLE|hSGko8MSZ(G!Ijp9E=v{xY@1AmO^%&VnGZT__ z@V3ms@erHoqBQqd4_T%CE~pSJG`B~|{pV@Eh0&kpE75LTtLes-Z4SvYm8!2*hv8cq z=T9WK$#r3|Et$)spM~_j;2iC>h>`Ei`fzt1BU_8EKka(-$Y|I21S^djms3`PEI=?j zy>0;l;XeuYTuII+&hoM>eEFSYs)o)z4h&%&d+c{{4bs+5bjoGujmvWFV&lTfmdYwC z?t{RaYF*RC9|0fXuEh{x!Tt5T_9LYzDJ!O0ayT=2l=-lNke$sxs&`69RA6@z%! z>0OP=Rts2np+?WM6VthIn=wGn^M;Ej@TU(&{87&UprMx2h33srC_NKsqA!Pi9OCpu zkJ^%K7!Mfe?GJKkL!MkabaI3oWICL~Ovn|aH);w-rNS{Wp@R*>=~S(^xa2j&Sjuhx z_ZaytkNL2SxeWPoud%w_v(5-5#!Ia};MM!tswhWgk;X=W8&&d{=o(<)TXN1>hd+}@ z)lvhSYko9@M-m8fo0ObgJ0~x_cwkRTJP&+nKk+Lp<*-jZ2Vapz+ndl-IsDO{s;e`U z4t$%EfiN&`*y+VT>TDAzyq7Ijl7TFDCtY_s=7Qy~E>Wu2q~e)V(j-sxirjvUXyHoY z6$2}-*BcFM+ck3D$yYPod*frE-xKznDUFhj(uw2w;`cl0?B}Z?m4my@(z6^~STC(g ztdP{^Ys-QM=Rf+Us~_vO2i86w&f1bt~8EnTyzL_g--|{ z9nVx>=l1(~HqTB&DV;xb@Rv*nvmDJU%`33Mj+cNL_Tcg)k5&^+!^BmMar>?TUnwKz&qv9N3|Jj3c&L| z>*(keg~1g!gK+-Gj3d`0a|drt2Ph+1eg;3|FPtfATSWcrDc0_|=)FvILvY0ATgUcMa7@ATTWxOU493lGQc2-Atku>vjmO91y z!?u`)ck`^07!7MSb6ugaZ?8IoxoB8>@+E3+ni>#qudw&>~NYjS_f7N8b=8WsF zDjY}y+agAyI471;MdJ0$)Fi`v<`o&yfGiJ;mt*inKa0Q+E6VYKA0Hvc`;??*^L+g$ zzoV1Ur~^vFB*Ja1FnaN-y60k zH?lnLkM2i>xf1$Yzn7gSD7zdqHst2a*_tD^@~m@({8YWCkxT}?V%O;@V1L<3Y86L_ zo9<876ChnrXK9=+WYqo0gLPk=Rg zbi7J?5aFy~W~TUDG825HvN007Ap&>u7Ir+n`Kf>ViN1p;!_?N}9`H(FFo5GLjfsqR zkb>hKMtN(T{{_LtAQ>5_-r5B%>Fp#FG4t z61LJ3@5f4L7K7G(amMN3=!sU36fAo2^_1YBtQhrhX6H)S1+X*5sEHxPRQ48!rp{Vq z*=stS65K@YZ0@IZ#HI|c)`?yr2zZkY(Q;iu#JfQnm19hOBCp&#(bKYX{?Z?a@ zzEfQ>oR3MIJUaEGIeUi(-Qi}`{}(--BFvDHAUGv?wo0y|L&8PbBlJ)ARAQUF+MpsPS3x+00!x4ls~b4 F`9JqINYVfR diff --git a/docs/api/source/includes/_errors.md b/docs/api/source/includes/_errors.md deleted file mode 100644 index 7b35e92b..00000000 --- a/docs/api/source/includes/_errors.md +++ /dev/null @@ -1,22 +0,0 @@ -# Errors - - - -The Kittn API uses the following error codes: - - -Error Code | Meaning ----------- | ------- -400 | Bad Request -- Your request is invalid. -401 | Unauthorized -- Your API key is wrong. -403 | Forbidden -- The kitten requested is hidden for administrators only. -404 | Not Found -- The specified kitten could not be found. -405 | Method Not Allowed -- You tried to access a kitten with an invalid method. -406 | Not Acceptable -- You requested a format that isn't json. -410 | Gone -- The kitten requested has been removed from our servers. -418 | I'm a teapot. -429 | Too Many Requests -- You're requesting too many kittens! Slow down! -500 | Internal Server Error -- We had a problem with our server. Try again later. -503 | Service Unavailable -- We're temporarily offline for maintenance. Please try again later. diff --git a/docs/api/source/includes/_git.md b/docs/api/source/includes/_git.md index e69de29b..f797a639 100644 --- a/docs/api/source/includes/_git.md +++ b/docs/api/source/includes/_git.md @@ -0,0 +1,148 @@ +# Git Repositories + + + +Endpoints for interacting with the list of Git repositories stored on the +server. + +## List repos + +```shell +curl \ + -H 'X-Api-Key: secret' \ + https://example.com/api/repos?offset=10&limit=20 +``` + +> JSON Output format + +```json +{ + "message": "", + "data": [ + { + "id": 1, + "url": "https://aur.archlinux.org/discord-ptb.git", + "branch": "master", + "repo": "bur", + "schedule": "", + "arch": [ + { + "id": 1, + "repo_id": 1, + "value": "x86_64" + } + ] + } + ] +} +``` + +Retrieve a list of Git repositories. + +### HTTP Request + +`GET /api/repos` + +### Query Parameters + +Parameter | Description +--------- | ----------- +limit | Maximum amount of results to return. +offset | Offset of results. +repo | Limit results to repositories that publish to the given repo. + +## Get a repo + +```shell +curl \ + -H 'X-Api-Key: secret' \ + https://example.com/api/repos/15 +``` + +> JSON Output format + +```json +{ + "message": "", + "data": { + "id": 1, + "url": "https://aur.archlinux.org/discord-ptb.git", + "branch": "master", + "repo": "bur", + "schedule": " 0 3", + "arch": [ + { + "id": 1, + "repo_id": 1, + "value": "x86_64" + } + ] + } +} +``` + +Get info about a specific Git repository. + +### HTTP Request + +`GET /api/repos/:id` + +### URL Parameters + +Parameter | Description +--------- | ----------- +repo | ID of requested repo + +## Create a new repo + +Create a new Git repository with the given data. + +### HTTP Request + +`POST /api/repos` + +### Query Parameters + +Parameter | Description +--------- | ----------- +url | URL of the Git repository. +branch | Branch of the Git repository. +repo | Vieter repository to publish built packages to. +schedule | Cron build schedule +arch | Comma-separated list of architectures to build package on. + +## Modify a repo + +Modify the data of an existing Git repository. + +### HTTP Request + +`PATCH /api/repos` + +### Query Parameters + +Parameter | Description +--------- | ----------- +url | URL of the Git repository. +branch | Branch of the Git repository. +repo | Vieter repository to publish built packages to. +schedule | Cron build schedule +arch | Comma-separated list of architectures to build package on. + +## Remove a repo + +Remove a Git repository from the server. + +### HTTP Request + +`DELETE /api/repos/:id` + +### URL Parameters + +Parameter | Description +--------- | ----------- +repo | ID of repo to remove diff --git a/docs/api/source/includes/_repository.md b/docs/api/source/includes/_repository.md index 20f5e5fe..fbbc329a 100644 --- a/docs/api/source/includes/_repository.md +++ b/docs/api/source/includes/_repository.md @@ -7,7 +7,7 @@ possible. ## Get a package archive or database file ```shell -curl -OL https://example.com/bur/x86_64/tuxedo-keyboard-3.0.10-1-x86_64.pkg.tar.zst +curl -L https://example.com/bur/x86_64/tuxedo-keyboard-3.0.10-1-x86_64.pkg.tar.zst ``` This endpoint is really the entire repository. It serves both the package @@ -43,7 +43,7 @@ filename | actual filename to request ## Check whether file exists ```shell -curl -IL https://example.com/bur/x86_64/tuxedo-keyboard-3.0.10-1-x86_64.pkg.tar.zst +curl -L https://example.com/bur/x86_64/tuxedo-keyboard-3.0.10-1-x86_64.pkg.tar.zst ``` The above request can also be performed as a HEAD request. The behavior is the @@ -73,7 +73,7 @@ This endpoint requests authentication. ```shell curl \ -H 'X-Api-Key: secret' \ - -XPOST + -XPOST \ -T tuxedo-keyboard-3.0.10-1-x86_64.pkg.tar.zst \ https://example.com/some-repo/publish ``` diff --git a/docs/api/source/index.html.md b/docs/api/source/index.html.md index a46d47ea..74774982 100644 --- a/docs/api/source/index.html.md +++ b/docs/api/source/index.html.md @@ -11,7 +11,6 @@ includes: - repository - git - logs - - errors search: true diff --git a/docs/config.toml b/docs/config.toml index 7f7e1863..5b0f20f4 100644 --- a/docs/config.toml +++ b/docs/config.toml @@ -27,16 +27,22 @@ enableGitInfo = true weight = 1 [menu] -# [[menu.before]] +[[menu.after]] + name = "API Documentation" + url = "https://rustybever.be/docs/vieter/api" + weight = 10 +[[menu.after]] + name = "Man Pages" + url = "https://rustybever.be/man/vieter/vieter.1.html" + weight = 20 [[menu.after]] name = "Source" url = "https://git.rustybever.be/Chewing_Bever/docs" - weight = 10 - + weight = 30 [[menu.after]] name = "Hugo Theme" url = "https://github.com/alex-shpak/hugo-book" - weight = 20 + weight = 40 [params] # (Optional, default light) Sets color theme: light, dark or auto. diff --git a/docs/content/api-old.md b/docs/content/api-old.md deleted file mode 100644 index 0fbb694f..00000000 --- a/docs/content/api-old.md +++ /dev/null @@ -1,89 +0,0 @@ -# API Reference - -All routes that return JSON use the following shape: - -```json -{ - "message": "some message", - "data": {} -} -``` - -Here, data can be any JSON object, so it's not guaranteed to be a struct. - -### `GET ///` - -This route serves the contents of a specific architecture' repo. - -If `` is one of `.db`, `.files`, `.db.tar.gz` or -`.files.tar.gz`, it will serve the respective archive file from the -repository. - -If `` contains `.pkg`, it assumes the request to be for a package -archive & will serve that file from the specific arch-repo's package directory. - -Finally, if none of the above are true, Vieter assumes it to be request for a -package version's desc file & tries to serve this instead. This functionality -is very useful for the build system for checking whether a package needs to be -rebuilt or not. - -### `HEAD ///` - -Behaves the same as the above route, but instead of returning actual data, it -returns either 200 or 404, depending on whether the file exists. This route is -used by the build system to determine whether a package needs to be rebuilt. - -### `POST //publish` - -This route is used to upload packages to a repository. It requires the API -key to be provided using the `X-Api-Key` HTTP header. Vieter will parse the -package's contents & update the repository files accordingely. I find the -easiest way to use this route is using cURL: - -```sh -curl -XPOST -T "path-to-package.pkg.tar.zst" -H "X-API-KEY: your-api-key" https://example.com/somerepo/publish -``` - -Packages are automatically added to the correct arch-repo. If a package type is -`any`, the package is added to the configured `default_arch`, as well as all -already present arch-repos. To prevent unnecessary duplication of package -files, these packages are shared between arch-repos' package directories using -hard links. - -{{< hint info >}} -**Note** -Vieter only supports uploading archives compressed using either gzip, zstd or -xz at the moment. -{{< /hint >}} - -### `GET /health` - -This endpoint's only use is to be used with healthchecks. It returns a JSON -response with the message "Healthy.". - -## API - -All API routes require the API key to provided using the `X-Api-Key` header. -Otherwise, they'll return a status code 401. - -### `GET /api/repos` - -Returns the current list of Git repositories. - -### `GET /api/repos/` - -Get the information for the Git repo with the given ID. - -### `POST /api/repos?&&&` - -Adds a new Git repository with the provided URL, Git branch & comma-separated -list of architectures. - -### `DELETE /api/repos/` - -Deletes the Git repository with the provided ID. - -### `PATCH /api/repos/?&&&` - -Updates the provided parameters for the repo with the given ID. All arguments -are optional. From 7ad5830e9f4bb530901d4efcd94b01f741e41ea5 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Fri, 3 Jun 2022 22:40:29 +0200 Subject: [PATCH 053/236] fix(ci): correct cd during docs build --- .woodpecker/.docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.woodpecker/.docs.yml b/.woodpecker/.docs.yml index f4567596..c342e388 100644 --- a/.woodpecker/.docs.yml +++ b/.woodpecker/.docs.yml @@ -29,7 +29,7 @@ pipeline: commands: - cp -r docs/api/build docs/public/api - 'cd docs/public && tar czvf ../../docs.tar.gz *' - - 'cd src/_docs && tar czvf ../../api-docs.tar.gz *' + - 'cd ../../src/_docs && tar czvf ../../api-docs.tar.gz *' when: event: push branch: dev From e734e658a0de80d3bfc02b1be16f674f10e7b1ff Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sat, 4 Jun 2022 12:11:51 +0200 Subject: [PATCH 054/236] fix(server): publish build log now uses epoch value for dates --- src/client/logs.v | 4 ++-- src/server/logs.v | 10 ++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/client/logs.v b/src/client/logs.v index d4d373f7..739de238 100644 --- a/src/client/logs.v +++ b/src/client/logs.v @@ -42,8 +42,8 @@ pub fn (c &Client) get_build_log_content(id int) ?string { pub fn (c &Client) add_build_log(repo_id int, start_time time.Time, end_time time.Time, arch string, exit_code int, content string) ?Response { params := { 'repo': repo_id.str() - 'startTime': start_time.str() - 'endTime': end_time.str() + 'startTime': start_time.unix_time().str() + 'endTime': end_time.unix_time().str() 'arch': arch 'exitCode': exit_code.str() } diff --git a/src/server/logs.v b/src/server/logs.v index 51b364fa..314e3228 100644 --- a/src/server/logs.v +++ b/src/server/logs.v @@ -70,13 +70,19 @@ fn (mut app App) post_log() web.Result { } // Parse query params - start_time := parse_query_time(app.query['startTime']) or { + start_time_int := app.query['startTime'].int() + + if start_time_int == 0 { return app.json(http.Status.bad_request, new_response('Invalid or missing start time.')) } + start_time := time.unix(start_time_int) - end_time := parse_query_time(app.query['endTime']) or { + end_time_int := app.query['endTime'].int() + + if end_time_int == 0 { return app.json(http.Status.bad_request, new_response('Invalid or missing end time.')) } + end_time := time.unix(end_time_int) if 'exitCode' !in app.query { return app.json(http.Status.bad_request, new_response('Missing exit code.')) From fc6d3909d2819d416e284d38011330c076969bd2 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sat, 4 Jun 2022 12:18:30 +0200 Subject: [PATCH 055/236] docs(slate): add build logs API documentation --- CHANGELOG.md | 3 +- docs/api/source/includes/_git.md | 18 ++-- docs/api/source/includes/_logs.md | 140 ++++++++++++++++++++++++++++++ 3 files changed, 154 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93e566a6..a46a4335 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * CLI flags to take advantage of above API improvements * Added CLI command to generate all man pages * PKGBUILDs now install man pages -* CLI man pages are now hosted on https://rustybever.be +* Hosted CLI man pages ([vieter(1)](https://rustybever.be/man/vieter/vieter.1.html)) +* Proper HTTP API docs ([link](https://rustybever.be/docs/vieter/api/)) ### Changed diff --git a/docs/api/source/includes/_git.md b/docs/api/source/includes/_git.md index f797a639..f41d28d5 100644 --- a/docs/api/source/includes/_git.md +++ b/docs/api/source/includes/_git.md @@ -17,7 +17,7 @@ curl \ https://example.com/api/repos?offset=10&limit=20 ``` -> JSON Output format +> JSON output format ```json { @@ -63,7 +63,7 @@ curl \ https://example.com/api/repos/15 ``` -> JSON Output format +> JSON output format ```json { @@ -73,7 +73,7 @@ curl \ "url": "https://aur.archlinux.org/discord-ptb.git", "branch": "master", "repo": "bur", - "schedule": " 0 3", + "schedule": "0 3", "arch": [ { "id": 1, @@ -95,7 +95,7 @@ Get info about a specific Git repository. Parameter | Description --------- | ----------- -repo | ID of requested repo +id | ID of requested repo ## Create a new repo @@ -121,7 +121,13 @@ Modify the data of an existing Git repository. ### HTTP Request -`PATCH /api/repos` +`PATCH /api/repos/:id` + +### URL Parameters + +Parameter | Description +--------- | ----------- +id | ID of requested repo ### Query Parameters @@ -145,4 +151,4 @@ Remove a Git repository from the server. Parameter | Description --------- | ----------- -repo | ID of repo to remove +id | ID of repo to remove diff --git a/docs/api/source/includes/_logs.md b/docs/api/source/includes/_logs.md index e69de29b..d4f76320 100644 --- a/docs/api/source/includes/_logs.md +++ b/docs/api/source/includes/_logs.md @@ -0,0 +1,140 @@ +# Build Logs + + + +Endpoints for interacting with stored build logs. + +## List logs + +```shell +curl \ + -H 'X-Api-Key: secret' \ + https://example.com/api/logs?offset=10&limit=20 +``` + +> JSON output format + +```json +{ + "message": "", + "data": [ + { + "id": 1, + "repo_id": 3, + "start_time": 1652008554, + "end_time": 1652008559, + "arch": "x86_64", + "exit_code": 0 + } + ] +} +``` + +Retrieve a list of build logs. + +### HTTP Request + +`GET /api/logs` + +### Query Parameters + +Parameter | Description +--------- | ----------- +limit | Maximum amount of results to return. +offset | Offset of results. +repo | Only return builds published to this repository. +before | Only return logs started before this time (UTC epoch) +after | Only return logs started after this time (UTC epoch) +arch | Only return logs built on this architecture +exit_codes | Comma-separated list of exit codes to limit result to; using `!` as a prefix makes it exclude that value. For example, `1,2` only returns logs with status code 1 or 2, while `!1,!2` returns those that don't have 1 or 2 as the result. + + +## Get build log + +```shell +curl \ + -H 'X-Api-Key: secret' \ + https://example.com/api/logs/15 +``` + +> JSON output format + +```json +{ + "message": "", + "data": { + "id": 1, + "repo_id": 3, + "start_time": 1652008554, + "end_time": 1652008559, + "arch": "x86_64", + "exit_code": 0 + } +} +``` + +Retrieve info about a specific build log. + +### HTTP Request + +`GET /api/logs/:id` + +### URL Parameters + +Parameter | Description +--------- | ----------- +id | ID of requested log + +## Get log contents + +```shell +curl \ + -H 'X-Api-Key: secret' \ + https://example.com/api/logs/15/content +``` + +Retrieve the contents of a build log. The response is the build log in +plaintext. + +### HTTP Request + +`GET /api/logs/:id/content` + +### URL Parameters + +Parameter | Description +--------- | ----------- +id | ID of requested log + +## Publish build log + + + +Publish a new build log to the server. + +### HTTP Request + +`POST /api/logs` + +### Query parameters + +Parameter | Description +--------- | ----------- +id | ID of requested log +startTime | Start time of the build (UTC epoch) +endTime | End time of the build (UTC epoch) +arch | Architecture on which the build was done +exitCode | Exit code of the build container + +### Request body + +Plaintext contents of the build log. From 95441bdea03f826952b225520f73651d7dfb7329 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sun, 5 Jun 2022 22:21:54 +0200 Subject: [PATCH 056/236] refactor: migrated to Response.body --- src/client/client.v | 2 +- src/web/web.v | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/client/client.v b/src/client/client.v index 7cb6be5d..2bb1ac20 100644 --- a/src/client/client.v +++ b/src/client/client.v @@ -66,5 +66,5 @@ fn (c &Client) send_request_with_body(method Method, url string, params map[s fn (c &Client) send_request_raw_response(method Method, url string, params map[string]string, body string) ?string { res := c.send_request_raw(method, url, params, body)? - return res.text + return res.body } diff --git a/src/web/web.v b/src/web/web.v index f237e246..b053904d 100644 --- a/src/web/web.v +++ b/src/web/web.v @@ -25,12 +25,12 @@ pub const ( http_302 = http.new_response( status: .found - text: '302 Found' + body: '302 Found' header: headers_close ) http_400 = http.new_response( status: .bad_request - text: '400 Bad Request' + body: '400 Bad Request' header: http.new_header( key: .content_type value: 'text/plain' @@ -38,7 +38,7 @@ pub const ( ) http_404 = http.new_response( status: .not_found - text: '404 Not Found' + body: '404 Not Found' header: http.new_header( key: .content_type value: 'text/plain' @@ -46,7 +46,7 @@ pub const ( ) http_500 = http.new_response( status: .internal_server_error - text: '500 Internal Server Error' + body: '500 Internal Server Error' header: http.new_header( key: .content_type value: 'text/plain' @@ -209,7 +209,7 @@ pub fn (mut ctx Context) send_response_to_client(mimetype string, res string) bo mut resp := http.Response{ header: header.join(web.headers_close) - text: res + body: res } resp.set_version(.v1_1) resp.set_status(ctx.status) From 4ecf6a11c4518f01854fbace08ce33db382a732b Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Wed, 8 Jun 2022 22:16:46 +0200 Subject: [PATCH 057/236] docs: rewrote installation page --- README.md | 37 +++++----- docs/config.toml | 7 +- docs/content/_index.md | 15 ++-- docs/content/installation.md | 131 +++++++++++++++++++---------------- 4 files changed, 97 insertions(+), 93 deletions(-) diff --git a/README.md b/README.md index 34e6fd3a..892396a2 100644 --- a/README.md +++ b/README.md @@ -22,13 +22,6 @@ a while now. I wanted a fast language that I could code while relaxing, without having to exert too much mental effort & V seemed like the right choice for that. -### Compiler - -Vieter compiles with the standard Vlang compiler. However, I do maintain a -[mirror](https://git.rustybever.be/Chewing_Bever/v). This is to ensure my CI -does not break without reason, as I control when & how frequently the mirror is -updated to reflect the official repository. - ## Features * Arch repository server @@ -41,20 +34,24 @@ updated to reflect the official repository. ## Building -In order to build Vieter, you'll need a couple of libraries: +Besides a V installer, Vieter also requires the following libraries to work: -* An installation of V * gc * libarchive * openssl +* sqlite3 -**NOTE**: if you encounter any issues compiling Vieter using the absolute -latest version of V, it might be because my mirror is missing a specific commit -that causes issues. For this reason, the `make v` command exists which will -clone my compiler in the `v` directory & build it. Afterwards, you can use this -compiler with make by prepending all make commands with `V_PATH=v/v`. If you do -encounter this issue, please let me know so I can update my mirror & the -codebase to fix it! +### Compiler + +Vieter compiles with the standard Vlang compiler. However, I do maintain a +[mirror](https://git.rustybever.be/vieter/v). This is to ensure my CI does not +break without reason, as I control when & how frequently the mirror is updated +to reflect the official repository. + +If you encounter issues using the latest V compiler, try using my mirror +instead. `make v` will clone the repository & build the mirror. Afterwards, +prepending any make command with `V_PATH=v/v` tells make to use the locally +compiled mirror instead. ## Contributing @@ -70,7 +67,7 @@ If you wish to contribute to the project, please take note of the following: The `docs` directory contains a Hugo site consisting of all user & administrator documentation. `docs/api` on the other hand is a -[slate](https://github.com/slatedocs/slate) project describing the HTTP web +[Slate](https://github.com/slatedocs/slate) project describing the HTTP web API. To modify the Hugo documentation, you'll need to install Hugo. Afterwards, you @@ -81,8 +78,8 @@ can use the following commands inside the `docs` directory: hugo # Host an auto-refreshing web server with the documentation. Important to note -is that the files will be at `http://localhost:1313/docs/vieter` instead of -just `http://localhost:1313/docs/vieter` +# is that the files will be at `http://localhost:1313/docs/vieter` instead of +# just `http://localhost:1313/` hugo server ``` @@ -96,6 +93,6 @@ docker run \ -v $(pwd)/docs/api/source:/srv/slate/source slatedocs/slate serve ``` -This'll make the slate docs available at http://localhost:4567. Sadly, this +This will make the Slate docs available at http://localhost:4567. Sadly, this server doesn't auto-refresh, so you'll have to manually refresh your browser every time you make a change. diff --git a/docs/config.toml b/docs/config.toml index 5b0f20f4..61aa3ae2 100644 --- a/docs/config.toml +++ b/docs/config.toml @@ -6,7 +6,8 @@ theme = 'hugo-book' # Book configuration disablePathToLower = true -enableGitInfo = true +# Doesn't work with docs as subdir +enableGitInfo = false # Needed for mermaid/katex shortcodes [markup] @@ -29,7 +30,7 @@ enableGitInfo = true [menu] [[menu.after]] name = "API Documentation" - url = "https://rustybever.be/docs/vieter/api" + url = "https://rustybever.be/docs/vieter/api/" weight = 10 [[menu.after]] name = "Man Pages" @@ -69,7 +70,7 @@ enableGitInfo = true # Set source repository location. # Used for 'Last Modified' and 'Edit this page' links. - BookRepo = 'https://git.rustybever.be/Chewing_Bever/docs' + BookRepo = 'https://git.rustybever.be/vieter/vieter/docs' # (Optional, default 'commit') Specifies commit portion of the link to the page's last modified # commit hash for 'doc' page type. diff --git a/docs/content/_index.md b/docs/content/_index.md index 3a1144ba..efcdf6d5 100644 --- a/docs/content/_index.md +++ b/docs/content/_index.md @@ -9,12 +9,9 @@ documentation might not be relevant anymore for the latest release. ## Overview -Vieter has a few main features: - -* It's a simple & lightweight implementation of an Arch repository server -* It allows for uploading of built package archives -* It supports a basic build system to periodically re-build packages & upload - them to the server +Vieter consists of two main parts, namely an implementation of an Arch +repository server & a scheduling system to periodically build Pacman packages & +publish them to a repository. {{< hint info >}} **Note** @@ -26,12 +23,12 @@ well. ### Why? -Vieter is my personal solution for a problem I've been facing for months: +Vieter is my personal solution to a problem I've been facing for months: extremely long AUR package build times. I run EndeavourOS on both my laptops, one of which being a rather old MacBook Air. I really like being a beta-tester for projects & run development builds for multiple packages (nheko, -newsflash...). The issue with this is that I have to regularly re-build these -packages in order to stay up to date with development & these builds can take a +newsflash...). Because of this, I have to regularly re-build these packages in +order to stay up to date with development. However, these builds can take a really long time on the old MacBook. This project is a solution to that problem: instead of building the packages locally, I can build them automatically in the cloud & just download them whenever I update my system! diff --git a/docs/content/installation.md b/docs/content/installation.md index 17d3874c..5cc7a0ee 100644 --- a/docs/content/installation.md +++ b/docs/content/installation.md @@ -3,76 +3,85 @@ weight: 10 --- # Installation +Vieter consists of a single binary, akin to busybox. The binary's behavior is +determined by its CLI arguments, e.g. `vieter server` starts the repository +server. + +All installation solutions can be configured the same way, +as described [here](/configuration). + ## Docker -Docker is the recommended way to install vieter. The images can be pulled from -[`chewingbever/vieter`](https://hub.docker.com/r/chewingbever/vieter). You can -either pull a release tag (e.g. `chewingbever/vieter:0.1.0-rc1`), or pull the -`chewingbever/vieter:dev` tag. The latter is updated every time a new commit is -pushed to the development branch. This branch will be the most up to date, but -does not give any guarantees about stability, so beware! +Docker images are published to the +[`chewingbever/vieter`](https://hub.docker.com/r/chewingbever/vieter) Docker +Hub repository. You can either pull a release tag (e.g. +`chewingbever/vieter:0.1.0-rc1`), or pull the `chewingbever/vieter:dev` tag. +The latter is updated every time a new commit is pushed to the development +branch. This branch will be the most up to date, but does not give any +guarantees about stability, so beware! -The simplest way to run the Docker image is using a plain Docker command: +Due to the single-binary design of Vieter, this image can be used both for the +repository server & the cron daemon. -```sh -docker run \ - --rm \ - -d \ - -v /path/to/data:/data \ - -e VIETER_API_KEY=changeme \ - -e VIETER_DEFAULT_ARCH=x86_64 \ - -p 8000:8000 \ - chewingbever/vieter:dev +Below is an example compose file to set up both the repository server & the +cron daemon: + +```yaml +version: '3' + +services: + server: + image: 'chewingbever/vieter:dev' + restart: 'always' + + environment: + - 'VIETER_API_KEY=secret' + - 'VIETER_DEFAULT_ARCH=x86_64' + volumes: + - 'data:/data' + + cron: + image: 'chewingbever/vieter:dev' + restart: 'always' + user: root + command: 'vieter cron' + + environment: + - 'VIETER_API_KEY=secret' + # MUST be public URL of Vieter repository + - 'VIETER_ADDRESS=https://example.com' + - 'VIETER_DEFAULT_ARCH=x86_64' + - 'VIETER_MAX_CONCURRENT_BUILDS=2' + - 'VIETER_GLOBAL_SCHEDULE=0 3' + +volumes: + data: ``` -Here, you should change `/path/to/data` to the path on your host where you want -vieter to store its files. - -The default configuration will store everything inside the `/data` directory. - -Inside the container, the Vieter server runs on port 8000. This port should be -exposed to the public accordingely. - -For an overview of how to configure vieter & which environment variables can be -used, see the [Configuration](/configuration) page. - ## Binary -On the [releases](https://git.rustybever.be/Chewing_Bever/vieter/releases) -page, you can find statically compiled binaries for all released versions. You -can download the binary for your host's architecture & run it that way. +On the +[releases](https://git.rustybever.be/vieter/vieter/releases) +page, you can find statically compiled binaries for all +released versions. This is the same binary as used inside +the Docker images. -For more information about configuring the binary, check out the -[Configuration](/configuration) page. +## Arch + +I publish both development & release versions of Vieter to my personal +repository, https://arch.r8r.be. Packages are available for `x86_64` & +`aarch64`. To use the repository, add the following to your `pacman.conf`: + +``` +[vieter] +Server = https://arch.r8r.be/$repo/$arch +SigLevel = Optional +``` + +Afterwards, you can update your system & install the `vieter` package for the +latest official release or `vieter-git` for the latest development release. ## Building from source -Because the project is still in heavy development, it might be useful to build -from source instead. Luckily, this process is very easy. You'll need make, -libarchive & openssl; all of which should be present on an every-day Arch -install. Then, after cloning the repository, you can use the following commands: - -```sh -# Builds the compiler; should usually only be ran once. Vieter compiles using -# the default compiler, but I maintain my own mirror to ensure nothing breaks -# without me knowing. -make v - -# Build vieter -# Alternatively, use `make prod` to build the production build. -make -``` -{{< hint info >}} -**Note** -My version of the V compiler is also available on my Vieter instance, -https://arch.r8r.be. It's in the `vieter` repository, with the package being -named `vieter-v`. The compiler is available for both x86_64 & aarch64. -{{< /hint >}} - -## My Vieter instance - -Besides uploading development Docker images, my CI also publishes x86_64 & -aarch64 packages to my personal Vieter instance, https://arch.r8r.be. If you'd -like, you can use this repository as well by adding it to your Pacman -configuration as described [here](/usage#configuring-pacman). Both the -repository & the package are called `vieter`. +The project [README](https://git.rustybever.be/vieter/vieter#building) contains +instructions for building Vieter from source. From d7d77afe09f193582b47168447bec40f8666f78e Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Thu, 9 Jun 2022 09:32:41 +0200 Subject: [PATCH 058/236] docs: wrote part of new usage section --- docs/config.toml | 12 +++++----- docs/content/installation.md | 15 ++++++++++++ docs/content/usage/_index.md | 4 ++++ docs/content/usage/repository.md | 38 +++++++++++++++++++++++++++++++ docs/content/{ => usage}/usage.md | 0 5 files changed, 63 insertions(+), 6 deletions(-) create mode 100644 docs/content/usage/_index.md create mode 100644 docs/content/usage/repository.md rename docs/content/{ => usage}/usage.md (100%) diff --git a/docs/config.toml b/docs/config.toml index 61aa3ae2..d0eecb27 100644 --- a/docs/config.toml +++ b/docs/config.toml @@ -7,7 +7,7 @@ theme = 'hugo-book' # Book configuration disablePathToLower = true # Doesn't work with docs as subdir -enableGitInfo = false +enableGitInfo = true # Needed for mermaid/katex shortcodes [markup] @@ -29,7 +29,7 @@ enableGitInfo = false [menu] [[menu.after]] - name = "API Documentation" + name = "HTTP API Docs" url = "https://rustybever.be/docs/vieter/api/" weight = 10 [[menu.after]] @@ -37,8 +37,8 @@ enableGitInfo = false url = "https://rustybever.be/man/vieter/vieter.1.html" weight = 20 [[menu.after]] - name = "Source" - url = "https://git.rustybever.be/Chewing_Bever/docs" + name = "Git" + url = "https://git.rustybever.be/vieter/vieter" weight = 30 [[menu.after]] name = "Hugo Theme" @@ -70,14 +70,14 @@ enableGitInfo = false # Set source repository location. # Used for 'Last Modified' and 'Edit this page' links. - BookRepo = 'https://git.rustybever.be/vieter/vieter/docs' + BookRepo = 'https://git.rustybever.be/vieter/vieter' # (Optional, default 'commit') Specifies commit portion of the link to the page's last modified # commit hash for 'doc' page type. # Requires 'BookRepo' param. # Value used to construct a URL consisting of BookRepo/BookCommitPath/ # Github uses 'commit', Bitbucket uses 'commits' - # BookCommitPath = 'commit' + BookCommitPath = 'src/commit' # Enable "Edit this page" links for 'doc' page type. # Disabled by default. Uncomment to enable. Requires 'BookRepo' param. diff --git a/docs/content/installation.md b/docs/content/installation.md index 5cc7a0ee..2cf57e79 100644 --- a/docs/content/installation.md +++ b/docs/content/installation.md @@ -53,11 +53,26 @@ services: - 'VIETER_DEFAULT_ARCH=x86_64' - 'VIETER_MAX_CONCURRENT_BUILDS=2' - 'VIETER_GLOBAL_SCHEDULE=0 3' + volumes: + - '/var/run/docker.sock:/var/run/docker.sock' volumes: data: ``` +If you do not require the build system, the repository server can be used +independently as well. + +{{< hint info >}} +**Note** +Builds are executed on the cron daemon's system using the host's Docker daemon. +A cron daemon on a specific architecture will only build packages for that +specific architecture. Therefore, if you wish to build packages for both +`x86_64` & `aarch64`, you'll have to deploy two cron daemons, one on each +architecture. Afterwards, any Git repositories enabled for those two +architectures will build on both. +{{< /hint >}} + ## Binary On the diff --git a/docs/content/usage/_index.md b/docs/content/usage/_index.md new file mode 100644 index 00000000..bb0376fd --- /dev/null +++ b/docs/content/usage/_index.md @@ -0,0 +1,4 @@ +# Usage + +This section describes how to use the various parts of Vieter. It assumes you +have a Vieter repository server up and running. diff --git a/docs/content/usage/repository.md b/docs/content/usage/repository.md new file mode 100644 index 00000000..c30c8ca1 --- /dev/null +++ b/docs/content/usage/repository.md @@ -0,0 +1,38 @@ +# Pacman repository + +The part of Vieter that users will interact with the most is the Pacman +repository aka `vieter server`. + +## Design overview + +A Vieter repository server has support for multiple repositories, with each +repository containing packages for multiple architectures. + +If you wish to use these repositories on your system, add the following to +`/etc/pacman.conf` for each repository you wish to use: + +``` +[repo-name] +Server = https://example.com/$repo/$arch +SigLevel = Optional +``` + +Here, `$repo` & `$arch` are not variables you have to fill in yourself. Rather, +Pacman will substitute these when reading the config file. `$repo` is replaced +by the name between the square brackets (in this case `repo-name`), & `$arch` +is replaced by your system's architecture, e.g. `x86_64`. Of course, you can +also fill in these values manually yourself, e.g. if you wish to use a +different name inside the square brackets. + +Important to note is that, when two repositories contain a package with the +same name, Pacman will choose the one from the repository that's highest up in +the `pacman.conf` file. Therefore, if you know your repository has packages +with the same name as ones from the official repositories, it might be better +to place the repository below the official repositories to avoid overwriting +official packages. + +## Publishing packages + +Packages can be easily published using a single HTTP POST request. Check out +the [HTTP API docs](https://rustybever.be/docs/vieter/api/) for more info on +these routes, including example cURL commands. diff --git a/docs/content/usage.md b/docs/content/usage/usage.md similarity index 100% rename from docs/content/usage.md rename to docs/content/usage/usage.md From 0ab39a334db6f4b278608c28f6239a94df2aa358 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Thu, 9 Jun 2022 10:25:36 +0200 Subject: [PATCH 059/236] docs: started new builds usage page --- docs/content/other/_index.md | 3 ++ docs/content/other/builds-in-depth.md | 4 +++ docs/content/usage/_index.md | 7 ++--- docs/content/usage/builds.md | 40 +++++++++++++++++++++++++++ docs/content/usage/repository.md | 3 ++ 5 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 docs/content/other/_index.md create mode 100644 docs/content/other/builds-in-depth.md create mode 100644 docs/content/usage/builds.md diff --git a/docs/content/other/_index.md b/docs/content/other/_index.md new file mode 100644 index 00000000..394456b2 --- /dev/null +++ b/docs/content/other/_index.md @@ -0,0 +1,3 @@ +--- +weight: 100 +--- diff --git a/docs/content/other/builds-in-depth.md b/docs/content/other/builds-in-depth.md new file mode 100644 index 00000000..a6f0c21a --- /dev/null +++ b/docs/content/other/builds-in-depth.md @@ -0,0 +1,4 @@ +# Builds In-depth + +For those interested, this page describes how the build system works +internally. diff --git a/docs/content/usage/_index.md b/docs/content/usage/_index.md index bb0376fd..1518e5e3 100644 --- a/docs/content/usage/_index.md +++ b/docs/content/usage/_index.md @@ -1,4 +1,3 @@ -# Usage - -This section describes how to use the various parts of Vieter. It assumes you -have a Vieter repository server up and running. +--- +weight: 30 +--- diff --git a/docs/content/usage/builds.md b/docs/content/usage/builds.md new file mode 100644 index 00000000..db11bec9 --- /dev/null +++ b/docs/content/usage/builds.md @@ -0,0 +1,40 @@ +--- +weight: 20 +--- +# Building packages + +The automatic build system is what makes Vieter very useful as a replacement +for an AUR helper. It can perodically build packages & publish them to your +personal Vieter repository server, removing the need to build the packages +locally. + +## Adding builds + +Before the cron system can start building your package, you need to add its +info to the system. The Vieter repository server exposes an HTTP API for this +(see the [HTTP API Docs](https://rustybever.be/docs/vieter/api/) for more +info). For ease of use, the Vieter binary contains a CLI interface for +interacting with this API (see [Configuration](/configuration) for +configuration details). The [man +pages](https://rustybever.be/man/vieter/vieter-repos.1.html) describe this in +greater detail, but the basic usage is as follows: + +``` +vieter repos add some-url some-branch some-repository +``` + +Here, `some-url` is the URL of the Git repository containing the PKGBUILD. This +URL is passed to `git clone`, so the repository should be public. Vieter +expects the same format as an AUR Git repository, so you can directly use AUR +URLs here. + +`some-branch` is the branch of the Git repository the build should check out. +If you're using an AUR package, this should be `master`. + +Finally, `some-repo` is the repository to which the built package archives +should be published. + +The above command intentionally leaves out a few parameters to make the CLI +more useable. For information on how to modify all parameters using the CLI, +see +[vieter-repos-edit(1)](https://rustybever.be/man/vieter/vieter-repos-edit.1.html). diff --git a/docs/content/usage/repository.md b/docs/content/usage/repository.md index c30c8ca1..1dc6bc6b 100644 --- a/docs/content/usage/repository.md +++ b/docs/content/usage/repository.md @@ -1,3 +1,6 @@ +--- +weight: 10 +--- # Pacman repository The part of Vieter that users will interact with the most is the Pacman From a9ddfd8ec8d9d76ea2f45e15bec643538ed13c76 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Thu, 9 Jun 2022 16:26:53 +0200 Subject: [PATCH 060/236] docs: too many changes to count --- docs/config.toml | 2 +- docs/content/configuration.md | 78 ++++++++++++++----- .../usage/{builds.md => builds/_index.md} | 2 +- docs/content/usage/builds/schedule.md | 42 ++++++++++ docs/content/usage/repository.md | 12 +-- 5 files changed, 109 insertions(+), 27 deletions(-) rename docs/content/usage/{builds.md => builds/_index.md} (95%) create mode 100644 docs/content/usage/builds/schedule.md diff --git a/docs/config.toml b/docs/config.toml index d0eecb27..5c82044c 100644 --- a/docs/config.toml +++ b/docs/config.toml @@ -1,7 +1,7 @@ # hugo server --minify --themesDir ... --baseURL=http://0.0.0.0:1313/theme/hugo-book/ baseURL = 'https://rustybever.be/docs/vieter/' -title = 'The Rusty Bever - Docs' +title = 'Vieter - Docs' theme = 'hugo-book' # Book configuration diff --git a/docs/content/configuration.md b/docs/content/configuration.md index ded40cb7..27eb213b 100644 --- a/docs/content/configuration.md +++ b/docs/content/configuration.md @@ -3,7 +3,7 @@ weight: 20 --- # Configuration -All vieter operations by default try to read in the TOML file `~/.vieterrc` for +By default, all vieter commands try to read in the TOML file `~/.vieterrc` for configuration. The location of this file can be changed by using the `-f` flag. If the above file doesn't exist or you wish to override some of its settings, @@ -19,38 +19,78 @@ the value in the environment variable is used. {{< hint info >}} **Note** All environment variables can also be provided from a file by appending them -with `_FILE`. This for example allows you to provide the API key from a docker +with `_FILE`. This for example allows you to provide the API key from a Docker secrets file. {{< /hint >}} -## Modes +## Commands -The vieter binary can run in several "modes", indicated by the first argument -passed to them. Each mode requires a different configuration. +The first argument passed to Vieter determines which command you wish to use. +Each of these can contain subcommands (e.g. `vieter repos list`), but all +subcommands will use the same configuration. Below you can find the +configuration variable required for each command. -### Server +### `vieter server` -* `log_level`: defines how much logs to show. Valid values are one of `FATAL`, - `ERROR`, `WARN`, `INFO` or `DEBUG`. Defaults to `WARN` -* `log_file`: log file to write logs to. Defaults to `vieter.log` in the - current directory. +* `log_level`: log verbosity level. Value should be one of `FATAL`, `ERROR`, + `WARN`, `INFO` or `DEBUG`. + * Default: `WARN` +* `log_file`: log file to write logs to. + * Default: `vieter.log` (in the current directory) * `pkg_dir`: where Vieter should store the actual package archives. * `data_dir`: where Vieter stores the repositories, log file & database. * `api_key`: the API key to use when authenticating requests. -* `default_arch`: architecture to always add packages of arch `any` to. +* `default_arch`: this setting serves two main purposes: + * Packages with architecture `any` are always added to this architecture. + This prevents the server from being confused when an `any` package is + published as the very first package for a repository. + * Git repositories added without an `arch` value use this value instead. -### Builder + +### `vieter repos` * `api_key`: the API key to use when authenticating requests. * `address`: Base your URL of your Vieter instance, e.g. https://example.com -* `base_image`: image to use when building a package. It should be an Archlinux - image. The default if not configured is `archlinux:base-devel`, but this - image only supports arm64. If you require aarch64 support as well, consider - using - [`menci/archlinuxarm:base-devel`](https://hub.docker.com/r/menci/archlinuxarm) - ([GH](https://github.com/Menci/docker-archlinuxarm)) +* `base_image`: image to use when building a package using `vieter repos + build`. + * Default: `archlinux:base-devel` -### Repos +### `vieter cron` + +* `log_level`: log verbosity level. Value should be one of `FATAL`, `ERROR`, + `WARN`, `INFO` or `DEBUG`. + * Default: `WARN` +* `log_file`: log file to write logs to. + * Default: `vieter.log` (in `data_dir`) +* `address`: *public* URL of the Vieter repository server to build for. From + this server the list of Git repositories is retrieved. All built packages are + published to this server. +* `api_key`: API key of the above server. +* `data_dir`: directory to store log file in. +* `base_image`: Docker image to use when building a package. Any Pacman-based + distro image should work, as long as `/etc/pacman.conf` is used & + `base-devel` exists in the repositories. Make sure that the image supports + the architecture of your cron daemon. + * Default: `archlinux:base-devel` (only works on `x86_64`). If you require + `aarch64` support, consider using + [`menci/archlinuxarm:base-devel`](https://hub.docker.com/r/menci/archlinuxarm) + ([GitHub](https://github.com/Menci/docker-archlinuxarm)). This is the image + used for the Vieter CI builds. +* `max_concurrent_builds`: how many builds to run at the same time. + * Default: `1` +* `api_update_frequency`: how frequently (in minutes) to poll the Vieter + repository server for a new list of Git repositories to build. + * Default: `15` +* `image_rebuild_frequency`: Vieter periodically builds a builder image using + the configured base image. This makes sure build containers do not have to + download a lot of packages when updating their system. This setting defines + how frequently (in minutes) to rebuild this builder image. + * Default: `1440` (every 24 hours) +* `global_schedule`: build schedule for any Git repository that does not have a + schedule defined. + * Default: `0 3` (3AM every night) + +### `vieter logs` * `api_key`: the API key to use when authenticating requests. * `address`: Base your URL of your Vieter instance, e.g. https://example.com diff --git a/docs/content/usage/builds.md b/docs/content/usage/builds/_index.md similarity index 95% rename from docs/content/usage/builds.md rename to docs/content/usage/builds/_index.md index db11bec9..c6f06030 100644 --- a/docs/content/usage/builds.md +++ b/docs/content/usage/builds/_index.md @@ -24,7 +24,7 @@ vieter repos add some-url some-branch some-repository ``` Here, `some-url` is the URL of the Git repository containing the PKGBUILD. This -URL is passed to `git clone`, so the repository should be public. Vieter +URL is passed to `git clone`, meaning the repository should be public. Vieter expects the same format as an AUR Git repository, so you can directly use AUR URLs here. diff --git a/docs/content/usage/builds/schedule.md b/docs/content/usage/builds/schedule.md new file mode 100644 index 00000000..38f76a40 --- /dev/null +++ b/docs/content/usage/builds/schedule.md @@ -0,0 +1,42 @@ +# Cron schedule syntax + +The Vieter cron daemon uses a subset of the cron expression syntax to schedule +builds. + +## Format + +`a b c d` + +* `a`: minutes +* `b`: hours +* `c`: days +* `d`: months + +An expression consists of two to four sections. If less than four sections are +provided, the parser will append `*` until there are four sections. This means +that `0 3` is the same as `0 3 * *`. + +Each section consists of one or more parts, separated by a comma. Each of these +parts, in turn, can be one of the following (any letters are integers): + +* `*`: allow all possible values. +* `a`: only this value is allowed. +* `*/n`: allow every n-th value. +* `a/n`: allow every n-th value, starting at a in the list. +* `a-b`: allow every value between a and b, bounds included. +* `a-b/n`: allow every n-th value inside the list of values between a and b, + bounds included. + +Each section can consist of as many of these parts as necessary. + +## Examples + +* `0 3`: every day at 03:00AM. +* `0 0 */7`: every 7th day of the month, at midnight. + +## CLI tool + +The Vieter binary contains a command that shows you the next matching times for +a given expression. This can be useful to understand the syntax. For more +information, see +[vieter-schedule(1)](https://rustybever.be/man/vieter/vieter-schedule.1.html). diff --git a/docs/content/usage/repository.md b/docs/content/usage/repository.md index 1dc6bc6b..3ddd2fc1 100644 --- a/docs/content/usage/repository.md +++ b/docs/content/usage/repository.md @@ -20,12 +20,12 @@ Server = https://example.com/$repo/$arch SigLevel = Optional ``` -Here, `$repo` & `$arch` are not variables you have to fill in yourself. Rather, -Pacman will substitute these when reading the config file. `$repo` is replaced -by the name between the square brackets (in this case `repo-name`), & `$arch` -is replaced by your system's architecture, e.g. `x86_64`. Of course, you can -also fill in these values manually yourself, e.g. if you wish to use a -different name inside the square brackets. +Here, `$repo` and `$arch` are not variables you have to fill in yourself. +Rather, Pacman will substitute these when reading the config file. `$repo` is +replaced by the name between the square brackets (in this case `repo-name`), +and `$arch` is replaced by your system's architecture, e.g. `x86_64`. Of +course, you can also fill in these values manually yourself, e.g. if you wish +to use a different name inside the square brackets. Important to note is that, when two repositories contain a package with the same name, Pacman will choose the one from the repository that's highest up in From c341d7a0243526a215d6698d75479d2c1bb20574 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Thu, 9 Jun 2022 17:24:16 +0200 Subject: [PATCH 061/236] docs: some changed; removed some old files --- docs/api/source/includes/_git.md | 2 +- docs/content/CLI.md | 27 --------------- docs/content/configuration.md | 29 +++++----------- docs/content/usage/builds/_index.md | 11 ++++++ docs/content/usage/usage.md | 54 ----------------------------- 5 files changed, 20 insertions(+), 103 deletions(-) delete mode 100644 docs/content/CLI.md delete mode 100644 docs/content/usage/usage.md diff --git a/docs/api/source/includes/_git.md b/docs/api/source/includes/_git.md index f41d28d5..8458834e 100644 --- a/docs/api/source/includes/_git.md +++ b/docs/api/source/includes/_git.md @@ -112,7 +112,7 @@ Parameter | Description url | URL of the Git repository. branch | Branch of the Git repository. repo | Vieter repository to publish built packages to. -schedule | Cron build schedule +schedule | Cron build schedule (syntax explained [here](https://rustybever.be/docs/vieter/usage/builds/schedule/)) arch | Comma-separated list of architectures to build package on. ## Modify a repo diff --git a/docs/content/CLI.md b/docs/content/CLI.md deleted file mode 100644 index 32bb6f86..00000000 --- a/docs/content/CLI.md +++ /dev/null @@ -1,27 +0,0 @@ -# Vieter CLI - -I provide a simple CLI tool that currently only allows changing the Git -repository API. Its usage is quite simple. - -First, you need to create a file in your home directory called `.vieterrc` with -the following content: - -```toml -address = "https://example.com" -api_key = "your-api-key" -``` - -You can also use a different file or use environment variables, as described in -[Configuration](/configuration). - -Now you're ready to use the CLI tool. - -## Usage - -* `vieter repos list` returns all repositories currently stored in the API. -* `vieter repos add url branch repo arch...` adds the repository with the given - URL, branch, repo & arch to the API. -* `vieter repos remove id` removes the repository with the given ID prefix. - -You can always check `vieter -help` or `vieter repos -help` for more -information about the commands. diff --git a/docs/content/configuration.md b/docs/content/configuration.md index 27eb213b..135c1ff1 100644 --- a/docs/content/configuration.md +++ b/docs/content/configuration.md @@ -47,14 +47,6 @@ configuration variable required for each command. * Git repositories added without an `arch` value use this value instead. -### `vieter repos` - -* `api_key`: the API key to use when authenticating requests. -* `address`: Base your URL of your Vieter instance, e.g. https://example.com -* `base_image`: image to use when building a package using `vieter repos - build`. - * Default: `archlinux:base-devel` - ### `vieter cron` * `log_level`: log verbosity level. Value should be one of `FATAL`, `ERROR`, @@ -87,7 +79,8 @@ configuration variable required for each command. how frequently (in minutes) to rebuild this builder image. * Default: `1440` (every 24 hours) * `global_schedule`: build schedule for any Git repository that does not have a - schedule defined. + schedule defined. For information about this syntax, see + [here](/usage/builds/schedule). * Default: `0 3` (3AM every night) ### `vieter logs` @@ -95,17 +88,11 @@ configuration variable required for each command. * `api_key`: the API key to use when authenticating requests. * `address`: Base your URL of your Vieter instance, e.g. https://example.com -### Cron +### `vieter repos` -* `log_level`: defines how much logs to show. Valid values are one of `FATAL`, - `ERROR`, `WARN`, `INFO` or `DEBUG`. Defaults to `WARN` * `api_key`: the API key to use when authenticating requests. -* `address`: Base your URL of your Vieter instance, e.g. https://example.com. - This *must* be the publicly facing URL of your Vieter instance. -* `data_dir`: where Vieter stores the log file. -* `base_image`: Docker image from which to create the builder images. -* `max_concurrent_builds`: amount of builds to run at once. -* `api_update_frequency`: how frequenty to check for changes in the repo list. -* `image_rebuild+frequency`: how frequently to rebuild the builder image -* `global_schedule`: cron schedule to use for any repo without an individual - schedule +* `address`: Base your URL of your Vieter instance, e.g. https://example.com +* `base_image`: image to use when building a package using `vieter repos + build`. + * Default: `archlinux:base-devel` + diff --git a/docs/content/usage/builds/_index.md b/docs/content/usage/builds/_index.md index c6f06030..cd463a4b 100644 --- a/docs/content/usage/builds/_index.md +++ b/docs/content/usage/builds/_index.md @@ -38,3 +38,14 @@ The above command intentionally leaves out a few parameters to make the CLI more useable. For information on how to modify all parameters using the CLI, see [vieter-repos-edit(1)](https://rustybever.be/man/vieter/vieter-repos-edit.1.html). + +## Reading logs + +The logs of each build are uploaded to the Vieter repository server, along with +information about the exit code of the build container, when the build +started/ended etc. These logs can then be accessed using the [HTTP +API](https://rustybever.be/docs/vieter/api/). + +For ease of use, the logs are also available using some CLI commands; see +[vieter-logs(1)](https://rustybever.be/man/vieter/vieter-logs.1.html) for more +information. diff --git a/docs/content/usage/usage.md b/docs/content/usage/usage.md deleted file mode 100644 index 06671b4c..00000000 --- a/docs/content/usage/usage.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -weight: 30 ---- -# Usage - -## Starting the server - -To start a server, either install it using Docker (see -[Installation](/installation)) or run it locally by executing `vieter -server`. See [Configuration](/configuration) for more information about -configuring the binary. - -## Multiple repositories - -Vieter works with multiple repositories. This means that a single Vieter server -can serve multiple repositories in Pacman. It also automatically divides files -with specific architectures among arch-repos. Arch-repos are the actual -repositories you add to your `/etc/pacman.conf` file. See [Configuring -Pacman](/usage#configuring-pacman) below for more info. - -## Adding packages - -Using Vieter is currently very simple. If you wish to add a package to Vieter, -build it using makepkg & POST that file to the `//publish` endpoint of -your server. This will add the package to the repository. Authentification -requires you to add the API key as the `X-Api-Key` header. - -All of this can be combined into a simple cURL call: - -``` -curl -XPOST -H "X-API-KEY: your-key" -T some-package.pkg.tar.zst https://example.com/somerepo/publish -``` - -`somerepo` is automatically created if it doesn't exist yet. - -## Configuring Pacman - -Configuring Pacman to use a Vieter instance is very simple. In your -`/etc/pacman.conf` file, add the following lines: - -``` -[vieter] -Server = https://example.com/$repo/$arch -SigLevel = Optional -``` - -Here, you see two important placeholder variables. `$repo` is replaced by the -name within the square brackets, which in this case would be `vieter`. `$arch` -is replaced by the output of `uname -m`. Because Vieter supports multiple -repositories & architectures per repository, using this notation makes sure you -always use the correct endpoint for fetching files. - -I recommend placing this below all other repository entries, as the order -decides which repository should be used if there's ever a naming conflict. From c15f4a482faa31d2746731a3ce2caafbfdd55bed Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Thu, 9 Jun 2022 21:08:13 +0200 Subject: [PATCH 062/236] docs: added in-depth build explanation --- docs/config.toml | 2 +- docs/content/other/builds-in-depth.md | 77 +++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/docs/config.toml b/docs/config.toml index 5c82044c..f8e23cdc 100644 --- a/docs/config.toml +++ b/docs/config.toml @@ -37,7 +37,7 @@ enableGitInfo = true url = "https://rustybever.be/man/vieter/vieter.1.html" weight = 20 [[menu.after]] - name = "Git" + name = "Vieter" url = "https://git.rustybever.be/vieter/vieter" weight = 30 [[menu.after]] diff --git a/docs/content/other/builds-in-depth.md b/docs/content/other/builds-in-depth.md index a6f0c21a..a04acc4e 100644 --- a/docs/content/other/builds-in-depth.md +++ b/docs/content/other/builds-in-depth.md @@ -2,3 +2,80 @@ For those interested, this page describes how the build system works internally. + +## Builder image + +Every cron daemon perodically creates a builder image that is then used as a +base for all builds. This is done to prevent build containers having to pull +down a bunch of updates when they update their system. + +The build container is created by running the following commands inside a +container started from the image defined in `base_image`: + +```sh +# Update repos & install required packages +pacman -Syu --needed --noconfirm base-devel git +# Add a non-root user to run makepkg +groupadd -g 1000 builder +useradd -mg builder builder +# Make sure they can use sudo without a password +echo 'builder ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers +# Create the directory for the builds & make it writeable for the +# build user +mkdir /build +chown -R builder:builder /build +``` + +This script updates the packages to their latest versions & creates a non-root +user to use when running `makepkg`. + +This script is base64-encoded & passed to the container as an environment +variable. The container's entrypoint is set to `/bin/sh -c` & its command +argument to `echo $BUILD_SCRIPT | base64 -d | /bin/sh -e`, with the +`BUILD_SCRIPT` environment variable containing the base64-encoded script. + +Once the container exits, a new Docker image is created from it. This image is +then used as the base for any builds. + +## Running builds + +Each build has its own Docker container, using the builder image as its base. +The same base64-based technique as above is used, just with a different script. +To make the build logs more clear, each command is appended by an echo command +printing the next command to stdout. + +Given the Git repository URL is `https://examplerepo.com` with branch `main`, +the URL of the Vieter server is `https://example.com` and `vieter` is the +repository we wish to publish to, we get the following script: + +```sh +echo -e '+ echo -e '\''[vieter]\\nServer = https://example.com/$repo/$arch\\nSigLevel = Optional'\'' >> /etc/pacman.conf' +echo -e '[vieter]\nServer = https://example.com/$repo/$arch\nSigLevel = Optional' >> /etc/pacman.conf +echo -e '+ pacman -Syu --needed --noconfirm' +pacman -Syu --needed --noconfirm +echo -e '+ su builder' +su builder +echo -e '+ git clone --single-branch --depth 1 --branch main https://examplerepo.com repo' +git clone --single-branch --depth 1 --branch main https://examplerepo.com repo +echo -e '+ cd repo' +cd repo +echo -e '+ makepkg --nobuild --syncdeps --needed --noconfirm' +makepkg --nobuild --syncdeps --needed --noconfirm +echo -e '+ source PKGBUILD' +source PKGBUILD +echo -e '+ curl -s --head --fail https://example.com/vieter/x86_64/$pkgname-$pkgver-$pkgrel && exit 0' +curl -s --head --fail https://example.com/vieter/x86_64/$pkgname-$pkgver-$pkgrel && exit 0 +echo -e '+ [ "$(id -u)" == 0 ] && exit 0' +[ "$(id -u)" == 0 ] && exit 0 +echo -e '+ MAKEFLAGS="-j$(nproc)" makepkg -s --noconfirm --needed && for pkg in $(ls -1 *.pkg*); do curl -XPOST -T "$pkg" -H "X-API-KEY: $API_KEY" https://example.com/vieter/publish; done' +MAKEFLAGS="-j$(nproc)" makepkg -s --noconfirm --needed && for pkg in $(ls -1 *.pkg*); do curl -XPOST -T "$pkg" -H "X-API-KEY: $API_KEY" https://example.com/vieter/publish; done +``` + +This script: + +1. Adds the target repository as a repository in the build container +2. Updates mirrors & packages +3. Clones the Git repository +4. Runs `makepkg` without building to calculate `pkgver` +5. Checks whether the package version is already present on the server +6. If not, run makepkg & publish any generated package archives to the server From ddccceb336aa0bb5b16b41baa1935e5b54323d18 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Thu, 9 Jun 2022 21:46:39 +0200 Subject: [PATCH 063/236] docs: removed another old file & read over some parts --- docs/content/builder.md | 56 --------------------------- docs/content/configuration.md | 4 +- docs/content/installation.md | 4 +- docs/content/other/builds-in-depth.md | 2 +- 4 files changed, 5 insertions(+), 61 deletions(-) delete mode 100644 docs/content/builder.md diff --git a/docs/content/builder.md b/docs/content/builder.md deleted file mode 100644 index 659717df..00000000 --- a/docs/content/builder.md +++ /dev/null @@ -1,56 +0,0 @@ -# Builder - -Vieter supports a basic build system that allows you to build the packages -defined using the Git repositories API by running `vieter build`. For -configuration, see [here](/configuration#builder). - -## How it works - -The build system works in two stages. First it pulls down the -`archlinux:latest` image from Docker Hub, runs `pacman -Syu` & configures a -non-root build user. It then creates a new Docker image from this container. -This is to prevent each build having to fully update the container's -repositories. After the image has been created, each repository returned by -`/api/repos` is built sequentially by starting up a new container with the -previously created image as a base. Each container goes through the following steps: - -1. The repository is cloned -2. `makepkg --nobuild --syncdeps --needed --noconfirm` is ran to update the `pkgver` variable inside - the `PKGBUILD` file -3. A HEAD request is sent to the Vieter server to check whether the specific - version of the package is already present. If it is, the container exits. -4. `makepkg` is ran with `MAKEFLAGS="-j\$(nproc)` -5. Each produced package archive is uploaded to the Vieter instance's - repository, as defined in the API for that specific Git repo. - -## Cron image - -The Vieter Docker image contains crond & a cron config that runs `vieter build` -every night at 3AM. This value is currently hardcoded, but I wish to change -that down the line (work is in progress). There's also some other caveats you -should be aware of, namely that the image should be run as root & that the -healthcheck will always fail, so you might have to disable it. This boils down -to the following docker-compose file: - -```yaml -version: '3' - -services: - cron: - image: 'chewingbever/vieter:dev' - command: crond -f - user: root - - healthcheck: - disable: true - - environment: - - 'VIETER_API_KEY=some-key' - - 'VIETER_ADDRESS=https://example.com' - volumes: - - '/var/run/docker.sock:/var/run/docker.sock' -``` - -Important to note is that the container also requires the host's Docker socket -to be mounted as this is how it spawns the necessary containers, as well as a -change to the container's command. diff --git a/docs/content/configuration.md b/docs/content/configuration.md index 135c1ff1..600c6f35 100644 --- a/docs/content/configuration.md +++ b/docs/content/configuration.md @@ -86,12 +86,12 @@ configuration variable required for each command. ### `vieter logs` * `api_key`: the API key to use when authenticating requests. -* `address`: Base your URL of your Vieter instance, e.g. https://example.com +* `address`: Base URL of your Vieter instance, e.g. https://example.com ### `vieter repos` * `api_key`: the API key to use when authenticating requests. -* `address`: Base your URL of your Vieter instance, e.g. https://example.com +* `address`: Base URL of your Vieter instance, e.g. https://example.com * `base_image`: image to use when building a package using `vieter repos build`. * Default: `archlinux:base-devel` diff --git a/docs/content/installation.md b/docs/content/installation.md index 2cf57e79..b5bdbaf0 100644 --- a/docs/content/installation.md +++ b/docs/content/installation.md @@ -20,8 +20,8 @@ The latter is updated every time a new commit is pushed to the development branch. This branch will be the most up to date, but does not give any guarantees about stability, so beware! -Due to the single-binary design of Vieter, this image can be used both for the -repository server & the cron daemon. +Thanks to the single-binary design of Vieter, this image can be used both for +the repository server & the cron daemon. Below is an example compose file to set up both the repository server & the cron daemon: diff --git a/docs/content/other/builds-in-depth.md b/docs/content/other/builds-in-depth.md index a04acc4e..d8df6ec7 100644 --- a/docs/content/other/builds-in-depth.md +++ b/docs/content/other/builds-in-depth.md @@ -78,4 +78,4 @@ This script: 3. Clones the Git repository 4. Runs `makepkg` without building to calculate `pkgver` 5. Checks whether the package version is already present on the server -6. If not, run makepkg & publish any generated package archives to the server +6. If not, run `makepkg` & publish any generated package archives to the server From f4b2109533c03e046e09b3710a31c2cc335b031d Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Fri, 10 Jun 2022 08:39:10 +0200 Subject: [PATCH 064/236] fix(ci): release checksum files should now use correct paths --- .woodpecker/.gitea.yml | 4 ++-- CHANGELOG.md | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.woodpecker/.gitea.yml b/.woodpecker/.gitea.yml index 18770502..d0825c2c 100644 --- a/.woodpecker/.gitea.yml +++ b/.woodpecker/.gitea.yml @@ -14,6 +14,7 @@ pipeline: commands: - mc alias set s3/ https://s3.rustybever.be "$S3_USERNAME" "$S3_PASSWORD" - mc cp -r "s3/vieter/commits/$CI_COMMIT_SHA" . + - mv "$CI_COMMIT_SHA"/vieter-* . when: event: tag @@ -23,9 +24,8 @@ pipeline: - gitea_release_api_key settings: base_url: https://git.rustybever.be - files: ${CI_COMMIT_SHA}/* + files: vieter-* checksum: - - md5 - sha256 title: ${CI_COMMIT_TAG} when: diff --git a/CHANGELOG.md b/CHANGELOG.md index a46a4335..ba2dc163 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 updated builder image * Build logs now show commands being executed +### Fixed + +* `POST /api/logs` now correctly uses epoch timestamps instead of strings + ## [0.3.0-alpha.2](https://git.rustybever.be/vieter/vieter/src/tag/0.3.0-alpha.2) ### Added From 0855d9efd82ad0a3b7202f37c1ab80f0c139faea Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Fri, 10 Jun 2022 08:40:28 +0200 Subject: [PATCH 065/236] fix(console): removed unimplemented -arch flag --- src/console/git/git.v | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/console/git/git.v b/src/console/git/git.v index b01f3dd7..e27c1c4e 100644 --- a/src/console/git/git.v +++ b/src/console/git/git.v @@ -38,11 +38,6 @@ pub fn cmd() cli.Command { description: 'Only return Git repos that publish to this repo.' flag: cli.FlagType.string }, - cli.Flag{ - name: 'arch' - description: 'Only return repos enabled for this architecture.' - flag: cli.FlagType.string - }, ] execute: fn (cmd cli.Command) ? { config_file := cmd.flags.get_string('config-file')? From ec128539d24ba74da1de752799e85dde479feb26 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Fri, 10 Jun 2022 15:09:55 +0200 Subject: [PATCH 066/236] chore: bumped version to 0.3.0-rc.1 --- CHANGELOG.md | 2 ++ PKGBUILD | 2 +- src/main.v | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba2dc163..4a7574f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://git.rustybever.be/vieter/vieter/src/branch/dev) +## [0.3.0-rc.1](https://git.rustybever.be/vieter/vieter/src/tag/0.3.0-rc.1) + ### Added * Database migrations diff --git a/PKGBUILD b/PKGBUILD index 656d9d47..f2fc9a68 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -3,7 +3,7 @@ pkgbase='vieter' pkgname='vieter' -pkgver='0.3.0_alpha.2' +pkgver='0.3.0_rc.1' pkgrel=1 pkgdesc="Vieter is a lightweight implementation of an Arch repository server." depends=('glibc' 'openssl' 'libarchive' 'sqlite') diff --git a/src/main.v b/src/main.v index 80c19556..3da1826b 100644 --- a/src/main.v +++ b/src/main.v @@ -13,7 +13,7 @@ fn main() { mut app := cli.Command{ name: 'vieter' description: 'Vieter is a lightweight implementation of an Arch repository server.' - version: '0.3.0-alpha.2' + version: '0.3.0-rc.1' flags: [ cli.Flag{ flag: cli.FlagType.string From 12805d713cb9a2e100f2334acf1e657fcf39fe06 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Mon, 13 Jun 2022 21:32:35 +0200 Subject: [PATCH 067/236] chore: bumped versions to 0.3.0 --- CHANGELOG.md | 4 ++++ PKGBUILD | 2 +- src/main.v | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a7574f2..99a795a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://git.rustybever.be/vieter/vieter/src/branch/dev) +## [0.3.0](https://git.rustybever.be/vieter/vieter/src/tag/0.3.0) + +Nothing besides bumping the versions. + ## [0.3.0-rc.1](https://git.rustybever.be/vieter/vieter/src/tag/0.3.0-rc.1) ### Added diff --git a/PKGBUILD b/PKGBUILD index f2fc9a68..639ce95c 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -3,7 +3,7 @@ pkgbase='vieter' pkgname='vieter' -pkgver='0.3.0_rc.1' +pkgver='0.3.0' pkgrel=1 pkgdesc="Vieter is a lightweight implementation of an Arch repository server." depends=('glibc' 'openssl' 'libarchive' 'sqlite') diff --git a/src/main.v b/src/main.v index 3da1826b..e1979de9 100644 --- a/src/main.v +++ b/src/main.v @@ -13,7 +13,7 @@ fn main() { mut app := cli.Command{ name: 'vieter' description: 'Vieter is a lightweight implementation of an Arch repository server.' - version: '0.3.0-rc.1' + version: '0.3.0' flags: [ cli.Flag{ flag: cli.FlagType.string From 44696fc11b200aa903641f852282f32a96de117b Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Wed, 15 Jun 2022 13:20:29 +0200 Subject: [PATCH 068/236] refactor: migrated env code to own external module --- README.md | 4 ++ src/console/git/git.v | 14 +++--- src/console/logs/logs.v | 8 ++-- src/cron/cli.v | 4 +- src/env/README.md | 7 --- src/env/env.v | 102 ---------------------------------------- src/server/cli.v | 4 +- src/v.mod | 5 ++ 8 files changed, 24 insertions(+), 124 deletions(-) delete mode 100644 src/env/README.md delete mode 100644 src/env/env.v diff --git a/README.md b/README.md index 892396a2..31f262e1 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,10 @@ Besides a V installer, Vieter also requires the following libraries to work: * openssl * sqlite3 +Vieter also depends on some external V modules which you can install using `cd +src && v install`. Make sure to keep these dependencies up to date using `v +update`. + ### Compiler Vieter compiles with the standard Vlang compiler. However, I do maintain a diff --git a/src/console/git/git.v b/src/console/git/git.v index e27c1c4e..303909e4 100644 --- a/src/console/git/git.v +++ b/src/console/git/git.v @@ -1,7 +1,7 @@ module git import cli -import env +import vieter.vconf import cron.expression { parse_expression } import client import console @@ -41,7 +41,7 @@ pub fn cmd() cli.Command { ] execute: fn (cmd cli.Command) ? { config_file := cmd.flags.get_string('config-file')? - conf := env.load(config_file)? + conf := vconf.load(default_path: config_file)? mut filter := GitRepoFilter{} @@ -70,7 +70,7 @@ pub fn cmd() cli.Command { description: 'Add a new repository.' execute: fn (cmd cli.Command) ? { config_file := cmd.flags.get_string('config-file')? - conf := env.load(config_file)? + conf := vconf.load(default_path: config_file)? add(conf, cmd.args[0], cmd.args[1], cmd.args[2])? } @@ -82,7 +82,7 @@ pub fn cmd() cli.Command { 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)? + conf := vconf.load(default_path: config_file)? remove(conf, cmd.args[0])? } @@ -94,7 +94,7 @@ pub fn cmd() cli.Command { description: 'Show detailed information for the repo matching the ID prefix.' execute: fn (cmd cli.Command) ? { config_file := cmd.flags.get_string('config-file')? - conf := env.load(config_file)? + conf := vconf.load(default_path: config_file)? info(conf, cmd.args[0])? } @@ -133,7 +133,7 @@ pub fn cmd() cli.Command { ] execute: fn (cmd cli.Command) ? { config_file := cmd.flags.get_string('config-file')? - conf := env.load(config_file)? + conf := vconf.load(default_path: config_file)? found := cmd.flags.get_all_found() @@ -155,7 +155,7 @@ pub fn cmd() cli.Command { description: 'Build the repo with the given id & publish it.' execute: fn (cmd cli.Command) ? { config_file := cmd.flags.get_string('config-file')? - conf := env.load(config_file)? + conf := vconf.load(default_path: config_file)? build(conf, cmd.args[0].int())? } diff --git a/src/console/logs/logs.v b/src/console/logs/logs.v index cb6997fc..d63e284a 100644 --- a/src/console/logs/logs.v +++ b/src/console/logs/logs.v @@ -1,7 +1,7 @@ module logs import cli -import env +import vieter.vconf import client import console import time @@ -65,7 +65,7 @@ pub fn cmd() cli.Command { ] execute: fn (cmd cli.Command) ? { config_file := cmd.flags.get_string('config-file')? - conf := env.load(config_file)? + conf := vconf.load(default_path: config_file)? mut filter := BuildLogFilter{} @@ -143,7 +143,7 @@ pub fn cmd() cli.Command { description: 'Show all info for a specific build log.' execute: fn (cmd cli.Command) ? { config_file := cmd.flags.get_string('config-file')? - conf := env.load(config_file)? + conf := vconf.load(default_path: config_file)? id := cmd.args[0].int() info(conf, id)? @@ -156,7 +156,7 @@ pub fn cmd() cli.Command { description: 'Output the content of a build log to stdout.' execute: fn (cmd cli.Command) ? { config_file := cmd.flags.get_string('config-file')? - conf := env.load(config_file)? + conf := vconf.load(default_path: config_file)? id := cmd.args[0].int() content(conf, id)? diff --git a/src/cron/cli.v b/src/cron/cli.v index 9703c663..10a4bdd9 100644 --- a/src/cron/cli.v +++ b/src/cron/cli.v @@ -1,7 +1,7 @@ module cron import cli -import env +import vieter.vconf struct Config { pub: @@ -24,7 +24,7 @@ pub fn cmd() cli.Command { description: 'Start the cron service that periodically runs builds.' execute: fn (cmd cli.Command) ? { config_file := cmd.flags.get_string('config-file')? - conf := env.load(config_file)? + conf := vconf.load(default_path: config_file)? cron(conf)? } diff --git a/src/env/README.md b/src/env/README.md deleted file mode 100644 index 135e8fa9..00000000 --- a/src/env/README.md +++ /dev/null @@ -1,7 +0,0 @@ -This module provides a framework for parsing a configuration, defined as a -struct, from both a TOML configuration file & environment variables. Some -notable features are: - -* Overwrite values in config file using environment variables -* Allow default values in config struct -* Read environment variable value from file diff --git a/src/env/env.v b/src/env/env.v deleted file mode 100644 index 5ed19552..00000000 --- a/src/env/env.v +++ /dev/null @@ -1,102 +0,0 @@ -module env - -import os -import toml - -const ( - // The prefix that every environment variable should have - prefix = 'VIETER_' - // The suffix an environment variable in order for it to be loaded from a file - // instead - file_suffix = '_FILE' -) - -// get_env_var tries to read the contents of the given environment variable. It -// looks for either `${env.prefix}${field_name.to_upper()}` or -// `${env.prefix}${field_name.to_upper()}${env.file_suffix}`, returning the -// contents of the file instead if the latter. If both or neither exist, the -// function returns an error. -fn get_env_var(field_name string) ?string { - 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) - - // If both are missing, we return an empty string - if env_var == '' && env_file == '' { - return '' - } - - // If they're both set, we report a conflict - if env_var != '' && env_file != '' { - 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. - // 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 - } - - // Otherwise, we process the file - return os.read_file(env_file) or { - error('Failed to read file defined in $env_file_name: ${err.msg()}.') - } -} - -// load attempts to create an object of type T from the given path to a toml -// file & environment variables. For each field, it will select either a value -// given from an environment variable, a value defined in the config file or a -// configured default if present, in that order. -pub fn load(path string) ?T { - mut res := T{} - - if os.exists(path) { - // We don't use reflect here because reflect also sets any fields not - // in the toml back to their zero value, which we don't want - doc := toml.parse_file(path)? - - $for field in T.fields { - s := doc.value(field.name) - - if s !is toml.Null { - $if field.typ is string { - res.$(field.name) = s.string() - } $else $if field.typ is int { - res.$(field.name) = s.int() - } - } - } - } - - $for field in T.fields { - env_value := get_env_var(field.name)? - - // The value of an env var will always take precedence over the toml - // file. - if env_value != '' { - $if field.typ is string { - res.$(field.name) = env_value - } $else $if field.typ is int { - res.$(field.name) = env_value.int() - } - } - - // Now, we check whether a value is present. If there isn't, that means - // it isn't in the config file, nor is there a default or an env var. - mut has_value := false - - $if field.typ is string { - has_value = res.$(field.name) != '' - } $else $if field.typ is int { - has_value = res.$(field.name) != 0 - } - - if !has_value { - return error("Missing config variable '$field.name' with no provided default. Either add it to the config file or provide it using an environment variable.") - } - } - return res -} diff --git a/src/server/cli.v b/src/server/cli.v index 556efcfe..cf0306cb 100644 --- a/src/server/cli.v +++ b/src/server/cli.v @@ -1,7 +1,7 @@ module server import cli -import env +import vieter.vconf struct Config { pub: @@ -19,7 +19,7 @@ pub fn cmd() cli.Command { description: 'Start the Vieter server.' execute: fn (cmd cli.Command) ? { config_file := cmd.flags.get_string('config-file')? - conf := env.load(config_file)? + conf := vconf.load(default_path: config_file)? server(conf)? } diff --git a/src/v.mod b/src/v.mod index e69de29b..7f459174 100644 --- a/src/v.mod +++ b/src/v.mod @@ -0,0 +1,5 @@ +Module { + dependencies: [ + 'https://git.rustybever.be/vieter/vconf' + ] +} From 592241c743d4f7f7c9ec065f422a3ccf5e2068d4 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Wed, 15 Jun 2022 17:47:12 +0200 Subject: [PATCH 069/236] chore: updated CI config & PKGBUILDs to new module split --- .woodpecker/.build.yml | 13 +++++++++++-- PKGBUILD | 8 ++++++++ PKGBUILD.dev | 8 ++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/.woodpecker/.build.yml b/.woodpecker/.build.yml index b0fd267e..d073b6c9 100644 --- a/.woodpecker/.build.yml +++ b/.woodpecker/.build.yml @@ -6,10 +6,19 @@ matrix: platform: ${PLATFORM} pipeline: - debug: + install-modules: image: 'chewingbever/vlang:latest' pull: true commands: + - export VMODULES=$PWD/.vmodules + - 'cd src && v install' + when: + event: [pull_request] + + debug: + image: 'chewingbever/vlang:latest' + commands: + - export VMODULES=$PWD/.vmodules - make when: event: [pull_request] @@ -18,10 +27,10 @@ pipeline: prod: image: 'chewingbever/vlang:latest' - pull: true environment: - LDFLAGS=-lz -lbz2 -llzma -lexpat -lzstd -llz4 -lsqlite3 -static commands: + - export VMODULES=$PWD/.vmodules # Apparently this -D is *very* important - CFLAGS='-DGC_THREADS=1' make prod # Make sure the binary is actually statically built diff --git a/PKGBUILD b/PKGBUILD index 639ce95c..33faec55 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -14,7 +14,15 @@ license=('AGPL3') source=("$pkgname::git+https://git.rustybever.be/vieter/vieter#tag=${pkgver//_/-}") md5sums=('SKIP') +prepare() { + export VMODULES="$srcdir/.vmodules" + + cd "$pkgname/src" && v install +} + build() { + export VMODULES="$srcdir/.vmodules" + cd "$pkgname" make prod diff --git a/PKGBUILD.dev b/PKGBUILD.dev index d25abb34..bff1459e 100644 --- a/PKGBUILD.dev +++ b/PKGBUILD.dev @@ -22,7 +22,15 @@ pkgver() { git describe --long --tags | sed 's/^v//;s/\([^-]*-g\)/r\1/;s/-/./g' } +prepare() { + export VMODULES="$srcdir/.vmodules" + + cd "$pkgname/src" && v install +} + build() { + export VMODULES="$srcdir/.vmodules" + cd "$pkgname" make prod From 233dd20345c005401dcd28ebd2ebc29a79608531 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Wed, 15 Jun 2022 19:59:48 +0200 Subject: [PATCH 070/236] fix(ci): also install modules when building on dev --- .woodpecker/.build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.woodpecker/.build.yml b/.woodpecker/.build.yml index d073b6c9..7cb7d535 100644 --- a/.woodpecker/.build.yml +++ b/.woodpecker/.build.yml @@ -13,7 +13,7 @@ pipeline: - export VMODULES=$PWD/.vmodules - 'cd src && v install' when: - event: [pull_request] + event: [push, pull_request] debug: image: 'chewingbever/vlang:latest' From 339267e6b2a58bd25adf8599d667085cb48850f6 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Wed, 15 Jun 2022 22:54:27 +0200 Subject: [PATCH 071/236] fix: added VIETER_ prefix to vconf.load calls --- src/console/git/git.v | 12 ++++++------ src/console/logs/logs.v | 6 +++--- src/cron/cli.v | 2 +- src/server/cli.v | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/console/git/git.v b/src/console/git/git.v index 303909e4..54cfaaa8 100644 --- a/src/console/git/git.v +++ b/src/console/git/git.v @@ -41,7 +41,7 @@ pub fn cmd() cli.Command { ] execute: fn (cmd cli.Command) ? { config_file := cmd.flags.get_string('config-file')? - conf := vconf.load(default_path: config_file)? + conf := vconf.load(prefix: 'VIETER_', default_path: config_file)? mut filter := GitRepoFilter{} @@ -70,7 +70,7 @@ pub fn cmd() cli.Command { description: 'Add a new repository.' execute: fn (cmd cli.Command) ? { config_file := cmd.flags.get_string('config-file')? - conf := vconf.load(default_path: config_file)? + conf := vconf.load(prefix: 'VIETER_', default_path: config_file)? add(conf, cmd.args[0], cmd.args[1], cmd.args[2])? } @@ -82,7 +82,7 @@ pub fn cmd() cli.Command { description: 'Remove a repository that matches the given ID prefix.' execute: fn (cmd cli.Command) ? { config_file := cmd.flags.get_string('config-file')? - conf := vconf.load(default_path: config_file)? + conf := vconf.load(prefix: 'VIETER_', default_path: config_file)? remove(conf, cmd.args[0])? } @@ -94,7 +94,7 @@ pub fn cmd() cli.Command { description: 'Show detailed information for the repo matching the ID prefix.' execute: fn (cmd cli.Command) ? { config_file := cmd.flags.get_string('config-file')? - conf := vconf.load(default_path: config_file)? + conf := vconf.load(prefix: 'VIETER_', default_path: config_file)? info(conf, cmd.args[0])? } @@ -133,7 +133,7 @@ pub fn cmd() cli.Command { ] execute: fn (cmd cli.Command) ? { config_file := cmd.flags.get_string('config-file')? - conf := vconf.load(default_path: config_file)? + conf := vconf.load(prefix: 'VIETER_', default_path: config_file)? found := cmd.flags.get_all_found() @@ -155,7 +155,7 @@ pub fn cmd() cli.Command { description: 'Build the repo with the given id & publish it.' execute: fn (cmd cli.Command) ? { config_file := cmd.flags.get_string('config-file')? - conf := vconf.load(default_path: config_file)? + conf := vconf.load(prefix: 'VIETER_', default_path: config_file)? build(conf, cmd.args[0].int())? } diff --git a/src/console/logs/logs.v b/src/console/logs/logs.v index d63e284a..f9dd3636 100644 --- a/src/console/logs/logs.v +++ b/src/console/logs/logs.v @@ -65,7 +65,7 @@ pub fn cmd() cli.Command { ] execute: fn (cmd cli.Command) ? { config_file := cmd.flags.get_string('config-file')? - conf := vconf.load(default_path: config_file)? + conf := vconf.load(prefix: 'VIETER_', default_path: config_file)? mut filter := BuildLogFilter{} @@ -143,7 +143,7 @@ pub fn cmd() cli.Command { description: 'Show all info for a specific build log.' execute: fn (cmd cli.Command) ? { config_file := cmd.flags.get_string('config-file')? - conf := vconf.load(default_path: config_file)? + conf := vconf.load(prefix: 'VIETER_', default_path: config_file)? id := cmd.args[0].int() info(conf, id)? @@ -156,7 +156,7 @@ pub fn cmd() cli.Command { description: 'Output the content of a build log to stdout.' execute: fn (cmd cli.Command) ? { config_file := cmd.flags.get_string('config-file')? - conf := vconf.load(default_path: config_file)? + conf := vconf.load(prefix: 'VIETER_', default_path: config_file)? id := cmd.args[0].int() content(conf, id)? diff --git a/src/cron/cli.v b/src/cron/cli.v index 10a4bdd9..c2e3d39a 100644 --- a/src/cron/cli.v +++ b/src/cron/cli.v @@ -24,7 +24,7 @@ pub fn cmd() cli.Command { description: 'Start the cron service that periodically runs builds.' execute: fn (cmd cli.Command) ? { config_file := cmd.flags.get_string('config-file')? - conf := vconf.load(default_path: config_file)? + conf := vconf.load(prefix: 'VIETER_', default_path: config_file)? cron(conf)? } diff --git a/src/server/cli.v b/src/server/cli.v index cf0306cb..53393f70 100644 --- a/src/server/cli.v +++ b/src/server/cli.v @@ -19,7 +19,7 @@ pub fn cmd() cli.Command { description: 'Start the Vieter server.' execute: fn (cmd cli.Command) ? { config_file := cmd.flags.get_string('config-file')? - conf := vconf.load(default_path: config_file)? + conf := vconf.load(prefix: 'VIETER_', default_path: config_file)? server(conf)? } From 6b79f7b5edf3f7d6ebd7b913d75034529fdd83d2 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Tue, 14 Jun 2022 12:54:32 +0200 Subject: [PATCH 072/236] feat(server): moved api routes under /v1 namespace --- CHANGELOG.md | 4 ++++ src/client/git.v | 10 +++++----- src/client/logs.v | 10 +++++----- src/server/git.v | 30 +++++++++++++++--------------- src/server/logs.v | 24 ++++++++++++------------ 5 files changed, 41 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99a795a4..9adee075 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://git.rustybever.be/vieter/vieter/src/branch/dev) +### Changed + +* Moved all API routes under `/v1` namespace + ## [0.3.0](https://git.rustybever.be/vieter/vieter/src/tag/0.3.0) Nothing besides bumping the versions. diff --git a/src/client/git.v b/src/client/git.v index 4496c08a..7b5e52e1 100644 --- a/src/client/git.v +++ b/src/client/git.v @@ -7,7 +7,7 @@ import response { Response } // get_git_repos returns a list of GitRepo's, given a filter object. pub fn (c &Client) get_git_repos(filter GitRepoFilter) ?[]GitRepo { params := models.params_from(filter) - data := c.send_request<[]GitRepo>(Method.get, '/api/repos', params)? + data := c.send_request<[]GitRepo>(Method.get, '/api/v1/repos', params)? return data.data } @@ -35,7 +35,7 @@ pub fn (c &Client) get_all_git_repos() ?[]GitRepo { // get_git_repo returns the repo for a specific ID. pub fn (c &Client) get_git_repo(id int) ?GitRepo { - data := c.send_request(Method.get, '/api/repos/$id', {})? + data := c.send_request(Method.get, '/api/v1/repos/$id', {})? return data.data } @@ -52,14 +52,14 @@ pub fn (c &Client) add_git_repo(url string, branch string, repo string, arch []s params['arch'] = arch.join(',') } - data := c.send_request(Method.post, '/api/repos', params)? + data := c.send_request(Method.post, '/api/v1/repos', params)? return data } // remove_git_repo removes the repo with the given ID from the server. pub fn (c &Client) remove_git_repo(id int) ?Response { - data := c.send_request(Method.delete, '/api/repos/$id', {})? + data := c.send_request(Method.delete, '/api/v1/repos/$id', {})? return data } @@ -67,7 +67,7 @@ pub fn (c &Client) remove_git_repo(id int) ?Response { // patch_git_repo sends a PATCH request to the given repo with the params as // payload. pub fn (c &Client) patch_git_repo(id int, params map[string]string) ?Response { - data := c.send_request(Method.patch, '/api/repos/$id', params)? + data := c.send_request(Method.patch, '/api/v1/repos/$id', params)? return data } diff --git a/src/client/logs.v b/src/client/logs.v index 739de238..2d78fc15 100644 --- a/src/client/logs.v +++ b/src/client/logs.v @@ -8,7 +8,7 @@ import time // get_build_logs returns all build logs. pub fn (c &Client) get_build_logs(filter BuildLogFilter) ?Response<[]BuildLog> { params := models.params_from(filter) - data := c.send_request<[]BuildLog>(Method.get, '/api/logs', params)? + data := c.send_request<[]BuildLog>(Method.get, '/api/v1/logs', params)? return data } @@ -19,21 +19,21 @@ pub fn (c &Client) get_build_logs_for_repo(repo_id int) ?Response<[]BuildLog> { 'repo': repo_id.str() } - data := c.send_request<[]BuildLog>(Method.get, '/api/logs', params)? + data := c.send_request<[]BuildLog>(Method.get, '/api/v1/logs', params)? return data } // get_build_log returns a specific build log. pub fn (c &Client) get_build_log(id int) ?Response { - data := c.send_request(Method.get, '/api/logs/$id', {})? + data := c.send_request(Method.get, '/api/v1/logs/$id', {})? return data } // get_build_log_content returns the contents of the build log file. pub fn (c &Client) get_build_log_content(id int) ?string { - data := c.send_request_raw_response(Method.get, '/api/logs/$id/content', {}, '')? + data := c.send_request_raw_response(Method.get, '/api/v1/logs/$id/content', {}, '')? return data } @@ -48,7 +48,7 @@ pub fn (c &Client) add_build_log(repo_id int, start_time time.Time, end_time tim 'exitCode': exit_code.str() } - data := c.send_request_with_body(Method.post, '/api/logs', params, content)? + data := c.send_request_with_body(Method.post, '/api/v1/logs', params, content)? return data } diff --git a/src/server/git.v b/src/server/git.v index c1bc6f34..f3fe010d 100644 --- a/src/server/git.v +++ b/src/server/git.v @@ -6,9 +6,9 @@ import response { new_data_response, new_response } import db import models { GitRepo, GitRepoArch, GitRepoFilter } -// get_repos returns the current list of repos. -['/api/repos'; get] -fn (mut app App) get_repos() web.Result { +// v1_get_repos returns the current list of repos. +['/api/v1/repos'; get] +fn (mut app App) v1_get_repos() web.Result { if !app.is_authorized() { return app.json(http.Status.unauthorized, new_response('Unauthorized.')) } @@ -21,9 +21,9 @@ fn (mut app App) get_repos() web.Result { return app.json(http.Status.ok, new_data_response(repos)) } -// get_single_repo returns the information for a single repo. -['/api/repos/:id'; get] -fn (mut app App) get_single_repo(id int) web.Result { +// v1_get_single_repo returns the information for a single repo. +['/api/v1/repos/:id'; get] +fn (mut app App) v1_get_single_repo(id int) web.Result { if !app.is_authorized() { return app.json(http.Status.unauthorized, new_response('Unauthorized.')) } @@ -33,9 +33,9 @@ fn (mut app App) get_single_repo(id int) web.Result { return app.json(http.Status.ok, new_data_response(repo)) } -// post_repo creates a new repo from the provided query string. -['/api/repos'; post] -fn (mut app App) post_repo() web.Result { +// v1_post_repo creates a new repo from the provided query string. +['/api/v1/repos'; post] +fn (mut app App) v1_post_repo() web.Result { if !app.is_authorized() { return app.json(http.Status.unauthorized, new_response('Unauthorized.')) } @@ -57,9 +57,9 @@ fn (mut app App) post_repo() web.Result { return app.json(http.Status.ok, new_response('Repo added successfully.')) } -// delete_repo removes a given repo from the server's list. -['/api/repos/:id'; delete] -fn (mut app App) delete_repo(id int) web.Result { +// v1_delete_repo removes a given repo from the server's list. +['/api/v1/repos/:id'; delete] +fn (mut app App) v1_delete_repo(id int) web.Result { if !app.is_authorized() { return app.json(http.Status.unauthorized, new_response('Unauthorized.')) } @@ -69,9 +69,9 @@ fn (mut app App) delete_repo(id int) web.Result { return app.json(http.Status.ok, new_response('Repo removed successfully.')) } -// patch_repo updates a repo's data with the given query params. -['/api/repos/:id'; patch] -fn (mut app App) patch_repo(id int) web.Result { +// v1_patch_repo updates a repo's data with the given query params. +['/api/v1/repos/:id'; patch] +fn (mut app App) v1_patch_repo(id int) web.Result { if !app.is_authorized() { return app.json(http.Status.unauthorized, new_response('Unauthorized.')) } diff --git a/src/server/logs.v b/src/server/logs.v index 314e3228..748c3a66 100644 --- a/src/server/logs.v +++ b/src/server/logs.v @@ -10,10 +10,10 @@ import os import util import models { BuildLog, BuildLogFilter } -// get_logs returns all build logs in the database. A 'repo' query param can +// v1_get_logs returns all build logs in the database. A 'repo' query param can // optionally be added to limit the list of build logs to that repository. -['/api/logs'; get] -fn (mut app App) get_logs() web.Result { +['/api/v1/logs'; get] +fn (mut app App) v1_get_logs() web.Result { if !app.is_authorized() { return app.json(http.Status.unauthorized, new_response('Unauthorized.')) } @@ -26,9 +26,9 @@ fn (mut app App) get_logs() web.Result { return app.json(http.Status.ok, new_data_response(logs)) } -// get_single_log returns the build log with the given id. -['/api/logs/:id'; get] -fn (mut app App) get_single_log(id int) web.Result { +// v1_get_single_log returns the build log with the given id. +['/api/v1/logs/:id'; get] +fn (mut app App) v1_get_single_log(id int) web.Result { if !app.is_authorized() { return app.json(http.Status.unauthorized, new_response('Unauthorized.')) } @@ -38,9 +38,9 @@ fn (mut app App) get_single_log(id int) web.Result { return app.json(http.Status.ok, new_data_response(log)) } -// get_log_content returns the actual build log file for the given id. -['/api/logs/:id/content'; get] -fn (mut app App) get_log_content(id int) web.Result { +// v1_get_log_content returns the actual build log file for the given id. +['/api/v1/logs/:id/content'; get] +fn (mut app App) v1_get_log_content(id int) web.Result { if !app.is_authorized() { return app.json(http.Status.unauthorized, new_response('Unauthorized.')) } @@ -62,9 +62,9 @@ fn parse_query_time(query string) ?time.Time { return t } -// post_log adds a new log to the database. -['/api/logs'; post] -fn (mut app App) post_log() web.Result { +// v1_post_log adds a new log to the database. +['/api/v1/logs'; post] +fn (mut app App) v1_post_log() web.Result { if !app.is_authorized() { return app.json(http.Status.unauthorized, new_response('Unauthorized.')) } From 4d581da7bf08fa73510c29aff9e11b1741dcce1d Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Tue, 14 Jun 2022 20:31:00 +0200 Subject: [PATCH 073/236] refactor: renamed api routes & client code to 'targets' --- src/client/git.v | 73 ----------------------------- src/client/targets.v | 73 +++++++++++++++++++++++++++++ src/console/git/build.v | 2 +- src/console/git/git.v | 10 ++-- src/cron/daemon/daemon.v | 2 +- src/server/README.md | 6 +++ src/server/{logs.v => api_logs.v} | 0 src/server/{git.v => api_targets.v} | 30 ++++++------ 8 files changed, 101 insertions(+), 95 deletions(-) delete mode 100644 src/client/git.v create mode 100644 src/client/targets.v create mode 100644 src/server/README.md rename src/server/{logs.v => api_logs.v} (100%) rename src/server/{git.v => api_targets.v} (72%) diff --git a/src/client/git.v b/src/client/git.v deleted file mode 100644 index 7b5e52e1..00000000 --- a/src/client/git.v +++ /dev/null @@ -1,73 +0,0 @@ -module client - -import models { GitRepo, GitRepoFilter } -import net.http { Method } -import response { Response } - -// get_git_repos returns a list of GitRepo's, given a filter object. -pub fn (c &Client) get_git_repos(filter GitRepoFilter) ?[]GitRepo { - params := models.params_from(filter) - data := c.send_request<[]GitRepo>(Method.get, '/api/v1/repos', params)? - - return data.data -} - -// get_all_git_repos retrieves *all* GitRepo's from the API using the default -// limit. -pub fn (c &Client) get_all_git_repos() ?[]GitRepo { - mut repos := []GitRepo{} - mut offset := u64(0) - - for { - sub_repos := c.get_git_repos(offset: offset)? - - if sub_repos.len == 0 { - break - } - - repos << sub_repos - - offset += u64(sub_repos.len) - } - - return repos -} - -// get_git_repo returns the repo for a specific ID. -pub fn (c &Client) get_git_repo(id int) ?GitRepo { - data := c.send_request(Method.get, '/api/v1/repos/$id', {})? - - return data.data -} - -// add_git_repo adds a new repo to the server. -pub fn (c &Client) add_git_repo(url string, branch string, repo string, arch []string) ?Response { - mut params := { - 'url': url - 'branch': branch - 'repo': repo - } - - if arch.len > 0 { - params['arch'] = arch.join(',') - } - - data := c.send_request(Method.post, '/api/v1/repos', params)? - - return data -} - -// remove_git_repo removes the repo with the given ID from the server. -pub fn (c &Client) remove_git_repo(id int) ?Response { - data := c.send_request(Method.delete, '/api/v1/repos/$id', {})? - - return data -} - -// patch_git_repo sends a PATCH request to the given repo with the params as -// payload. -pub fn (c &Client) patch_git_repo(id int, params map[string]string) ?Response { - data := c.send_request(Method.patch, '/api/v1/repos/$id', params)? - - return data -} diff --git a/src/client/targets.v b/src/client/targets.v new file mode 100644 index 00000000..90ff1a2d --- /dev/null +++ b/src/client/targets.v @@ -0,0 +1,73 @@ +module client + +import models { GitRepo, GitRepoFilter } +import net.http { Method } +import response { Response } + +// get_targets returns a list of GitRepo's, given a filter object. +pub fn (c &Client) get_targets(filter GitRepoFilter) ?[]GitRepo { + params := models.params_from(filter) + data := c.send_request<[]GitRepo>(Method.get, '/api/v1/targets', params)? + + return data.data +} + +// get_all_targets retrieves *all* GitRepo's from the API using the default +// limit. +pub fn (c &Client) get_all_targets() ?[]GitRepo { + mut repos := []GitRepo{} + mut offset := u64(0) + + for { + sub_repos := c.get_targets(offset: offset)? + + if sub_repos.len == 0 { + break + } + + repos << sub_repos + + offset += u64(sub_repos.len) + } + + return repos +} + +// get_target returns the repo for a specific ID. +pub fn (c &Client) get_target(id int) ?GitRepo { + data := c.send_request(Method.get, '/api/v1/targets/$id', {})? + + return data.data +} + +// add_target adds a new repo to the server. +pub fn (c &Client) add_target(url string, branch string, repo string, arch []string) ?Response { + mut params := { + 'url': url + 'branch': branch + 'repo': repo + } + + if arch.len > 0 { + params['arch'] = arch.join(',') + } + + data := c.send_request(Method.post, '/api/v1/targets', params)? + + return data +} + +// remove_target removes the repo with the given ID from the server. +pub fn (c &Client) remove_target(id int) ?Response { + data := c.send_request(Method.delete, '/api/v1/targets/$id', {})? + + return data +} + +// patch_target sends a PATCH request to the given repo with the params as +// payload. +pub fn (c &Client) patch_target(id int, params map[string]string) ?Response { + data := c.send_request(Method.patch, '/api/v1/targets/$id', params)? + + return data +} diff --git a/src/console/git/build.v b/src/console/git/build.v index fac760df..b3c46a18 100644 --- a/src/console/git/build.v +++ b/src/console/git/build.v @@ -8,7 +8,7 @@ import build // build builds every Git repo in the server's list. fn build(conf Config, repo_id int) ? { c := client.new(conf.address, conf.api_key) - repo := c.get_git_repo(repo_id)? + repo := c.get_target(repo_id)? build_arch := os.uname().machine diff --git a/src/console/git/git.v b/src/console/git/git.v index 54cfaaa8..6a4b9a87 100644 --- a/src/console/git/git.v +++ b/src/console/git/git.v @@ -170,7 +170,7 @@ pub fn cmd() cli.Command { // list prints out a list of all repositories. fn list(conf Config, filter GitRepoFilter) ? { c := client.new(conf.address, conf.api_key) - repos := c.get_git_repos(filter)? + repos := c.get_targets(filter)? data := repos.map([it.id.str(), it.url, it.branch, it.repo]) println(console.pretty_table(['id', 'url', 'branch', 'repo'], data)?) @@ -179,7 +179,7 @@ fn list(conf Config, filter GitRepoFilter) ? { // add adds a new repository to the server's list. fn add(conf Config, url string, branch string, repo string) ? { c := client.new(conf.address, conf.api_key) - res := c.add_git_repo(url, branch, repo, [])? + res := c.add_target(url, branch, repo, [])? println(res.message) } @@ -191,7 +191,7 @@ fn remove(conf Config, id string) ? { if id_int != 0 { c := client.new(conf.address, conf.api_key) - res := c.remove_git_repo(id_int)? + res := c.remove_target(id_int)? println(res.message) } } @@ -209,7 +209,7 @@ fn patch(conf Config, id string, params map[string]string) ? { id_int := id.int() if id_int != 0 { c := client.new(conf.address, conf.api_key) - res := c.patch_git_repo(id_int, params)? + res := c.patch_target(id_int, params)? println(res.message) } @@ -224,6 +224,6 @@ fn info(conf Config, id string) ? { } c := client.new(conf.address, conf.api_key) - repo := c.get_git_repo(id_int)? + repo := c.get_target(id_int)? println(repo) } diff --git a/src/cron/daemon/daemon.v b/src/cron/daemon/daemon.v index f1206d6d..1f36e740 100644 --- a/src/cron/daemon/daemon.v +++ b/src/cron/daemon/daemon.v @@ -178,7 +178,7 @@ fn (mut d Daemon) schedule_build(repo GitRepo) { fn (mut d Daemon) renew_repos() { d.linfo('Renewing repos...') - mut new_repos := d.client.get_all_git_repos() or { + mut new_repos := d.client.get_all_targets() or { d.lerror('Failed to renew repos. Retrying in ${daemon.api_update_retry_timeout}s...') d.api_update_timestamp = time.now().add_seconds(daemon.api_update_retry_timeout) diff --git a/src/server/README.md b/src/server/README.md new file mode 100644 index 00000000..ded99857 --- /dev/null +++ b/src/server/README.md @@ -0,0 +1,6 @@ +This module contains the Vieter HTTP server, consisting of the repository +implementation & the REST API. + +**NOTE**: vweb defines the priority order of routes by the file names in this +module. Therefore, it's very important that all API routes are defined in files +prefixed with `api_`, as this is before the word `routes` alphabetically. diff --git a/src/server/logs.v b/src/server/api_logs.v similarity index 100% rename from src/server/logs.v rename to src/server/api_logs.v diff --git a/src/server/git.v b/src/server/api_targets.v similarity index 72% rename from src/server/git.v rename to src/server/api_targets.v index f3fe010d..67117b92 100644 --- a/src/server/git.v +++ b/src/server/api_targets.v @@ -6,9 +6,9 @@ import response { new_data_response, new_response } import db import models { GitRepo, GitRepoArch, GitRepoFilter } -// v1_get_repos returns the current list of repos. -['/api/v1/repos'; get] -fn (mut app App) v1_get_repos() web.Result { +// v1_get_targets returns the current list of repos. +['/api/v1/targets'; get] +fn (mut app App) v1_get_targets() web.Result { if !app.is_authorized() { return app.json(http.Status.unauthorized, new_response('Unauthorized.')) } @@ -21,9 +21,9 @@ fn (mut app App) v1_get_repos() web.Result { return app.json(http.Status.ok, new_data_response(repos)) } -// v1_get_single_repo returns the information for a single repo. -['/api/v1/repos/:id'; get] -fn (mut app App) v1_get_single_repo(id int) web.Result { +// v1_get_single_target returns the information for a single repo. +['/api/v1/targets/:id'; get] +fn (mut app App) v1_get_single_target(id int) web.Result { if !app.is_authorized() { return app.json(http.Status.unauthorized, new_response('Unauthorized.')) } @@ -33,9 +33,9 @@ fn (mut app App) v1_get_single_repo(id int) web.Result { return app.json(http.Status.ok, new_data_response(repo)) } -// v1_post_repo creates a new repo from the provided query string. -['/api/v1/repos'; post] -fn (mut app App) v1_post_repo() web.Result { +// v1_post_target creates a new repo from the provided query string. +['/api/v1/targets'; post] +fn (mut app App) v1_post_target() web.Result { if !app.is_authorized() { return app.json(http.Status.unauthorized, new_response('Unauthorized.')) } @@ -57,9 +57,9 @@ fn (mut app App) v1_post_repo() web.Result { return app.json(http.Status.ok, new_response('Repo added successfully.')) } -// v1_delete_repo removes a given repo from the server's list. -['/api/v1/repos/:id'; delete] -fn (mut app App) v1_delete_repo(id int) web.Result { +// v1_delete_target removes a given repo from the server's list. +['/api/v1/targets/:id'; delete] +fn (mut app App) v1_delete_target(id int) web.Result { if !app.is_authorized() { return app.json(http.Status.unauthorized, new_response('Unauthorized.')) } @@ -69,9 +69,9 @@ fn (mut app App) v1_delete_repo(id int) web.Result { return app.json(http.Status.ok, new_response('Repo removed successfully.')) } -// v1_patch_repo updates a repo's data with the given query params. -['/api/v1/repos/:id'; patch] -fn (mut app App) v1_patch_repo(id int) web.Result { +// v1_patch_target updates a repo's data with the given query params. +['/api/v1/targets/:id'; patch] +fn (mut app App) v1_patch_target(id int) web.Result { if !app.is_authorized() { return app.json(http.Status.unauthorized, new_response('Unauthorized.')) } From faec08f846492718a5fefdb5d0d234bc5928e70c Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Tue, 14 Jun 2022 20:38:20 +0200 Subject: [PATCH 074/236] refactor(console): renamed stuff to 'targets' --- src/console/{git => targets}/build.v | 4 ++-- src/console/{git/git.v => targets/targets.v} | 20 ++++++++++---------- src/main.v | 4 ++-- 3 files changed, 14 insertions(+), 14 deletions(-) rename src/console/{git => targets}/build.v (90%) rename src/console/{git/git.v => targets/targets.v} (91%) diff --git a/src/console/git/build.v b/src/console/targets/build.v similarity index 90% rename from src/console/git/build.v rename to src/console/targets/build.v index b3c46a18..d50dfd2c 100644 --- a/src/console/git/build.v +++ b/src/console/targets/build.v @@ -1,11 +1,11 @@ -module git +module targets import client import docker import os import build -// build builds every Git repo in the server's list. +// build locally builds the target with the given id. fn build(conf Config, repo_id int) ? { c := client.new(conf.address, conf.api_key) repo := c.get_target(repo_id)? diff --git a/src/console/git/git.v b/src/console/targets/targets.v similarity index 91% rename from src/console/git/git.v rename to src/console/targets/targets.v index 6a4b9a87..9917913c 100644 --- a/src/console/git/git.v +++ b/src/console/targets/targets.v @@ -1,4 +1,4 @@ -module git +module targets import cli import vieter.vconf @@ -16,12 +16,12 @@ struct Config { // cmd returns the cli submodule that handles the repos API interaction pub fn cmd() cli.Command { return cli.Command{ - name: 'repos' - description: 'Interact with the repos API.' + name: 'targets' + description: 'Interact with the targets API.' commands: [ cli.Command{ name: 'list' - description: 'List the current repos.' + description: 'List the current targets.' flags: [ cli.Flag{ name: 'limit' @@ -35,7 +35,7 @@ pub fn cmd() cli.Command { }, cli.Flag{ name: 'repo' - description: 'Only return Git repos that publish to this repo.' + description: 'Only return targets that publish to this repo.' flag: cli.FlagType.string }, ] @@ -67,7 +67,7 @@ pub fn cmd() cli.Command { name: 'add' required_args: 3 usage: 'url branch repo' - description: 'Add a new repository.' + description: 'Add a new Git repository target.' execute: fn (cmd cli.Command) ? { config_file := cmd.flags.get_string('config-file')? conf := vconf.load(prefix: 'VIETER_', default_path: config_file)? @@ -79,7 +79,7 @@ pub fn cmd() cli.Command { name: 'remove' required_args: 1 usage: 'id' - description: 'Remove a repository that matches the given ID prefix.' + description: 'Remove a target that matches the given id.' execute: fn (cmd cli.Command) ? { config_file := cmd.flags.get_string('config-file')? conf := vconf.load(prefix: 'VIETER_', default_path: config_file)? @@ -91,7 +91,7 @@ pub fn cmd() cli.Command { name: 'info' required_args: 1 usage: 'id' - description: 'Show detailed information for the repo matching the ID prefix.' + description: 'Show detailed information for the target matching the id.' execute: fn (cmd cli.Command) ? { config_file := cmd.flags.get_string('config-file')? conf := vconf.load(prefix: 'VIETER_', default_path: config_file)? @@ -103,7 +103,7 @@ pub fn cmd() cli.Command { name: 'edit' required_args: 1 usage: 'id' - description: 'Edit the repository that matches the given ID prefix.' + description: 'Edit the Git repository target that matches the given id.' flags: [ cli.Flag{ name: 'url' @@ -152,7 +152,7 @@ pub fn cmd() cli.Command { name: 'build' required_args: 1 usage: 'id' - description: 'Build the repo with the given id & publish it.' + description: 'Build the target with the given id & publish it.' execute: fn (cmd cli.Command) ? { config_file := cmd.flags.get_string('config-file')? conf := vconf.load(prefix: 'VIETER_', default_path: config_file)? diff --git a/src/main.v b/src/main.v index e1979de9..cba410ce 100644 --- a/src/main.v +++ b/src/main.v @@ -3,7 +3,7 @@ module main import os import server import cli -import console.git +import console.targets import console.logs import console.schedule import console.man @@ -26,7 +26,7 @@ fn main() { ] commands: [ server.cmd(), - git.cmd(), + targets.cmd(), cron.cmd(), logs.cmd(), schedule.cmd(), From 102a7f88999102a795646d3136475e7ac1238dfe Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Tue, 14 Jun 2022 22:25:40 +0200 Subject: [PATCH 075/236] refactor: renamed codebase to "targets" --- src/build/build.v | 6 +- src/build/shell.v | 6 +- src/build/shell_test.v | 6 +- src/client/logs.v | 6 +- src/client/targets.v | 36 +++---- src/console/logs/logs.v | 22 ++--- src/console/targets/targets.v | 6 +- src/cron/daemon/daemon.v | 8 +- src/db/db.v | 8 +- src/db/git.v | 99 ------------------- src/db/logs.v | 12 +-- .../migrations/002-rename-to-targets/down.sql | 5 + .../migrations/002-rename-to-targets/up.sql | 5 + src/db/targets.v | 99 +++++++++++++++++++ src/models/logs.v | 6 +- src/models/models.v | 4 +- src/models/{git.v => targets.v} | 18 ++-- src/server/api_logs.v | 14 +-- src/server/api_targets.v | 30 +++--- 19 files changed, 205 insertions(+), 191 deletions(-) delete mode 100644 src/db/git.v create mode 100644 src/db/migrations/002-rename-to-targets/down.sql create mode 100644 src/db/migrations/002-rename-to-targets/up.sql create mode 100644 src/db/targets.v rename src/models/{git.v => targets.v} (75%) diff --git a/src/build/build.v b/src/build/build.v index 2e86471f..3c6b439a 100644 --- a/src/build/build.v +++ b/src/build/build.v @@ -6,7 +6,7 @@ import time import os import strings import util -import models { GitRepo } +import models { Target } const ( container_build_dir = '/build' @@ -91,9 +91,9 @@ pub: } // build_repo builds, packages & publishes a given Arch package based on the -// provided GitRepo. The base image ID should be of an image previously created +// provided target. The base image ID should be of an image previously created // by create_build_image. It returns the logs of the container. -pub fn build_repo(address string, api_key string, base_image_id string, repo &GitRepo) ?BuildResult { +pub fn build_repo(address string, api_key string, base_image_id string, repo &Target) ?BuildResult { mut dd := docker.new_conn()? defer { diff --git a/src/build/shell.v b/src/build/shell.v index a3121fe8..c5944bf5 100644 --- a/src/build/shell.v +++ b/src/build/shell.v @@ -1,6 +1,6 @@ module build -import models { GitRepo } +import models { Target } // escape_shell_string escapes any characters that could be interpreted // incorrectly by a shell. The resulting value should be safe to use inside an @@ -22,8 +22,8 @@ pub fn echo_commands(cmds []string) []string { return out } -// create_build_script generates a shell script that builds a given GitRepo. -fn create_build_script(address string, repo &GitRepo, build_arch string) string { +// create_build_script generates a shell script that builds a given Target. +fn create_build_script(address string, repo &Target, build_arch string) string { repo_url := '$address/$repo.repo' commands := echo_commands([ diff --git a/src/build/shell_test.v b/src/build/shell_test.v index 46ab3501..85340549 100644 --- a/src/build/shell_test.v +++ b/src/build/shell_test.v @@ -1,15 +1,15 @@ module build -import models { GitRepo } +import models { Target } fn test_create_build_script() { - repo := GitRepo{ + target := Target{ id: 1 url: 'https://examplerepo.com' branch: 'main' repo: 'vieter' } - build_script := create_build_script('https://example.com', repo, 'x86_64') + build_script := create_build_script('https://example.com', target, 'x86_64') expected := $embed_file('build_script.sh') assert build_script == expected.to_string().trim_space() diff --git a/src/client/logs.v b/src/client/logs.v index 2d78fc15..49ef679b 100644 --- a/src/client/logs.v +++ b/src/client/logs.v @@ -13,10 +13,10 @@ pub fn (c &Client) get_build_logs(filter BuildLogFilter) ?Response<[]BuildLog> { return data } -// get_build_logs_for_repo returns all build logs for a given repo. -pub fn (c &Client) get_build_logs_for_repo(repo_id int) ?Response<[]BuildLog> { +// get_build_logs_for_target returns all build logs for a given target. +pub fn (c &Client) get_build_logs_for_target(target_id int) ?Response<[]BuildLog> { params := { - 'repo': repo_id.str() + 'repo': target_id.str() } data := c.send_request<[]BuildLog>(Method.get, '/api/v1/logs', params)? diff --git a/src/client/targets.v b/src/client/targets.v index 90ff1a2d..86d8c2a9 100644 --- a/src/client/targets.v +++ b/src/client/targets.v @@ -1,46 +1,46 @@ module client -import models { GitRepo, GitRepoFilter } +import models { Target, TargetFilter } import net.http { Method } import response { Response } -// get_targets returns a list of GitRepo's, given a filter object. -pub fn (c &Client) get_targets(filter GitRepoFilter) ?[]GitRepo { +// get_targets returns a list of targets, given a filter object. +pub fn (c &Client) get_targets(filter TargetFilter) ?[]Target { params := models.params_from(filter) - data := c.send_request<[]GitRepo>(Method.get, '/api/v1/targets', params)? + data := c.send_request<[]Target>(Method.get, '/api/v1/targets', params)? return data.data } -// get_all_targets retrieves *all* GitRepo's from the API using the default +// get_all_targets retrieves *all* targs from the API using the default // limit. -pub fn (c &Client) get_all_targets() ?[]GitRepo { - mut repos := []GitRepo{} +pub fn (c &Client) get_all_targets() ?[]Target { + mut targets := []Target{} mut offset := u64(0) for { - sub_repos := c.get_targets(offset: offset)? + sub_targets := c.get_targets(offset: offset)? - if sub_repos.len == 0 { + if sub_targets.len == 0 { break } - repos << sub_repos + targets << sub_targets - offset += u64(sub_repos.len) + offset += u64(sub_targets.len) } - return repos + return targets } -// get_target returns the repo for a specific ID. -pub fn (c &Client) get_target(id int) ?GitRepo { - data := c.send_request(Method.get, '/api/v1/targets/$id', {})? +// get_target returns the target for a specific id. +pub fn (c &Client) get_target(id int) ?Target { + data := c.send_request(Method.get, '/api/v1/targets/$id', {})? return data.data } -// add_target adds a new repo to the server. +// add_target adds a new target to the server. pub fn (c &Client) add_target(url string, branch string, repo string, arch []string) ?Response { mut params := { 'url': url @@ -57,14 +57,14 @@ pub fn (c &Client) add_target(url string, branch string, repo string, arch []str return data } -// remove_target removes the repo with the given ID from the server. +// remove_target removes the target with the given id from the server. pub fn (c &Client) remove_target(id int) ?Response { data := c.send_request(Method.delete, '/api/v1/targets/$id', {})? return data } -// patch_target sends a PATCH request to the given repo with the params as +// patch_target sends a PATCH request to the given target with the params as // payload. pub fn (c &Client) patch_target(id int, params map[string]string) ?Response { data := c.send_request(Method.patch, '/api/v1/targets/$id', params)? diff --git a/src/console/logs/logs.v b/src/console/logs/logs.v index f9dd3636..5155ae88 100644 --- a/src/console/logs/logs.v +++ b/src/console/logs/logs.v @@ -12,7 +12,7 @@ struct Config { api_key string [required] } -// cmd returns the cli module that handles the build repos API. +// cmd returns the cli module that handles the build logs API. pub fn cmd() cli.Command { return cli.Command{ name: 'logs' @@ -33,8 +33,8 @@ pub fn cmd() cli.Command { flag: cli.FlagType.int }, cli.Flag{ - name: 'repo' - description: 'Only return logs for this repo id.' + name: 'target' + description: 'Only return logs for this target id.' flag: cli.FlagType.int }, cli.Flag{ @@ -79,9 +79,9 @@ pub fn cmd() cli.Command { filter.offset = u64(offset) } - repo_id := cmd.flags.get_int('repo')? - if repo_id != 0 { - filter.repo = repo_id + target_id := cmd.flags.get_int('target')? + if target_id != 0 { + filter.target = target_id } tz_offset := time.offset() @@ -168,10 +168,10 @@ pub fn cmd() cli.Command { // print_log_list prints a list of logs. fn print_log_list(logs []BuildLog) ? { - data := logs.map([it.id.str(), it.repo_id.str(), it.start_time.local().str(), + data := logs.map([it.id.str(), it.target_id.str(), it.start_time.local().str(), it.exit_code.str()]) - println(console.pretty_table(['id', 'repo', 'start time', 'exit code'], data)?) + println(console.pretty_table(['id', 'target', 'start time', 'exit code'], data)?) } // list prints a list of all build logs. @@ -182,10 +182,10 @@ fn list(conf Config, filter BuildLogFilter) ? { print_log_list(logs)? } -// list prints a list of all build logs for a given repo. -fn list_for_repo(conf Config, repo_id int) ? { +// list prints a list of all build logs for a given target. +fn list_for_target(conf Config, target_id int) ? { c := client.new(conf.address, conf.api_key) - logs := c.get_build_logs_for_repo(repo_id)?.data + logs := c.get_build_logs_for_target(target_id)?.data print_log_list(logs)? } diff --git a/src/console/targets/targets.v b/src/console/targets/targets.v index 9917913c..530184d4 100644 --- a/src/console/targets/targets.v +++ b/src/console/targets/targets.v @@ -5,7 +5,7 @@ import vieter.vconf import cron.expression { parse_expression } import client import console -import models { GitRepoFilter } +import models { TargetFilter } struct Config { address string [required] @@ -43,7 +43,7 @@ pub fn cmd() cli.Command { config_file := cmd.flags.get_string('config-file')? conf := vconf.load(prefix: 'VIETER_', default_path: config_file)? - mut filter := GitRepoFilter{} + mut filter := TargetFilter{} limit := cmd.flags.get_int('limit')? if limit != 0 { @@ -168,7 +168,7 @@ pub fn cmd() cli.Command { // ID. If multiple or none are found, an error is raised. // list prints out a list of all repositories. -fn list(conf Config, filter GitRepoFilter) ? { +fn list(conf Config, filter TargetFilter) ? { c := client.new(conf.address, conf.api_key) repos := c.get_targets(filter)? data := repos.map([it.id.str(), it.url, it.branch, it.repo]) diff --git a/src/cron/daemon/daemon.v b/src/cron/daemon/daemon.v index 1f36e740..7b514aa4 100644 --- a/src/cron/daemon/daemon.v +++ b/src/cron/daemon/daemon.v @@ -9,7 +9,7 @@ import build import docker import os import client -import models { GitRepo } +import models { Target } const ( // How many seconds to wait before retrying to update API if failed @@ -20,7 +20,7 @@ const ( struct ScheduledBuild { pub: - repo GitRepo + repo Target timestamp time.Time } @@ -38,7 +38,7 @@ mut: api_update_frequency int image_rebuild_frequency int // Repos currently loaded from API. - repos []GitRepo + repos []Target // At what point to update the list of repositories. api_update_timestamp time.Time image_build_timestamp time.Time @@ -149,7 +149,7 @@ pub fn (mut d Daemon) run() { } // schedule_build adds the next occurence of the given repo build to the queue. -fn (mut d Daemon) schedule_build(repo GitRepo) { +fn (mut d Daemon) schedule_build(repo Target) { ce := if repo.schedule != '' { parse_expression(repo.schedule) or { // TODO This shouldn't return an error if the expression is empty. diff --git a/src/db/db.v b/src/db/db.v index fac14581..64a57d22 100644 --- a/src/db/db.v +++ b/src/db/db.v @@ -13,8 +13,12 @@ struct MigrationVersion { } const ( - migrations_up = [$embed_file('migrations/001-initial/up.sql')] - migrations_down = [$embed_file('migrations/001-initial/down.sql')] + migrations_up = [ + $embed_file('migrations/001-initial/up.sql'), + $embed_file('migrations/002-rename-to-targets/up.sql'), + ] + migrations_down = [$embed_file('migrations/001-initial/down.sql'), + $embed_file('migrations/002-rename-to-targets/down.sql')] ) // init initializes a database & adds the correct tables. diff --git a/src/db/git.v b/src/db/git.v deleted file mode 100644 index 8cc493f3..00000000 --- a/src/db/git.v +++ /dev/null @@ -1,99 +0,0 @@ -module db - -import models { GitRepo, GitRepoArch, GitRepoFilter } - -// get_git_repos returns all GitRepo's in the database. -pub fn (db &VieterDb) get_git_repos(filter GitRepoFilter) []GitRepo { - // This seems to currently be blocked by a bug in the ORM, I'll have to ask - // around. - if filter.repo != '' { - res := sql db.conn { - select from GitRepo where repo == filter.repo order by id limit filter.limit offset filter.offset - } - - return res - } - - res := sql db.conn { - select from GitRepo order by id limit filter.limit offset filter.offset - } - - return res -} - -// get_git_repo tries to return a specific GitRepo. -pub fn (db &VieterDb) get_git_repo(repo_id int) ?GitRepo { - res := sql db.conn { - select from GitRepo where id == repo_id - } - - // If a select statement fails, it returns a zeroed object. By - // checking one of the required fields, we can see whether the query - // returned a result or not. - if res.id == 0 { - return none - } - - return res -} - -// add_git_repo inserts the given GitRepo into the database. -pub fn (db &VieterDb) add_git_repo(repo GitRepo) { - sql db.conn { - insert repo into GitRepo - } -} - -// delete_git_repo deletes the repo with the given ID from the database. -pub fn (db &VieterDb) delete_git_repo(repo_id int) { - sql db.conn { - delete from GitRepo where id == repo_id - delete from GitRepoArch where repo_id == repo_id - } -} - -// update_git_repo updates any non-array values for a given GitRepo. -pub fn (db &VieterDb) update_git_repo(repo_id int, params map[string]string) { - mut values := []string{} - - // TODO does this allow for SQL injection? - $for field in GitRepo.fields { - if field.name in params { - // Any fields that are array types require their own update method - $if field.typ is string { - values << "$field.name = '${params[field.name]}'" - } - } - } - values_str := values.join(', ') - // I think this is actual SQL & not the ORM language - query := 'update GitRepo set $values_str where id == $repo_id' - - db.conn.exec_none(query) -} - -// update_git_repo_archs updates a given GitRepo's arch value. -pub fn (db &VieterDb) update_git_repo_archs(repo_id int, archs []GitRepoArch) { - archs_with_id := archs.map(GitRepoArch{ - ...it - repo_id: repo_id - }) - - sql db.conn { - delete from GitRepoArch where repo_id == repo_id - } - - for arch in archs_with_id { - sql db.conn { - insert arch into GitRepoArch - } - } -} - -// git_repo_exists is a utility function that checks whether a repo with the -// given id exists. -pub fn (db &VieterDb) git_repo_exists(repo_id int) bool { - db.get_git_repo(repo_id) or { return false } - - return true -} diff --git a/src/db/logs.v b/src/db/logs.v index cac08e7f..af5f53c3 100644 --- a/src/db/logs.v +++ b/src/db/logs.v @@ -7,8 +7,8 @@ import time pub fn (db &VieterDb) get_build_logs(filter BuildLogFilter) []BuildLog { mut where_parts := []string{} - if filter.repo != 0 { - where_parts << 'repo_id == $filter.repo' + if filter.target != 0 { + where_parts << 'target_id == $filter.target' } if filter.before != time.Time{} { @@ -55,11 +55,11 @@ pub fn (db &VieterDb) get_build_logs(filter BuildLogFilter) []BuildLog { return res } -// get_build_logs_for_repo returns all BuildLog's in the database for a given -// repo. -pub fn (db &VieterDb) get_build_logs_for_repo(repo_id int) []BuildLog { +// get_build_logs_for_target returns all BuildLog's in the database for a given +// target. +pub fn (db &VieterDb) get_build_logs_for_target(target_id int) []BuildLog { res := sql db.conn { - select from BuildLog where repo_id == repo_id order by id + select from BuildLog where target_id == target_id order by id } return res diff --git a/src/db/migrations/002-rename-to-targets/down.sql b/src/db/migrations/002-rename-to-targets/down.sql new file mode 100644 index 00000000..861bfa9f --- /dev/null +++ b/src/db/migrations/002-rename-to-targets/down.sql @@ -0,0 +1,5 @@ +ALTER TABLE Target RENAME TO GitRepo; +ALTER TABLE TargetArch RENAME TO GitRepoArch; + +ALTER TABLE GitRepoArch RENAME COLUMN target_id TO repo_id; +ALTER TABLE BuildLog RENAME COLUMN target_id TO repo_id; diff --git a/src/db/migrations/002-rename-to-targets/up.sql b/src/db/migrations/002-rename-to-targets/up.sql new file mode 100644 index 00000000..081e3eee --- /dev/null +++ b/src/db/migrations/002-rename-to-targets/up.sql @@ -0,0 +1,5 @@ +ALTER TABLE GitRepo RENAME TO Target; +ALTER TABLE GitRepoArch RENAME TO TargetArch; + +ALTER TABLE TargetArch RENAME COLUMN repo_id TO target_id; +ALTER TABLE BuildLog RENAME COLUMN repo_id TO target_id; diff --git a/src/db/targets.v b/src/db/targets.v new file mode 100644 index 00000000..91020337 --- /dev/null +++ b/src/db/targets.v @@ -0,0 +1,99 @@ +module db + +import models { Target, TargetArch, TargetFilter } + +// get_targets returns all targets in the database. +pub fn (db &VieterDb) get_targets(filter TargetFilter) []Target { + // This seems to currently be blocked by a bug in the ORM, I'll have to ask + // around. + if filter.repo != '' { + res := sql db.conn { + select from Target where repo == filter.repo order by id limit filter.limit offset filter.offset + } + + return res + } + + res := sql db.conn { + select from Target order by id limit filter.limit offset filter.offset + } + + return res +} + +// get_target tries to return a specific target. +pub fn (db &VieterDb) get_target(target_id int) ?Target { + res := sql db.conn { + select from Target where id == target_id + } + + // If a select statement fails, it returns a zeroed object. By + // checking one of the required fields, we can see whether the query + // returned a result or not. + if res.id == 0 { + return none + } + + return res +} + +// add_target inserts the given target into the database. +pub fn (db &VieterDb) add_target(repo Target) { + sql db.conn { + insert repo into Target + } +} + +// delete_target deletes the target with the given id from the database. +pub fn (db &VieterDb) delete_target(target_id int) { + sql db.conn { + delete from Target where id == target_id + delete from TargetArch where target_id == target_id + } +} + +// update_target updates any non-array values for a given target. +pub fn (db &VieterDb) update_target(target_id int, params map[string]string) { + mut values := []string{} + + // TODO does this allow for SQL injection? + $for field in Target.fields { + if field.name in params { + // Any fields that are array types require their own update method + $if field.typ is string { + values << "$field.name = '${params[field.name]}'" + } + } + } + values_str := values.join(', ') + // I think this is actual SQL & not the ORM language + query := 'update Target set $values_str where id == $target_id' + + db.conn.exec_none(query) +} + +// update_target_archs updates a given target's arch value. +pub fn (db &VieterDb) update_target_archs(target_id int, archs []TargetArch) { + archs_with_id := archs.map(TargetArch{ + ...it + target_id: target_id + }) + + sql db.conn { + delete from TargetArch where target_id == target_id + } + + for arch in archs_with_id { + sql db.conn { + insert arch into TargetArch + } + } +} + +// target_exists is a utility function that checks whether a target with the +// given id exists. +pub fn (db &VieterDb) target_exists(target_id int) bool { + db.get_target(target_id) or { return false } + + return true +} diff --git a/src/models/logs.v b/src/models/logs.v index 7f5a5fe5..12907d84 100644 --- a/src/models/logs.v +++ b/src/models/logs.v @@ -5,7 +5,7 @@ import time pub struct BuildLog { pub mut: id int [primary; sql: serial] - repo_id int [nonull] + target_id int [nonull] start_time time.Time [nonull] end_time time.Time [nonull] arch string [nonull] @@ -16,7 +16,7 @@ pub mut: pub fn (bl &BuildLog) str() string { mut parts := [ 'id: $bl.id', - 'repo id: $bl.repo_id', + 'target id: $bl.target_id', 'start time: $bl.start_time.local()', 'end time: $bl.end_time.local()', 'duration: ${bl.end_time - bl.start_time}', @@ -33,7 +33,7 @@ pub struct BuildLogFilter { pub mut: limit u64 = 25 offset u64 - repo int + target int before time.Time after time.Time arch string diff --git a/src/models/models.v b/src/models/models.v index 3a127bcc..b6103d31 100644 --- a/src/models/models.v +++ b/src/models/models.v @@ -23,8 +23,8 @@ pub fn patch_from_params(mut o T, params map[string]string) ? { o.$(field.name) = params[field.name].int() } $else $if field.typ is u64 { o.$(field.name) = params[field.name].u64() - } $else $if field.typ is []GitRepoArch { - o.$(field.name) = params[field.name].split(',').map(GitRepoArch{ value: it }) + } $else $if field.typ is []TargetArch { + o.$(field.name) = params[field.name].split(',').map(TargetArch{ value: it }) } $else $if field.typ is time.Time { o.$(field.name) = time.unix(params[field.name].int()) } $else $if field.typ is []string { diff --git a/src/models/git.v b/src/models/targets.v similarity index 75% rename from src/models/git.v rename to src/models/targets.v index 5dcc13a7..537bbc4d 100644 --- a/src/models/git.v +++ b/src/models/targets.v @@ -1,18 +1,18 @@ module models -pub struct GitRepoArch { +pub struct TargetArch { pub: - id int [primary; sql: serial] - repo_id int [nonull] - value string [nonull] + id int [primary; sql: serial] + target_id int [nonull] + value string [nonull] } // str returns a string representation. -pub fn (gra &GitRepoArch) str() string { +pub fn (gra &TargetArch) str() string { return gra.value } -pub struct GitRepo { +pub struct Target { pub mut: id int [primary; sql: serial] // URL of the Git repository @@ -25,11 +25,11 @@ pub mut: schedule string // On which architectures the package is allowed to be built. In reality, // this controls which builders will periodically build the image. - arch []GitRepoArch [fkey: 'repo_id'] + arch []TargetArch [fkey: 'target_id'] } // str returns a string representation. -pub fn (gr &GitRepo) str() string { +pub fn (gr &Target) str() string { mut parts := [ 'id: $gr.id', 'url: $gr.url', @@ -44,7 +44,7 @@ pub fn (gr &GitRepo) str() string { } [params] -pub struct GitRepoFilter { +pub struct TargetFilter { pub mut: limit u64 = 25 offset u64 diff --git a/src/server/api_logs.v b/src/server/api_logs.v index 748c3a66..fa3338ea 100644 --- a/src/server/api_logs.v +++ b/src/server/api_logs.v @@ -10,7 +10,7 @@ import os import util import models { BuildLog, BuildLogFilter } -// v1_get_logs returns all build logs in the database. A 'repo' query param can +// v1_get_logs returns all build logs in the database. A 'target' query param can // optionally be added to limit the list of build logs to that repository. ['/api/v1/logs'; get] fn (mut app App) v1_get_logs() web.Result { @@ -47,7 +47,7 @@ fn (mut app App) v1_get_log_content(id int) web.Result { log := app.db.get_build_log(id) or { return app.not_found() } file_name := log.start_time.custom_format('YYYY-MM-DD_HH-mm-ss') - full_path := os.join_path(app.conf.data_dir, logs_dir_name, log.repo_id.str(), log.arch, + full_path := os.join_path(app.conf.data_dir, logs_dir_name, log.target_id.str(), log.arch, file_name) return app.file(full_path) @@ -96,15 +96,15 @@ fn (mut app App) v1_post_log() web.Result { arch := app.query['arch'] - repo_id := app.query['repo'].int() + target_id := app.query['target'].int() - if !app.db.git_repo_exists(repo_id) { - return app.json(http.Status.bad_request, new_response('Unknown Git repo.')) + if !app.db.target_exists(target_id) { + return app.json(http.Status.bad_request, new_response('Unknown target.')) } // Store log in db log := BuildLog{ - repo_id: repo_id + target_id: target_id start_time: start_time end_time: end_time arch: arch @@ -113,7 +113,7 @@ fn (mut app App) v1_post_log() web.Result { app.db.add_build_log(log) - repo_logs_dir := os.join_path(app.conf.data_dir, logs_dir_name, repo_id.str(), arch) + repo_logs_dir := os.join_path(app.conf.data_dir, logs_dir_name, target_id.str(), arch) // Create the logs directory of it doesn't exist if !os.exists(repo_logs_dir) { diff --git a/src/server/api_targets.v b/src/server/api_targets.v index 67117b92..f807fe94 100644 --- a/src/server/api_targets.v +++ b/src/server/api_targets.v @@ -4,36 +4,36 @@ import web import net.http import response { new_data_response, new_response } import db -import models { GitRepo, GitRepoArch, GitRepoFilter } +import models { Target, TargetArch, TargetFilter } -// v1_get_targets returns the current list of repos. +// v1_get_targets returns the current list of targets. ['/api/v1/targets'; get] fn (mut app App) v1_get_targets() web.Result { if !app.is_authorized() { return app.json(http.Status.unauthorized, new_response('Unauthorized.')) } - filter := models.from_params(app.query) or { + filter := models.from_params(app.query) or { return app.json(http.Status.bad_request, new_response('Invalid query parameters.')) } - repos := app.db.get_git_repos(filter) + repos := app.db.get_targets(filter) return app.json(http.Status.ok, new_data_response(repos)) } -// v1_get_single_target returns the information for a single repo. +// v1_get_single_target returns the information for a single target. ['/api/v1/targets/:id'; get] fn (mut app App) v1_get_single_target(id int) web.Result { if !app.is_authorized() { return app.json(http.Status.unauthorized, new_response('Unauthorized.')) } - repo := app.db.get_git_repo(id) or { return app.not_found() } + repo := app.db.get_target(id) or { return app.not_found() } return app.json(http.Status.ok, new_data_response(repo)) } -// v1_post_target creates a new repo from the provided query string. +// v1_post_target creates a new target from the provided query string. ['/api/v1/targets'; post] fn (mut app App) v1_post_target() web.Result { if !app.is_authorized() { @@ -48,40 +48,40 @@ fn (mut app App) v1_post_target() web.Result { params['arch'] = app.conf.default_arch } - new_repo := models.from_params(params) or { + new_repo := models.from_params(params) or { return app.json(http.Status.bad_request, new_response(err.msg())) } - app.db.add_git_repo(new_repo) + app.db.add_target(new_repo) return app.json(http.Status.ok, new_response('Repo added successfully.')) } -// v1_delete_target removes a given repo from the server's list. +// v1_delete_target removes a given target from the server's list. ['/api/v1/targets/:id'; delete] fn (mut app App) v1_delete_target(id int) web.Result { if !app.is_authorized() { return app.json(http.Status.unauthorized, new_response('Unauthorized.')) } - app.db.delete_git_repo(id) + app.db.delete_target(id) return app.json(http.Status.ok, new_response('Repo removed successfully.')) } -// v1_patch_target updates a repo's data with the given query params. +// v1_patch_target updates a target's data with the given query params. ['/api/v1/targets/:id'; patch] fn (mut app App) v1_patch_target(id int) web.Result { if !app.is_authorized() { return app.json(http.Status.unauthorized, new_response('Unauthorized.')) } - app.db.update_git_repo(id, app.query) + app.db.update_target(id, app.query) if 'arch' in app.query { - arch_objs := app.query['arch'].split(',').map(GitRepoArch{ value: it }) + arch_objs := app.query['arch'].split(',').map(TargetArch{ value: it }) - app.db.update_git_repo_archs(id, arch_objs) + app.db.update_target_archs(id, arch_objs) } return app.json(http.Status.ok, new_response('Repo updated successfully.')) From 9727b8620302276ed9f65b8d24c12273371bb99e Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Tue, 14 Jun 2022 22:41:28 +0200 Subject: [PATCH 076/236] docs(api): updated to new "targets" naming --- docs/api/source/includes/_logs.md | 10 ++-- .../source/includes/{_git.md => _targets.md} | 51 +++++++++---------- docs/api/source/index.html.md | 2 +- 3 files changed, 31 insertions(+), 32 deletions(-) rename docs/api/source/includes/{_git.md => _targets.md} (71%) diff --git a/docs/api/source/includes/_logs.md b/docs/api/source/includes/_logs.md index d4f76320..a38bf484 100644 --- a/docs/api/source/includes/_logs.md +++ b/docs/api/source/includes/_logs.md @@ -24,7 +24,7 @@ curl \ "data": [ { "id": 1, - "repo_id": 3, + "target_id": 3, "start_time": 1652008554, "end_time": 1652008559, "arch": "x86_64", @@ -46,7 +46,7 @@ Parameter | Description --------- | ----------- limit | Maximum amount of results to return. offset | Offset of results. -repo | Only return builds published to this repository. +target | Only return builds for this target id. before | Only return logs started before this time (UTC epoch) after | Only return logs started after this time (UTC epoch) arch | Only return logs built on this architecture @@ -58,7 +58,7 @@ exit_codes | Comma-separated list of exit codes to limit result to; using `!` as ```shell curl \ -H 'X-Api-Key: secret' \ - https://example.com/api/logs/15 + https://example.com/api/logs/1 ``` > JSON output format @@ -68,7 +68,7 @@ curl \ "message": "", "data": { "id": 1, - "repo_id": 3, + "target_id": 3, "start_time": 1652008554, "end_time": 1652008559, "arch": "x86_64", @@ -129,11 +129,11 @@ Publish a new build log to the server. Parameter | Description --------- | ----------- -id | ID of requested log startTime | Start time of the build (UTC epoch) endTime | End time of the build (UTC epoch) arch | Architecture on which the build was done exitCode | Exit code of the build container +target | id of target this build is for ### Request body diff --git a/docs/api/source/includes/_git.md b/docs/api/source/includes/_targets.md similarity index 71% rename from docs/api/source/includes/_git.md rename to docs/api/source/includes/_targets.md index 8458834e..05cf0015 100644 --- a/docs/api/source/includes/_git.md +++ b/docs/api/source/includes/_targets.md @@ -1,4 +1,4 @@ -# Git Repositories +# Targets -Endpoints for interacting with the list of Git repositories stored on the -server. +Endpoints for interacting with the list of targets stored on the server. -## List repos +## List targets ```shell curl \ -H 'X-Api-Key: secret' \ - https://example.com/api/repos?offset=10&limit=20 + https://example.com/api/targets?offset=10&limit=20 ``` > JSON output format @@ -32,7 +31,7 @@ curl \ "arch": [ { "id": 1, - "repo_id": 1, + "target_id": 1, "value": "x86_64" } ] @@ -41,11 +40,11 @@ curl \ } ``` -Retrieve a list of Git repositories. +Retrieve a list of targets. ### HTTP Request -`GET /api/repos` +`GET /api/targets` ### Query Parameters @@ -53,14 +52,14 @@ Parameter | Description --------- | ----------- limit | Maximum amount of results to return. offset | Offset of results. -repo | Limit results to repositories that publish to the given repo. +repo | Limit results to targets that publish to the given repo. -## Get a repo +## Get specific target ```shell curl \ -H 'X-Api-Key: secret' \ - https://example.com/api/repos/15 + https://example.com/api/targets/1 ``` > JSON output format @@ -77,7 +76,7 @@ curl \ "arch": [ { "id": 1, - "repo_id": 1, + "target_id": 1, "value": "x86_64" } ] @@ -85,25 +84,25 @@ curl \ } ``` -Get info about a specific Git repository. +Get info about a specific target. ### HTTP Request -`GET /api/repos/:id` +`GET /api/targets/:id` ### URL Parameters Parameter | Description --------- | ----------- -id | ID of requested repo +id | id of requested target -## Create a new repo +## Create a new target -Create a new Git repository with the given data. +Create a new target with the given data. ### HTTP Request -`POST /api/repos` +`POST /api/targets` ### Query Parameters @@ -115,19 +114,19 @@ repo | Vieter repository to publish built packages to. schedule | Cron build schedule (syntax explained [here](https://rustybever.be/docs/vieter/usage/builds/schedule/)) arch | Comma-separated list of architectures to build package on. -## Modify a repo +## Modify a target -Modify the data of an existing Git repository. +Modify the data of an existing target. ### HTTP Request -`PATCH /api/repos/:id` +`PATCH /api/targets/:id` ### URL Parameters Parameter | Description --------- | ----------- -id | ID of requested repo +id | id of target to modify ### Query Parameters @@ -139,16 +138,16 @@ repo | Vieter repository to publish built packages to. schedule | Cron build schedule arch | Comma-separated list of architectures to build package on. -## Remove a repo +## Remove a target -Remove a Git repository from the server. +Remove a target from the server. ### HTTP Request -`DELETE /api/repos/:id` +`DELETE /api/targets/:id` ### URL Parameters Parameter | Description --------- | ----------- -id | ID of repo to remove +id | id of target to remove diff --git a/docs/api/source/index.html.md b/docs/api/source/index.html.md index 74774982..4bfddb8e 100644 --- a/docs/api/source/index.html.md +++ b/docs/api/source/index.html.md @@ -9,7 +9,7 @@ toc_footers: includes: - repository - - git + - targets - logs search: true From fcdcf9c5ca0985c9fbaded2e9580e3f5c90b79dc Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Wed, 15 Jun 2022 12:10:58 +0200 Subject: [PATCH 077/236] feat(server): add config option for server port --- CHANGELOG.md | 6 ++++++ docs/content/configuration.md | 3 ++- src/server/cli.v | 1 + src/server/server.v | 3 +-- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9adee075..5fe1de7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://git.rustybever.be/vieter/vieter/src/branch/dev) +### Added + +* Server port can now be configured + ### Changed * Moved all API routes under `/v1` namespace +* Renamed `vieter repos` to `vieter targets` +* Renamed `/v1/api/repos` namespace to `/v1/api/targets` ## [0.3.0](https://git.rustybever.be/vieter/vieter/src/tag/0.3.0) diff --git a/docs/content/configuration.md b/docs/content/configuration.md index 600c6f35..f4bf9649 100644 --- a/docs/content/configuration.md +++ b/docs/content/configuration.md @@ -45,7 +45,8 @@ configuration variable required for each command. This prevents the server from being confused when an `any` package is published as the very first package for a repository. * Git repositories added without an `arch` value use this value instead. - +* `port`: HTTP port to run on + * Default: `8000` ### `vieter cron` diff --git a/src/server/cli.v b/src/server/cli.v index 53393f70..a62f56ec 100644 --- a/src/server/cli.v +++ b/src/server/cli.v @@ -10,6 +10,7 @@ pub: data_dir string api_key string default_arch string + port int = 8000 } // cmd returns the cli submodule that handles starting the server diff --git a/src/server/server.v b/src/server/server.v index 2309ee7b..1a9df3f8 100644 --- a/src/server/server.v +++ b/src/server/server.v @@ -8,7 +8,6 @@ import util import db const ( - port = 8000 log_file_name = 'vieter.log' repo_dir_name = 'repos' db_file_name = 'vieter.sqlite' @@ -77,5 +76,5 @@ pub fn server(conf Config) ? { conf: conf repo: repo db: db - }, server.port) + }, conf.port) } From cf94b644005d4d11f45b99a053ad665204c9603c Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Thu, 16 Jun 2022 17:38:41 +0200 Subject: [PATCH 078/236] docs: forgot some renames --- CHANGELOG.md | 2 +- docs/content/configuration.md | 6 +++--- docs/content/usage/builds/_index.md | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fe1de7b..109e65c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Moved all API routes under `/v1` namespace * Renamed `vieter repos` to `vieter targets` -* Renamed `/v1/api/repos` namespace to `/v1/api/targets` +* Renamed `/api/v1/repos` namespace to `/api/v1/targets` ## [0.3.0](https://git.rustybever.be/vieter/vieter/src/tag/0.3.0) diff --git a/docs/content/configuration.md b/docs/content/configuration.md index f4bf9649..af941a22 100644 --- a/docs/content/configuration.md +++ b/docs/content/configuration.md @@ -26,7 +26,7 @@ secrets file. ## Commands The first argument passed to Vieter determines which command you wish to use. -Each of these can contain subcommands (e.g. `vieter repos list`), but all +Each of these can contain subcommands (e.g. `vieter targets list`), but all subcommands will use the same configuration. Below you can find the configuration variable required for each command. @@ -89,11 +89,11 @@ configuration variable required for each command. * `api_key`: the API key to use when authenticating requests. * `address`: Base URL of your Vieter instance, e.g. https://example.com -### `vieter repos` +### `vieter targets` * `api_key`: the API key to use when authenticating requests. * `address`: Base URL of your Vieter instance, e.g. https://example.com -* `base_image`: image to use when building a package using `vieter repos +* `base_image`: image to use when building a package using `vieter targets build`. * Default: `archlinux:base-devel` diff --git a/docs/content/usage/builds/_index.md b/docs/content/usage/builds/_index.md index cd463a4b..7babb062 100644 --- a/docs/content/usage/builds/_index.md +++ b/docs/content/usage/builds/_index.md @@ -16,11 +16,11 @@ info to the system. The Vieter repository server exposes an HTTP API for this info). For ease of use, the Vieter binary contains a CLI interface for interacting with this API (see [Configuration](/configuration) for configuration details). The [man -pages](https://rustybever.be/man/vieter/vieter-repos.1.html) describe this in +pages](https://rustybever.be/man/vieter/vieter-targets.1.html) describe this in greater detail, but the basic usage is as follows: ``` -vieter repos add some-url some-branch some-repository +vieter targets add some-url some-branch some-repository ``` Here, `some-url` is the URL of the Git repository containing the PKGBUILD. This @@ -37,7 +37,7 @@ should be published. The above command intentionally leaves out a few parameters to make the CLI more useable. For information on how to modify all parameters using the CLI, see -[vieter-repos-edit(1)](https://rustybever.be/man/vieter/vieter-repos-edit.1.html). +[vieter-targets-edit(1)](https://rustybever.be/man/vieter/vieter-targets-edit.1.html). ## Reading logs From 3d38df6d038a2e75053461ad0c2829d10fb8a4e2 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Thu, 16 Jun 2022 18:10:05 +0200 Subject: [PATCH 079/236] fix(client): use new "target" name for param --- src/client/logs.v | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/logs.v b/src/client/logs.v index 49ef679b..f242f6e8 100644 --- a/src/client/logs.v +++ b/src/client/logs.v @@ -39,9 +39,9 @@ pub fn (c &Client) get_build_log_content(id int) ?string { } // add_build_log adds a new build log to the server. -pub fn (c &Client) add_build_log(repo_id int, start_time time.Time, end_time time.Time, arch string, exit_code int, content string) ?Response { +pub fn (c &Client) add_build_log(target_id int, start_time time.Time, end_time time.Time, arch string, exit_code int, content string) ?Response { params := { - 'repo': repo_id.str() + 'target': target_id.str() 'startTime': start_time.unix_time().str() 'endTime': end_time.unix_time().str() 'arch': arch From 1b7c14e7dcd8850d622687e27eb454c6f8d5b05c Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Wed, 15 Jun 2022 22:15:11 +0200 Subject: [PATCH 080/236] feat(server): no longer calculate md5 hashes for packages --- CHANGELOG.md | 4 ++++ src/package/package.v | 10 ++++------ src/repo/repo.v | 2 +- src/util/util.v | 19 ++++++------------- 4 files changed, 15 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 109e65c6..d01409a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Renamed `vieter repos` to `vieter targets` * Renamed `/api/v1/repos` namespace to `/api/v1/targets` +### Removed + +* md5 hashes are no longer calculated for packages + ## [0.3.0](https://git.rustybever.be/vieter/vieter/src/tag/0.3.0) Nothing besides bumping the versions. diff --git a/src/package/package.v b/src/package/package.v index 273322fa..86bf40ad 100644 --- a/src/package/package.v +++ b/src/package/package.v @@ -42,8 +42,8 @@ pub mut: checkdepends []string } -// checksum calculates the md5 & sha256 hash of the package -pub fn (p &Pkg) checksum() ?(string, string) { +// checksum calculates the sha256 hash of the package +pub fn (p &Pkg) checksum() ?string { return util.hash_file(p.path) } @@ -201,8 +201,7 @@ pub fn (pkg &Pkg) filename() string { } // to_desc returns a desc file valid string representation -// TODO calculate md5 & sha256 instead of believing the file -pub fn (pkg &Pkg) to_desc() string { +pub fn (pkg &Pkg) to_desc() ?string { p := pkg.info // filename @@ -223,9 +222,8 @@ pub fn (pkg &Pkg) to_desc() string { desc += format_entry('CSIZE', p.csize.str()) desc += format_entry('ISIZE', p.size.str()) - md5sum, sha256sum := pkg.checksum() or { '', '' } + sha256sum := pkg.checksum()? - desc += format_entry('MD5SUM', md5sum) desc += format_entry('SHA256SUM', sha256sum) // TODO add pgpsig stuff diff --git a/src/repo/repo.v b/src/repo/repo.v index 817ec304..c4b85c0b 100644 --- a/src/repo/repo.v +++ b/src/repo/repo.v @@ -139,7 +139,7 @@ fn (r &RepoGroupManager) add_pkg_in_arch_repo(repo string, arch string, pkg &pac os.mkdir_all(pkg_dir) or { return error('Failed to create package directory.') } - os.write_file(os.join_path_single(pkg_dir, 'desc'), pkg.to_desc()) or { + os.write_file(os.join_path_single(pkg_dir, 'desc'), pkg.to_desc()?) or { os.rmdir_all(pkg_dir)? return error('Failed to write desc file.') diff --git a/src/util/util.v b/src/util/util.v index 266bcb5b..4cd374e7 100644 --- a/src/util/util.v +++ b/src/util/util.v @@ -1,7 +1,6 @@ module util import os -import crypto.md5 import crypto.sha256 const ( @@ -23,12 +22,10 @@ pub fn exit_with_message(code int, msg string) { exit(code) } -// hash_file returns the md5 & sha256 hash of a given file -// TODO actually implement sha256 -pub fn hash_file(path &string) ?(string, string) { +// hash_file returns the sha256 hash of a given file +pub fn hash_file(path &string) ?string { file := os.open(path) or { return error('Failed to open file.') } - mut md5sum := md5.new() mut sha256sum := sha256.new() buf_size := int(1_000_000) @@ -40,16 +37,12 @@ pub fn hash_file(path &string) ?(string, string) { bytes_read := file.read(mut buf) or { return error('Failed to read from file.') } bytes_left -= u64(bytes_read) - // For now we'll assume that this always works - md5sum.write(buf[..bytes_read]) or { - return error('Failed to update md5 checksum. This should never happen.') - } - sha256sum.write(buf[..bytes_read]) or { - return error('Failed to update sha256 checksum. This should never happen.') - } + // This function never actually fails, but returns an option to follow + // the Writer interface. + sha256sum.write(buf[..bytes_read])? } - return md5sum.checksum().hex(), sha256sum.checksum().hex() + return sha256sum.checksum().hex() } // pretty_bytes converts a byte count to human-readable version From a8d647cca365dec1c036c055783d9cd3ef632973 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Thu, 16 Jun 2022 22:38:42 +0200 Subject: [PATCH 081/236] fix(docs): use versioned endpoints in HTTP API docs --- docs/api/source/includes/_logs.md | 14 +++++++------- docs/api/source/includes/_targets.md | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/api/source/includes/_logs.md b/docs/api/source/includes/_logs.md index a38bf484..2797e60d 100644 --- a/docs/api/source/includes/_logs.md +++ b/docs/api/source/includes/_logs.md @@ -13,7 +13,7 @@ Endpoints for interacting with stored build logs. ```shell curl \ -H 'X-Api-Key: secret' \ - https://example.com/api/logs?offset=10&limit=20 + https://example.com/api/v1/logs?offset=10&limit=20 ``` > JSON output format @@ -38,7 +38,7 @@ Retrieve a list of build logs. ### HTTP Request -`GET /api/logs` +`GET /api/v1/logs` ### Query Parameters @@ -58,7 +58,7 @@ exit_codes | Comma-separated list of exit codes to limit result to; using `!` as ```shell curl \ -H 'X-Api-Key: secret' \ - https://example.com/api/logs/1 + https://example.com/api/v1/logs/1 ``` > JSON output format @@ -81,7 +81,7 @@ Retrieve info about a specific build log. ### HTTP Request -`GET /api/logs/:id` +`GET /api/v1/logs/:id` ### URL Parameters @@ -94,7 +94,7 @@ id | ID of requested log ```shell curl \ -H 'X-Api-Key: secret' \ - https://example.com/api/logs/15/content + https://example.com/api/v1/logs/15/content ``` Retrieve the contents of a build log. The response is the build log in @@ -102,7 +102,7 @@ plaintext. ### HTTP Request -`GET /api/logs/:id/content` +`GET /api/v1/logs/:id/content` ### URL Parameters @@ -123,7 +123,7 @@ Publish a new build log to the server. ### HTTP Request -`POST /api/logs` +`POST /api/v1/logs` ### Query parameters diff --git a/docs/api/source/includes/_targets.md b/docs/api/source/includes/_targets.md index 05cf0015..f7ea21e5 100644 --- a/docs/api/source/includes/_targets.md +++ b/docs/api/source/includes/_targets.md @@ -13,7 +13,7 @@ Endpoints for interacting with the list of targets stored on the server. ```shell curl \ -H 'X-Api-Key: secret' \ - https://example.com/api/targets?offset=10&limit=20 + https://example.com/api/v1/targets?offset=10&limit=20 ``` > JSON output format @@ -44,7 +44,7 @@ Retrieve a list of targets. ### HTTP Request -`GET /api/targets` +`GET /api/v1/targets` ### Query Parameters @@ -59,7 +59,7 @@ repo | Limit results to targets that publish to the given repo. ```shell curl \ -H 'X-Api-Key: secret' \ - https://example.com/api/targets/1 + https://example.com/api/v1/targets/1 ``` > JSON output format @@ -88,7 +88,7 @@ Get info about a specific target. ### HTTP Request -`GET /api/targets/:id` +`GET /api/v1/targets/:id` ### URL Parameters @@ -102,7 +102,7 @@ Create a new target with the given data. ### HTTP Request -`POST /api/targets` +`POST /api/v1/targets` ### Query Parameters @@ -120,7 +120,7 @@ Modify the data of an existing target. ### HTTP Request -`PATCH /api/targets/:id` +`PATCH /api/v1/targets/:id` ### URL Parameters @@ -144,7 +144,7 @@ Remove a target from the server. ### HTTP Request -`DELETE /api/targets/:id` +`DELETE /api/v1/targets/:id` ### URL Parameters From bb5643bb036eff0d8ece2a94ad05d14b42dcf901 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Fri, 17 Jun 2022 13:45:21 +0200 Subject: [PATCH 082/236] feat: added ability to specify kind of target --- src/client/targets.v | 21 +++++----- src/console/targets/targets.v | 40 ++++++++++++++----- src/db/db.v | 8 +++- .../migrations/003-target-url-type/down.sql | 4 ++ src/db/migrations/003-target-url-type/up.sql | 1 + src/models/targets.v | 9 +++-- 6 files changed, 58 insertions(+), 25 deletions(-) create mode 100644 src/db/migrations/003-target-url-type/down.sql create mode 100644 src/db/migrations/003-target-url-type/up.sql diff --git a/src/client/targets.v b/src/client/targets.v index 86d8c2a9..d7cc4161 100644 --- a/src/client/targets.v +++ b/src/client/targets.v @@ -40,18 +40,17 @@ pub fn (c &Client) get_target(id int) ?Target { return data.data } +pub struct NewTarget { + kind string + url string + branch string + repo string + arch []string +} + // add_target adds a new target to the server. -pub fn (c &Client) add_target(url string, branch string, repo string, arch []string) ?Response { - mut params := { - 'url': url - 'branch': branch - 'repo': repo - } - - if arch.len > 0 { - params['arch'] = arch.join(',') - } - +pub fn (c &Client) add_target(t NewTarget) ?Response { + params := models.params_from(t) data := c.send_request(Method.post, '/api/v1/targets', params)? return data diff --git a/src/console/targets/targets.v b/src/console/targets/targets.v index 530184d4..71a5c92c 100644 --- a/src/console/targets/targets.v +++ b/src/console/targets/targets.v @@ -3,7 +3,7 @@ module targets import cli import vieter.vconf import cron.expression { parse_expression } -import client +import client { NewTarget } import console import models { TargetFilter } @@ -65,14 +65,36 @@ pub fn cmd() cli.Command { }, cli.Command{ name: 'add' - required_args: 3 - usage: 'url branch repo' - description: 'Add a new Git repository target.' + required_args: 2 + usage: 'url repo' + description: "Add a new target with the given URL & target repo." + flags: [ + cli.Flag{ + name: 'kind' + description: "Kind of target to add. Defaults to 'git' if not specified. One of 'git', 'url'." + flag: cli.FlagType.string + default_value: ['git'] + }, + cli.Flag{ + name: 'branch' + description: "Which branch to clone; only applies to kind 'git'." + flag: cli.FlagType.string + } + ] execute: fn (cmd cli.Command) ? { config_file := cmd.flags.get_string('config-file')? conf := vconf.load(prefix: 'VIETER_', default_path: config_file)? - add(conf, cmd.args[0], cmd.args[1], cmd.args[2])? + kind := cmd.flags.get_string('kind')? + + t := NewTarget{ + kind: cmd.flags.get_string('kind')? + url: cmd.args[0] + repo: cmd.args[1] + branch: cmd.flags.get_string('branch') or { '' } + } + + add(conf, t)? } }, cli.Command{ @@ -171,15 +193,15 @@ pub fn cmd() cli.Command { fn list(conf Config, filter TargetFilter) ? { c := client.new(conf.address, conf.api_key) repos := c.get_targets(filter)? - data := repos.map([it.id.str(), it.url, it.branch, it.repo]) + data := repos.map([it.id.str(), it.kind, it.url, it.repo]) - println(console.pretty_table(['id', 'url', 'branch', 'repo'], data)?) + println(console.pretty_table(['id', 'kind', 'url', 'repo'], data)?) } // add adds a new repository to the server's list. -fn add(conf Config, url string, branch string, repo string) ? { +fn add(conf Config, t &NewTarget) ? { c := client.new(conf.address, conf.api_key) - res := c.add_target(url, branch, repo, [])? + res := c.add_target(t)? println(res.message) } diff --git a/src/db/db.v b/src/db/db.v index 64a57d22..eaf71abb 100644 --- a/src/db/db.v +++ b/src/db/db.v @@ -16,9 +16,13 @@ const ( migrations_up = [ $embed_file('migrations/001-initial/up.sql'), $embed_file('migrations/002-rename-to-targets/up.sql'), + $embed_file('migrations/003-target-url-type/up.sql'), + ] + migrations_down = [ + $embed_file('migrations/001-initial/down.sql'), + $embed_file('migrations/002-rename-to-targets/down.sql'), + $embed_file('migrations/003-target-url-type/down.sql'), ] - migrations_down = [$embed_file('migrations/001-initial/down.sql'), - $embed_file('migrations/002-rename-to-targets/down.sql')] ) // init initializes a database & adds the correct tables. diff --git a/src/db/migrations/003-target-url-type/down.sql b/src/db/migrations/003-target-url-type/down.sql new file mode 100644 index 00000000..9d9b45c6 --- /dev/null +++ b/src/db/migrations/003-target-url-type/down.sql @@ -0,0 +1,4 @@ +-- I'm not sure whether I should remove any non-git targets here. Keeping them +-- will result in invalid targets, but removing them means losing data. +ALTER TABLE Target DROP COLUMN kind; + diff --git a/src/db/migrations/003-target-url-type/up.sql b/src/db/migrations/003-target-url-type/up.sql new file mode 100644 index 00000000..f6be4f47 --- /dev/null +++ b/src/db/migrations/003-target-url-type/up.sql @@ -0,0 +1 @@ +ALTER TABLE Target ADD COLUMN kind TEXT NOT NULL DEFAULT 'git'; diff --git a/src/models/targets.v b/src/models/targets.v index 537bbc4d..b8fc88a8 100644 --- a/src/models/targets.v +++ b/src/models/targets.v @@ -15,10 +15,13 @@ pub fn (gra &TargetArch) str() string { pub struct Target { pub mut: id int [primary; sql: serial] - // URL of the Git repository + kind string [nonull] + // If kind is git: URL of the Git repository + // If kind is url: URL to PKGBUILD file url string [nonull] - // Branch of the Git repository to use - branch string [nonull] + // Branch of the Git repository to use; only applicable when kind is git. + // If not provided, the repository is cloned with the default branch. + branch string // Which repo the builder should publish packages to repo string [nonull] // Cron schedule describing how frequently to build the repo. From bd079645090280eecd5e827ddd03220c8f9f53dd Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Fri, 17 Jun 2022 13:56:38 +0200 Subject: [PATCH 083/236] feat(api): prevent invalid kind values --- src/client/targets.v | 8 ++++---- src/console/targets/targets.v | 13 ++++++++----- src/models/targets.v | 5 ++++- src/server/api_targets.v | 5 +++++ 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/client/targets.v b/src/client/targets.v index d7cc4161..82c78780 100644 --- a/src/client/targets.v +++ b/src/client/targets.v @@ -41,11 +41,11 @@ pub fn (c &Client) get_target(id int) ?Target { } pub struct NewTarget { - kind string - url string + kind string + url string branch string - repo string - arch []string + repo string + arch []string } // add_target adds a new target to the server. diff --git a/src/console/targets/targets.v b/src/console/targets/targets.v index 71a5c92c..a9934f1f 100644 --- a/src/console/targets/targets.v +++ b/src/console/targets/targets.v @@ -67,7 +67,7 @@ pub fn cmd() cli.Command { name: 'add' required_args: 2 usage: 'url repo' - description: "Add a new target with the given URL & target repo." + description: 'Add a new target with the given URL & target repo.' flags: [ cli.Flag{ name: 'kind' @@ -79,14 +79,12 @@ pub fn cmd() cli.Command { name: 'branch' description: "Which branch to clone; only applies to kind 'git'." flag: cli.FlagType.string - } + }, ] execute: fn (cmd cli.Command) ? { config_file := cmd.flags.get_string('config-file')? conf := vconf.load(prefix: 'VIETER_', default_path: config_file)? - kind := cmd.flags.get_string('kind')? - t := NewTarget{ kind: cmd.flags.get_string('kind')? url: cmd.args[0] @@ -152,6 +150,11 @@ pub fn cmd() cli.Command { description: 'Cron schedule for repository.' flag: cli.FlagType.string }, + cli.Flag{ + name: 'kind' + description: 'Kind of target.' + flag: cli.FlagType.string + }, ] execute: fn (cmd cli.Command) ? { config_file := cmd.flags.get_string('config-file')? @@ -195,7 +198,7 @@ fn list(conf Config, filter TargetFilter) ? { repos := c.get_targets(filter)? data := repos.map([it.id.str(), it.kind, it.url, it.repo]) - println(console.pretty_table(['id', 'kind', 'url', 'repo'], data)?) + println(console.pretty_table(['id', 'kind', 'url', 'repo'], data)?) } // add adds a new repository to the server's list. diff --git a/src/models/targets.v b/src/models/targets.v index b8fc88a8..c8aa5357 100644 --- a/src/models/targets.v +++ b/src/models/targets.v @@ -1,5 +1,7 @@ module models +pub const valid_kinds = ['git', 'url'] + pub struct TargetArch { pub: id int [primary; sql: serial] @@ -14,7 +16,7 @@ pub fn (gra &TargetArch) str() string { pub struct Target { pub mut: - id int [primary; sql: serial] + id int [primary; sql: serial] kind string [nonull] // If kind is git: URL of the Git repository // If kind is url: URL to PKGBUILD file @@ -35,6 +37,7 @@ pub mut: pub fn (gr &Target) str() string { mut parts := [ 'id: $gr.id', + 'kind: $gr.kind', 'url: $gr.url', 'branch: $gr.branch', 'repo: $gr.repo', diff --git a/src/server/api_targets.v b/src/server/api_targets.v index f807fe94..3867c94b 100644 --- a/src/server/api_targets.v +++ b/src/server/api_targets.v @@ -52,6 +52,11 @@ fn (mut app App) v1_post_target() web.Result { return app.json(http.Status.bad_request, new_response(err.msg())) } + // Ensure someone doesn't submit an invalid kind + if new_repo.kind !in models.valid_kinds { + return app.json(http.Status.bad_request, new_response('Invalid kind.')) + } + app.db.add_target(new_repo) return app.json(http.Status.ok, new_response('Repo added successfully.')) From 8f91c1fde52fd7d3a6a11e4c3fa54e0cc414afd5 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Fri, 17 Jun 2022 14:31:34 +0200 Subject: [PATCH 084/236] feat(build): added support for 'url' kind --- .../{build_script.sh => build_script_git.sh} | 4 +- src/build/build_script_git_branch.sh | 20 ++++++++++ src/build/build_script_url.sh | 22 +++++++++++ src/build/shell.v | 39 +++++++++++++++---- src/build/shell_test.v | 31 ++++++++++++++- src/console/targets/targets.v | 5 +-- 6 files changed, 107 insertions(+), 14 deletions(-) rename src/build/{build_script.sh => build_script_git.sh} (87%) create mode 100644 src/build/build_script_git_branch.sh create mode 100644 src/build/build_script_url.sh diff --git a/src/build/build_script.sh b/src/build/build_script_git.sh similarity index 87% rename from src/build/build_script.sh rename to src/build/build_script_git.sh index 29f163ed..73e0965d 100644 --- a/src/build/build_script.sh +++ b/src/build/build_script_git.sh @@ -4,8 +4,8 @@ echo -e '+ pacman -Syu --needed --noconfirm' pacman -Syu --needed --noconfirm echo -e '+ su builder' su builder -echo -e '+ git clone --single-branch --depth 1 --branch main https://examplerepo.com repo' -git clone --single-branch --depth 1 --branch main https://examplerepo.com repo +echo -e '+ git clone --single-branch --depth 1 '\''https://examplerepo.com'\'' repo' +git clone --single-branch --depth 1 'https://examplerepo.com' repo echo -e '+ cd repo' cd repo echo -e '+ makepkg --nobuild --syncdeps --needed --noconfirm' diff --git a/src/build/build_script_git_branch.sh b/src/build/build_script_git_branch.sh new file mode 100644 index 00000000..be1ff4fb --- /dev/null +++ b/src/build/build_script_git_branch.sh @@ -0,0 +1,20 @@ +echo -e '+ echo -e '\''[vieter]\\nServer = https://example.com/$repo/$arch\\nSigLevel = Optional'\'' >> /etc/pacman.conf' +echo -e '[vieter]\nServer = https://example.com/$repo/$arch\nSigLevel = Optional' >> /etc/pacman.conf +echo -e '+ pacman -Syu --needed --noconfirm' +pacman -Syu --needed --noconfirm +echo -e '+ su builder' +su builder +echo -e '+ git clone --single-branch --depth 1 --branch main '\''https://examplerepo.com'\'' repo' +git clone --single-branch --depth 1 --branch main 'https://examplerepo.com' repo +echo -e '+ cd repo' +cd repo +echo -e '+ makepkg --nobuild --syncdeps --needed --noconfirm' +makepkg --nobuild --syncdeps --needed --noconfirm +echo -e '+ source PKGBUILD' +source PKGBUILD +echo -e '+ curl -s --head --fail https://example.com/vieter/x86_64/$pkgname-$pkgver-$pkgrel && exit 0' +curl -s --head --fail https://example.com/vieter/x86_64/$pkgname-$pkgver-$pkgrel && exit 0 +echo -e '+ [ "$(id -u)" == 0 ] && exit 0' +[ "$(id -u)" == 0 ] && exit 0 +echo -e '+ MAKEFLAGS="-j$(nproc)" makepkg -s --noconfirm --needed && for pkg in $(ls -1 *.pkg*); do curl -XPOST -T "$pkg" -H "X-API-KEY: $API_KEY" https://example.com/vieter/publish; done' +MAKEFLAGS="-j$(nproc)" makepkg -s --noconfirm --needed && for pkg in $(ls -1 *.pkg*); do curl -XPOST -T "$pkg" -H "X-API-KEY: $API_KEY" https://example.com/vieter/publish; done diff --git a/src/build/build_script_url.sh b/src/build/build_script_url.sh new file mode 100644 index 00000000..3bc97e12 --- /dev/null +++ b/src/build/build_script_url.sh @@ -0,0 +1,22 @@ +echo -e '+ echo -e '\''[vieter]\\nServer = https://example.com/$repo/$arch\\nSigLevel = Optional'\'' >> /etc/pacman.conf' +echo -e '[vieter]\nServer = https://example.com/$repo/$arch\nSigLevel = Optional' >> /etc/pacman.conf +echo -e '+ pacman -Syu --needed --noconfirm' +pacman -Syu --needed --noconfirm +echo -e '+ su builder' +su builder +echo -e '+ mkdir repo' +mkdir repo +echo -e '+ curl -o repo/PKGBUILD -L '\''https://examplerepo.com'\''' +curl -o repo/PKGBUILD -L 'https://examplerepo.com' +echo -e '+ cd repo' +cd repo +echo -e '+ makepkg --nobuild --syncdeps --needed --noconfirm' +makepkg --nobuild --syncdeps --needed --noconfirm +echo -e '+ source PKGBUILD' +source PKGBUILD +echo -e '+ curl -s --head --fail https://example.com/vieter/x86_64/$pkgname-$pkgver-$pkgrel && exit 0' +curl -s --head --fail https://example.com/vieter/x86_64/$pkgname-$pkgver-$pkgrel && exit 0 +echo -e '+ [ "$(id -u)" == 0 ] && exit 0' +[ "$(id -u)" == 0 ] && exit 0 +echo -e '+ MAKEFLAGS="-j$(nproc)" makepkg -s --noconfirm --needed && for pkg in $(ls -1 *.pkg*); do curl -XPOST -T "$pkg" -H "X-API-KEY: $API_KEY" https://example.com/vieter/publish; done' +MAKEFLAGS="-j$(nproc)" makepkg -s --noconfirm --needed && for pkg in $(ls -1 *.pkg*); do curl -XPOST -T "$pkg" -H "X-API-KEY: $API_KEY" https://example.com/vieter/publish; done diff --git a/src/build/shell.v b/src/build/shell.v index c5944bf5..e573d53c 100644 --- a/src/build/shell.v +++ b/src/build/shell.v @@ -23,20 +23,45 @@ pub fn echo_commands(cmds []string) []string { } // create_build_script generates a shell script that builds a given Target. -fn create_build_script(address string, repo &Target, build_arch string) string { - repo_url := '$address/$repo.repo' +fn create_build_script(address string, target &Target, build_arch string) string { + repo_url := '$address/$target.repo' - commands := echo_commands([ + mut commands := [ // This will later be replaced by a proper setting for changing the // mirrorlist - "echo -e '[$repo.repo]\\nServer = $address/\$repo/\$arch\\nSigLevel = Optional' >> /etc/pacman.conf" + "echo -e '[$target.repo]\\nServer = $address/\$repo/\$arch\\nSigLevel = Optional' >> /etc/pacman.conf" // We need to update the package list of the repo we just added above. // This should however not pull in a lot of packages as long as the // builder image is rebuilt frequently. 'pacman -Syu --needed --noconfirm', // makepkg can't run as root 'su builder', - 'git clone --single-branch --depth 1 --branch $repo.branch $repo.url repo', + ] + + commands << match target.kind { + 'git' { + if target.branch == '' { + [ + "git clone --single-branch --depth 1 '$target.url' repo", + ] + } else { + [ + "git clone --single-branch --depth 1 --branch $target.branch '$target.url' repo", + ] + } + } + 'url' { + [ + 'mkdir repo', + "curl -o repo/PKGBUILD -L '$target.url'", + ] + } + else { + panic("Invalid kind. This shouldn't be possible.") + } + } + + commands << [ 'cd repo', 'makepkg --nobuild --syncdeps --needed --noconfirm', 'source PKGBUILD', @@ -49,7 +74,7 @@ fn create_build_script(address string, repo &Target, build_arch string) string { // we're in root so we don't proceed. '[ "\$(id -u)" == 0 ] && exit 0', 'MAKEFLAGS="-j\$(nproc)" makepkg -s --noconfirm --needed && for pkg in \$(ls -1 *.pkg*); do curl -XPOST -T "\$pkg" -H "X-API-KEY: \$API_KEY" $repo_url/publish; done', - ]) + ] - return commands.join('\n') + return echo_commands(commands).join('\n') } diff --git a/src/build/shell_test.v b/src/build/shell_test.v index 85340549..341df888 100644 --- a/src/build/shell_test.v +++ b/src/build/shell_test.v @@ -2,15 +2,42 @@ module build import models { Target } -fn test_create_build_script() { +fn test_create_build_script_git_branch() { target := Target{ id: 1 + kind: 'git' url: 'https://examplerepo.com' branch: 'main' repo: 'vieter' } build_script := create_build_script('https://example.com', target, 'x86_64') - expected := $embed_file('build_script.sh') + expected := $embed_file('build_script_git_branch.sh') + + assert build_script == expected.to_string().trim_space() +} + +fn test_create_build_script_git() { + target := Target{ + id: 1 + kind: 'git' + url: 'https://examplerepo.com' + repo: 'vieter' + } + build_script := create_build_script('https://example.com', target, 'x86_64') + expected := $embed_file('build_script_git.sh') + + assert build_script == expected.to_string().trim_space() +} + +fn test_create_build_script_url() { + target := Target{ + id: 1 + kind: 'url' + url: 'https://examplerepo.com' + repo: 'vieter' + } + build_script := create_build_script('https://example.com', target, 'x86_64') + expected := $embed_file('build_script_url.sh') assert build_script == expected.to_string().trim_space() } diff --git a/src/console/targets/targets.v b/src/console/targets/targets.v index a9934f1f..ce1cd149 100644 --- a/src/console/targets/targets.v +++ b/src/console/targets/targets.v @@ -123,11 +123,11 @@ pub fn cmd() cli.Command { name: 'edit' required_args: 1 usage: 'id' - description: 'Edit the Git repository target that matches the given id.' + description: 'Edit the target that matches the given id.' flags: [ cli.Flag{ name: 'url' - description: 'URL of the Git repository.' + description: 'URL value. Meaning depends on kind of target.' flag: cli.FlagType.string }, cli.Flag{ @@ -211,7 +211,6 @@ fn add(conf Config, t &NewTarget) ? { // remove removes a repository from the server's list. fn remove(conf Config, id string) ? { - // id, _ := get_repo_by_prefix(conf, id_prefix) ? id_int := id.int() if id_int != 0 { From 449656eb97a016fdf5fc115414fc58d3c4445012 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Fri, 17 Jun 2022 14:52:59 +0200 Subject: [PATCH 085/236] docs: updated to new 'kind' field --- CHANGELOG.md | 6 ++++++ docs/api/source/includes/_targets.md | 4 ++++ docs/content/usage/builds/_index.md | 16 ++++++++-------- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d01409a0..eee1f9ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,12 +10,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added * Server port can now be configured +* Targets now have a 'kind' field describing whether it's a Git repository or a + URL to a PKGBUILD +* Targets with kind 'url' can provide a direct URL to a PKGBUILD instead of + providing a Git repository ### Changed * Moved all API routes under `/v1` namespace * Renamed `vieter repos` to `vieter targets` * Renamed `/api/v1/repos` namespace to `/api/v1/targets` +* Branch name for 'git' targets is now optional; if not provided, the + repository will be cloned with the default branch ### Removed diff --git a/docs/api/source/includes/_targets.md b/docs/api/source/includes/_targets.md index f7ea21e5..c7061c86 100644 --- a/docs/api/source/includes/_targets.md +++ b/docs/api/source/includes/_targets.md @@ -24,6 +24,7 @@ curl \ "data": [ { "id": 1, + "kind": "git", "url": "https://aur.archlinux.org/discord-ptb.git", "branch": "master", "repo": "bur", @@ -69,6 +70,7 @@ curl \ "message": "", "data": { "id": 1, + "kind": "git", "url": "https://aur.archlinux.org/discord-ptb.git", "branch": "master", "repo": "bur", @@ -108,6 +110,7 @@ Create a new target with the given data. Parameter | Description --------- | ----------- +kind | Kind of target to add; one of 'git', 'url'. url | URL of the Git repository. branch | Branch of the Git repository. repo | Vieter repository to publish built packages to. @@ -132,6 +135,7 @@ id | id of target to modify Parameter | Description --------- | ----------- +kind | Kind of target; one of 'git', 'url'. url | URL of the Git repository. branch | Branch of the Git repository. repo | Vieter repository to publish built packages to. diff --git a/docs/content/usage/builds/_index.md b/docs/content/usage/builds/_index.md index 7babb062..e6c0b1c7 100644 --- a/docs/content/usage/builds/_index.md +++ b/docs/content/usage/builds/_index.md @@ -20,24 +20,24 @@ pages](https://rustybever.be/man/vieter/vieter-targets.1.html) describe this in greater detail, but the basic usage is as follows: ``` -vieter targets add some-url some-branch some-repository +vieter targets add some-url some-repository ``` Here, `some-url` is the URL of the Git repository containing the PKGBUILD. This URL is passed to `git clone`, meaning the repository should be public. Vieter expects the same format as an AUR Git repository, so you can directly use AUR -URLs here. +URLs here. Alternatively, you can also provide the URL to a PKGBUILD file +instead. See +[vieter-targets-add(1)](https://rustybever.be/man/vieter/vieter-targets-add.1.html) +for more information. -`some-branch` is the branch of the Git repository the build should check out. -If you're using an AUR package, this should be `master`. - -Finally, `some-repo` is the repository to which the built package archives -should be published. +`some-repo` is the repository to which the built package archives should be +published. The above command intentionally leaves out a few parameters to make the CLI more useable. For information on how to modify all parameters using the CLI, see -[vieter-targets-edit(1)](https://rustybever.be/man/vieter/vieter-targets-edit.1.html). +[vieter-targets(1)](https://rustybever.be/man/vieter/vieter-targets.1.html). ## Reading logs From 5e11a91f3d1a7d0932b6d74598f36dff94cf008d Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Fri, 17 Jun 2022 16:23:47 +0200 Subject: [PATCH 086/236] refactor: renamed cron & build code to use "target" naming --- src/build/build.v | 6 ++-- src/console/targets/build.v | 8 ++--- src/cron/daemon/build.v | 20 +++++++----- src/cron/daemon/daemon.v | 65 +++++++++++++++++++------------------ 4 files changed, 51 insertions(+), 48 deletions(-) diff --git a/src/build/build.v b/src/build/build.v index 3c6b439a..cd0007fb 100644 --- a/src/build/build.v +++ b/src/build/build.v @@ -90,10 +90,10 @@ pub: logs string } -// build_repo builds, packages & publishes a given Arch package based on the +// build_target builds, packages & publishes a given Arch package based on the // provided target. The base image ID should be of an image previously created // by create_build_image. It returns the logs of the container. -pub fn build_repo(address string, api_key string, base_image_id string, repo &Target) ?BuildResult { +pub fn build_target(address string, api_key string, base_image_id string, target &Target) ?BuildResult { mut dd := docker.new_conn()? defer { @@ -101,7 +101,7 @@ pub fn build_repo(address string, api_key string, base_image_id string, repo &Ta } build_arch := os.uname().machine - build_script := create_build_script(address, repo, build_arch) + build_script := create_build_script(address, target, build_arch) // We convert the build script into a base64 string, which then gets passed // to the container as an env var diff --git a/src/console/targets/build.v b/src/console/targets/build.v index d50dfd2c..1f522a6c 100644 --- a/src/console/targets/build.v +++ b/src/console/targets/build.v @@ -6,9 +6,9 @@ import os import build // build locally builds the target with the given id. -fn build(conf Config, repo_id int) ? { +fn build(conf Config, target_id int) ? { c := client.new(conf.address, conf.api_key) - repo := c.get_target(repo_id)? + target := c.get_target(target_id)? build_arch := os.uname().machine @@ -16,7 +16,7 @@ fn build(conf Config, repo_id int) ? { image_id := build.create_build_image(conf.base_image)? println('Running build...') - res := build.build_repo(conf.address, conf.api_key, image_id, repo)? + res := build.build_target(conf.address, conf.api_key, image_id, target)? println('Removing build image...') @@ -29,6 +29,6 @@ fn build(conf Config, repo_id int) ? { dd.remove_image(image_id)? println('Uploading logs to Vieter...') - c.add_build_log(repo.id, res.start_time, res.end_time, build_arch, res.exit_code, + c.add_build_log(target.id, res.start_time, res.end_time, build_arch, res.exit_code, res.logs)? } diff --git a/src/cron/daemon/build.v b/src/cron/daemon/build.v index aa08f9f3..beed9fc8 100644 --- a/src/cron/daemon/build.v +++ b/src/cron/daemon/build.v @@ -71,29 +71,31 @@ fn (mut d Daemon) start_build(sb ScheduledBuild) bool { return false } -// run_build actually starts the build process for a given repo. +// run_build actually starts the build process for a given target. fn (mut d Daemon) run_build(build_index int, sb ScheduledBuild) { - d.linfo('started build: $sb.repo.url $sb.repo.branch') + d.linfo('started build: $sb.target.url -> $sb.target.repo') // 0 means success, 1 means failure mut status := 0 - res := build.build_repo(d.client.address, d.client.api_key, d.builder_images.last(), - &sb.repo) or { - d.ldebug('build_repo error: $err.msg()') + res := build.build_target(d.client.address, d.client.api_key, d.builder_images.last(), + &sb.target) or { + d.ldebug('build_target error: $err.msg()') status = 1 build.BuildResult{} } if status == 0 { - d.linfo('finished build: $sb.repo.url $sb.repo.branch; uploading logs...') + d.linfo('finished build: $sb.target.url -> $sb.target.repo; uploading logs...') build_arch := os.uname().machine - d.client.add_build_log(sb.repo.id, res.start_time, res.end_time, build_arch, res.exit_code, - res.logs) or { d.lerror('Failed to upload logs for $sb.repo.url $sb.repo.arch') } + d.client.add_build_log(sb.target.id, res.start_time, res.end_time, build_arch, + res.exit_code, res.logs) or { + d.lerror('Failed to upload logs for build: $sb.target.url -> $sb.target.repo') + } } else { - d.linfo('failed build: $sb.repo.url $sb.repo.branch') + d.linfo('an error occured during build: $sb.target.url -> $sb.target.repo') } stdatomic.store_u64(&d.atomics[build_index], daemon.build_done) diff --git a/src/cron/daemon/daemon.v b/src/cron/daemon/daemon.v index 7b514aa4..9a06dcd8 100644 --- a/src/cron/daemon/daemon.v +++ b/src/cron/daemon/daemon.v @@ -20,7 +20,7 @@ const ( struct ScheduledBuild { pub: - repo Target + target Target timestamp time.Time } @@ -37,9 +37,9 @@ mut: global_schedule CronExpression api_update_frequency int image_rebuild_frequency int - // Repos currently loaded from API. - repos []Target - // At what point to update the list of repositories. + // Targets currently loaded from API. + targets []Target + // At what point to update the list of targets. api_update_timestamp time.Time image_build_timestamp time.Time queue MinHeap @@ -51,7 +51,7 @@ mut: logger shared log.Log } -// init_daemon initializes a new Daemon object. It renews the repositories & +// init_daemon initializes a new Daemon object. It renews the targets & // populates the build queue for the first time. pub fn init_daemon(logger log.Log, address string, api_key string, base_image string, global_schedule CronExpression, max_concurrent_builds int, api_update_frequency int, image_rebuild_frequency int) ?Daemon { mut d := Daemon{ @@ -65,8 +65,8 @@ pub fn init_daemon(logger log.Log, address string, api_key string, base_image st logger: logger } - // Initialize the repos & queue - d.renew_repos() + // Initialize the targets & queue + d.renew_targets() d.renew_queue() if !d.rebuild_base_image() { return error('The base image failed to build. The Vieter cron daemon cannot run without an initial builder image.') @@ -76,21 +76,21 @@ pub fn init_daemon(logger log.Log, address string, api_key string, base_image st } // run starts the actual daemon process. It runs builds when possible & -// periodically refreshes the list of repositories to ensure we stay in sync. +// periodically refreshes the list of targets to ensure we stay in sync. pub fn (mut d Daemon) run() { for { finished_builds := d.clean_finished_builds() // Update the API's contents if needed & renew the queue if time.now() >= d.api_update_timestamp { - d.renew_repos() + d.renew_targets() d.renew_queue() } // The finished builds should only be rescheduled if the API contents // haven't been renewed. else { for sb in finished_builds { - d.schedule_build(sb.repo) + d.schedule_build(sb.target) } } @@ -114,7 +114,7 @@ pub fn (mut d Daemon) run() { // every second to clean up any finished builds & start new ones. mut delay := time.Duration(1 * time.second) - // Sleep either until we have to refresh the repos or when the next + // Sleep either until we have to refresh the targets or when the next // build has to start, with a minimum of 1 second. if d.current_build_count() == 0 { now := time.now() @@ -148,12 +148,13 @@ pub fn (mut d Daemon) run() { } } -// schedule_build adds the next occurence of the given repo build to the queue. -fn (mut d Daemon) schedule_build(repo Target) { - ce := if repo.schedule != '' { - parse_expression(repo.schedule) or { +// schedule_build adds the next occurence of the given targets build to the +// queue. +fn (mut d Daemon) schedule_build(target Target) { + ce := if target.schedule != '' { + parse_expression(target.schedule) or { // TODO This shouldn't return an error if the expression is empty. - d.lerror("Error while parsing cron expression '$repo.schedule' (id $repo.id): $err.msg()") + d.lerror("Error while parsing cron expression '$target.schedule' (id $target.id): $err.msg()") d.global_schedule } @@ -161,41 +162,41 @@ fn (mut d Daemon) schedule_build(repo Target) { d.global_schedule } - // A repo that can't be scheduled will just be skipped for now + // A target that can't be scheduled will just be skipped for now timestamp := ce.next_from_now() or { - d.lerror("Couldn't calculate next timestamp from '$repo.schedule'; skipping") + d.lerror("Couldn't calculate next timestamp from '$target.schedule'; skipping") return } d.queue.insert(ScheduledBuild{ - repo: repo + target: target timestamp: timestamp }) } -// renew_repos requests the newest list of Git repos from the server & replaces +// renew_targets requests the newest list of targets from the server & replaces // the old one. -fn (mut d Daemon) renew_repos() { - d.linfo('Renewing repos...') +fn (mut d Daemon) renew_targets() { + d.linfo('Renewing targets...') - mut new_repos := d.client.get_all_targets() or { - d.lerror('Failed to renew repos. Retrying in ${daemon.api_update_retry_timeout}s...') + mut new_targets := d.client.get_all_targets() or { + d.lerror('Failed to renew targets. Retrying in ${daemon.api_update_retry_timeout}s...') d.api_update_timestamp = time.now().add_seconds(daemon.api_update_retry_timeout) return } - // Filter out any repos that shouldn't run on this architecture + // Filter out any targets that shouldn't run on this architecture cur_arch := os.uname().machine - new_repos = new_repos.filter(it.arch.any(it.value == cur_arch)) + new_targets = new_targets.filter(it.arch.any(it.value == cur_arch)) - d.repos = new_repos + d.targets = new_targets d.api_update_timestamp = time.now().add_seconds(60 * d.api_update_frequency) } // renew_queue replaces the old queue with a new one that reflects the newest -// values in repos_map. +// values in targets. fn (mut d Daemon) renew_queue() { d.linfo('Renewing queue...') mut new_queue := MinHeap{} @@ -225,10 +226,10 @@ fn (mut d Daemon) renew_queue() { d.queue = new_queue - // For each repository in repos_map, parse their cron expression (or use - // the default one if not present) & add them to the queue - for repo in d.repos { - d.schedule_build(repo) + // For each target in targets, parse their cron expression (or use the + // default one if not present) & add them to the queue + for target in d.targets { + d.schedule_build(target) } } From 4200f5c8de675b60fc70e9edc852ea5bff9b8e0b Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Fri, 17 Jun 2022 20:19:15 +0200 Subject: [PATCH 087/236] fix(build): explicitely set PATH variable in build containers --- CHANGELOG.md | 1 + src/build/build.v | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eee1f9ff..e5bd6c58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Renamed `/api/v1/repos` namespace to `/api/v1/targets` * Branch name for 'git' targets is now optional; if not provided, the repository will be cloned with the default branch +* Build containers now explicitely set the PATH variable ### Removed diff --git a/src/build/build.v b/src/build/build.v index cd0007fb..8048ec41 100644 --- a/src/build/build.v +++ b/src/build/build.v @@ -11,6 +11,9 @@ import models { Target } const ( container_build_dir = '/build' build_image_repo = 'vieter-build' + // Contents of PATH variable in build containers + path_dirs = ['/sbin', '/bin', '/usr/sbin', '/usr/bin', '/usr/local/sbin', + '/usr/local/bin', '/usr/bin/site_perl', '/usr/bin/vendor_perl', '/usr/bin/core_perl'] ) // create_build_image creates a builder image given some base image which can @@ -109,7 +112,13 @@ pub fn build_target(address string, api_key string, base_image_id string, target c := docker.NewContainer{ image: '$base_image_id' - env: ['BUILD_SCRIPT=$base64_script', 'API_KEY=$api_key'] + env: [ + 'BUILD_SCRIPT=$base64_script', + 'API_KEY=$api_key', + // `archlinux:base-devel` does not correctly set the path variable, + // causing certain builds to fail. This fixes it. + 'PATH=${build.path_dirs.join(':')}', + ] entrypoint: ['/bin/sh', '-c'] cmd: ['echo \$BUILD_SCRIPT | base64 -d | /bin/bash -e'] work_dir: '/build' From 39eb03077e1437aa2a25a807972e425ba71524b6 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Mon, 20 Jun 2022 17:31:05 +0200 Subject: [PATCH 088/236] docs: mention matrix room in README [CI SKIP] --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 31f262e1..70906f13 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,12 @@ # Vieter -## Documentation - I host documentation for Vieter over at https://rustybever.be/docs/vieter/. API documentation for the current codebase can be found at https://rustybever.be/api-docs/vieter/. +For more information, questions or just a chat, there's +[#vieter:rustybever.be](https://matrix.to/#/#vieter:rustybever.be) on Matrix! + ## Overview Vieter is a restart of the Pieter project. The goal is to create a simple, From d060366dcb42ef1ee32fb6d8459b29b5e6a9bd92 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sat, 18 Jun 2022 18:28:59 +0200 Subject: [PATCH 089/236] refactor: move docker code to external vdocker module --- src/build/build.v | 2 +- src/console/targets/build.v | 2 +- src/cron/daemon/daemon.v | 2 +- src/docker/README.md | 3 - src/docker/containers.v | 123 -------------------------------- src/docker/docker.v | 137 ------------------------------------ src/docker/images.v | 61 ---------------- src/docker/stream.v | 135 ----------------------------------- src/v.mod | 3 +- 9 files changed, 5 insertions(+), 463 deletions(-) delete mode 100644 src/docker/README.md delete mode 100644 src/docker/containers.v delete mode 100644 src/docker/docker.v delete mode 100644 src/docker/images.v delete mode 100644 src/docker/stream.v diff --git a/src/build/build.v b/src/build/build.v index 8048ec41..6688296e 100644 --- a/src/build/build.v +++ b/src/build/build.v @@ -1,6 +1,6 @@ module build -import docker +import vieter.vdocker as docker import encoding.base64 import time import os diff --git a/src/console/targets/build.v b/src/console/targets/build.v index 1f522a6c..7403e2ea 100644 --- a/src/console/targets/build.v +++ b/src/console/targets/build.v @@ -1,7 +1,7 @@ module targets import client -import docker +import vieter.vdocker as docker import os import build diff --git a/src/cron/daemon/daemon.v b/src/cron/daemon/daemon.v index 9a06dcd8..0ab792aa 100644 --- a/src/cron/daemon/daemon.v +++ b/src/cron/daemon/daemon.v @@ -6,7 +6,7 @@ import datatypes { MinHeap } import cron.expression { CronExpression, parse_expression } import math import build -import docker +import vieter.vdocker as docker import os import client import models { Target } diff --git a/src/docker/README.md b/src/docker/README.md deleted file mode 100644 index 4cc8971a..00000000 --- a/src/docker/README.md +++ /dev/null @@ -1,3 +0,0 @@ -This module implements part of the Docker Engine API v1.41 -([documentation](https://docs.docker.com/engine/api/v1.41/)) using socket-based -HTTP communication. diff --git a/src/docker/containers.v b/src/docker/containers.v deleted file mode 100644 index 8fbf0274..00000000 --- a/src/docker/containers.v +++ /dev/null @@ -1,123 +0,0 @@ -module docker - -import json -import net.urllib -import time -import net.http { Method } - -struct DockerError { - message string -} - -pub struct NewContainer { - image string [json: Image] - entrypoint []string [json: Entrypoint] - cmd []string [json: Cmd] - env []string [json: Env] - work_dir string [json: WorkingDir] - user string [json: User] -} - -struct CreatedContainer { -pub: - id string [json: Id] - warnings []string [json: Warnings] -} - -// create_container creates a new container with the given config. -pub fn (mut d DockerConn) create_container(c NewContainer) ?CreatedContainer { - d.send_request_with_json(Method.post, urllib.parse('/v1.41/containers/create')?, c)? - head, res := d.read_response()? - - if head.status_code != 201 { - data := json.decode(DockerError, res)? - - return error(data.message) - } - - data := json.decode(CreatedContainer, res)? - - return data -} - -// start_container starts the container with the given id. -pub fn (mut d DockerConn) start_container(id string) ? { - d.send_request(Method.post, urllib.parse('/v1.41/containers/$id/start')?)? - head, body := d.read_response()? - - if head.status_code != 204 { - data := json.decode(DockerError, body)? - - return error(data.message) - } -} - -struct ContainerInspect { -pub mut: - state ContainerState [json: State] -} - -struct ContainerState { -pub: - running bool [json: Running] - status string [json: Status] - exit_code int [json: ExitCode] - // These use a rather specific format so they have to be parsed later - start_time_str string [json: StartedAt] - end_time_str string [json: FinishedAt] -pub mut: - start_time time.Time [skip] - end_time time.Time [skip] -} - -// inspect_container returns detailed information for a given container. -pub fn (mut d DockerConn) inspect_container(id string) ?ContainerInspect { - d.send_request(Method.get, urllib.parse('/v1.41/containers/$id/json')?)? - head, body := d.read_response()? - - if head.status_code != 200 { - data := json.decode(DockerError, body)? - - return error(data.message) - } - - mut data := json.decode(ContainerInspect, body)? - - // The Docker engine API *should* always return UTC time. - data.state.start_time = time.parse_rfc3339(data.state.start_time_str)? - - if data.state.status == 'exited' { - data.state.end_time = time.parse_rfc3339(data.state.end_time_str)? - } - - return data -} - -// remove_container removes the container with the given id. -pub fn (mut d DockerConn) remove_container(id string) ? { - d.send_request(Method.delete, urllib.parse('/v1.41/containers/$id')?)? - head, body := d.read_response()? - - if head.status_code != 204 { - data := json.decode(DockerError, body)? - - return error(data.message) - } -} - -// get_container_logs returns a reader object allowing access to the -// container's logs. -pub fn (mut d DockerConn) get_container_logs(id string) ?&StreamFormatReader { - d.send_request(Method.get, urllib.parse('/v1.41/containers/$id/logs?stdout=true&stderr=true')?)? - head := d.read_response_head()? - - if head.status_code != 200 { - content_length := head.header.get(http.CommonHeader.content_length)?.int() - body := d.read_response_body(content_length)? - data := json.decode(DockerError, body)? - - return error(data.message) - } - - return d.get_stream_format_reader() -} diff --git a/src/docker/docker.v b/src/docker/docker.v deleted file mode 100644 index ccc6bedd..00000000 --- a/src/docker/docker.v +++ /dev/null @@ -1,137 +0,0 @@ -module docker - -import net.unix -import io -import net.http -import strings -import net.urllib -import json -import util - -const ( - socket = '/var/run/docker.sock' - buf_len = 10 * 1024 - http_separator = [u8(`\r`), `\n`, `\r`, `\n`] - http_chunk_separator = [u8(`\r`), `\n`] -) - -pub struct DockerConn { -mut: - socket &unix.StreamConn - reader &io.BufferedReader -} - -// new_conn creates a new connection to the Docker daemon. -pub fn new_conn() ?&DockerConn { - s := unix.connect_stream(docker.socket)? - - d := &DockerConn{ - socket: s - reader: io.new_buffered_reader(reader: s) - } - - return d -} - -// close closes the underlying socket connection. -pub fn (mut d DockerConn) close() ? { - d.socket.close()? -} - -// send_request sends an HTTP request without body. -pub fn (mut d DockerConn) send_request(method http.Method, url urllib.URL) ? { - req := '$method $url.request_uri() HTTP/1.1\nHost: localhost\n\n' - - d.socket.write_string(req)? - - // When starting a new request, the reader needs to be reset. - d.reader = io.new_buffered_reader(reader: d.socket) -} - -// send_request_with_body sends an HTTP request with the given body. -pub fn (mut d DockerConn) send_request_with_body(method http.Method, url urllib.URL, content_type string, body string) ? { - req := '$method $url.request_uri() HTTP/1.1\nHost: localhost\nContent-Type: $content_type\nContent-Length: $body.len\n\n$body\n\n' - - d.socket.write_string(req)? - - // When starting a new request, the reader needs to be reset. - d.reader = io.new_buffered_reader(reader: d.socket) -} - -// send_request_with_json is a convenience wrapper around -// send_request_with_body that encodes the input as JSON. -pub fn (mut d DockerConn) send_request_with_json(method http.Method, url urllib.URL, data &T) ? { - body := json.encode(data) - - return d.send_request_with_body(method, url, 'application/json', body) -} - -// read_response_head consumes the socket's contents until it encounters -// '\r\n\r\n', after which it parses the response as an HTTP response. -// Importantly, this function never consumes the reader past the HTTP -// separator, so the body can be read fully later on. -pub fn (mut d DockerConn) read_response_head() ?http.Response { - mut res := []u8{} - - util.read_until_separator(mut d.reader, mut res, docker.http_separator)? - - return http.parse_response(res.bytestr()) -} - -// read_response_body reads `length` bytes from the stream. It can be used when -// the response encoding isn't chunked to fully read it. -pub fn (mut d DockerConn) read_response_body(length int) ?string { - if length == 0 { - return '' - } - - mut buf := []u8{len: docker.buf_len} - mut c := 0 - mut builder := strings.new_builder(docker.buf_len) - - for builder.len < length { - c = d.reader.read(mut buf) or { break } - - builder.write(buf[..c])? - } - - return builder.str() -} - -// read_response is a convenience function which always consumes the entire -// response & returns it. It should only be used when we're certain that the -// result isn't too large. -pub fn (mut d DockerConn) read_response() ?(http.Response, string) { - head := d.read_response_head()? - - if head.header.get(http.CommonHeader.transfer_encoding) or { '' } == 'chunked' { - mut builder := strings.new_builder(1024) - mut body := d.get_chunked_response_reader() - - util.reader_to_writer(mut body, mut builder)? - - return head, builder.str() - } - - content_length := head.header.get(http.CommonHeader.content_length)?.int() - res := d.read_response_body(content_length)? - - return head, res -} - -// get_chunked_response_reader returns a ChunkedResponseReader using the socket -// as reader. -pub fn (mut d DockerConn) get_chunked_response_reader() &ChunkedResponseReader { - r := new_chunked_response_reader(d.reader) - - return r -} - -// get_stream_format_reader returns a StreamFormatReader using the socket as -// reader. -pub fn (mut d DockerConn) get_stream_format_reader() &StreamFormatReader { - r := new_chunked_response_reader(d.reader) - r2 := new_stream_format_reader(r) - - return r2 -} diff --git a/src/docker/images.v b/src/docker/images.v deleted file mode 100644 index 6161565a..00000000 --- a/src/docker/images.v +++ /dev/null @@ -1,61 +0,0 @@ -module docker - -import net.http { Method } -import net.urllib -import json - -struct Image { -pub: - id string [json: Id] -} - -// pull_image pulls the given image:tag. -pub fn (mut d DockerConn) pull_image(image string, tag string) ? { - d.send_request(Method.post, urllib.parse('/v1.41/images/create?fromImage=$image&tag=$tag')?)? - head := d.read_response_head()? - - if head.status_code != 200 { - content_length := head.header.get(http.CommonHeader.content_length)?.int() - body := d.read_response_body(content_length)? - data := json.decode(DockerError, body)? - - return error(data.message) - } - - // Keep reading the body until the pull has completed - mut body := d.get_chunked_response_reader() - - mut buf := []u8{len: 1024} - - for { - body.read(mut buf) or { break } - } -} - -// create_image_from_container creates a new image from a container. -pub fn (mut d DockerConn) create_image_from_container(id string, repo string, tag string) ?Image { - d.send_request(Method.post, urllib.parse('/v1.41/commit?container=$id&repo=$repo&tag=$tag')?)? - head, body := d.read_response()? - - if head.status_code != 201 { - data := json.decode(DockerError, body)? - - return error(data.message) - } - - data := json.decode(Image, body)? - - return data -} - -// remove_image removes the image with the given id. -pub fn (mut d DockerConn) remove_image(id string) ? { - d.send_request(Method.delete, urllib.parse('/v1.41/images/$id')?)? - head, body := d.read_response()? - - if head.status_code != 200 { - data := json.decode(DockerError, body)? - - return error(data.message) - } -} diff --git a/src/docker/stream.v b/src/docker/stream.v deleted file mode 100644 index 001f4b3e..00000000 --- a/src/docker/stream.v +++ /dev/null @@ -1,135 +0,0 @@ -module docker - -import io -import util -import encoding.binary -import encoding.hex - -// ChunkedResponseReader parses an underlying HTTP chunked response, exposing -// it as if it was a continuous stream of data. -struct ChunkedResponseReader { -mut: - reader io.BufferedReader - bytes_left_in_chunk u64 - started bool -} - -// new_chunked_response_reader creates a new ChunkedResponseReader on the heap -// with the provided reader. -pub fn new_chunked_response_reader(reader io.BufferedReader) &ChunkedResponseReader { - r := &ChunkedResponseReader{ - reader: reader - } - - return r -} - -// read satisfies the io.Reader interface. -pub fn (mut r ChunkedResponseReader) read(mut buf []u8) ?int { - if r.bytes_left_in_chunk == 0 { - // An io.BufferedReader always returns none if its stream has - // ended. - r.bytes_left_in_chunk = r.read_chunk_size()? - } - - mut c := 0 - - // Make sure we don't read more than we can safely read. This is to avoid - // the underlying reader from becoming out of sync with our parsing: - if buf.len > r.bytes_left_in_chunk { - c = r.reader.read(mut buf[..r.bytes_left_in_chunk])? - } else { - c = r.reader.read(mut buf)? - } - - r.bytes_left_in_chunk -= u64(c) - - return c -} - -// read_chunk_size advances the reader & reads the size of the next HTTP chunk. -// This function should only be called if the previous chunk has been -// completely consumed. -fn (mut r ChunkedResponseReader) read_chunk_size() ?u64 { - if r.started { - mut buf := []u8{len: 2} - - // Each chunk ends with a `\r\n` which we want to skip first - r.reader.read(mut buf)? - } - - r.started = true - - mut res := []u8{} - util.read_until_separator(mut r.reader, mut res, http_chunk_separator)? - - // The length of the next chunk is provided as a hexadecimal - mut num_data := hex.decode(res#[..-2].bytestr())? - - for num_data.len < 8 { - num_data.insert(0, 0) - } - - num := binary.big_endian_u64(num_data) - - // This only occurs for the very last chunk, which always reports a size of - // 0. - if num == 0 { - return none - } - - return num -} - -// StreamFormatReader parses an underlying stream of Docker logs, removing the -// header bytes. -struct StreamFormatReader { -mut: - reader ChunkedResponseReader - bytes_left_in_chunk u32 -} - -// new_stream_format_reader creates a new StreamFormatReader using the given -// reader. -pub fn new_stream_format_reader(reader ChunkedResponseReader) &StreamFormatReader { - r := &StreamFormatReader{ - reader: reader - } - - return r -} - -// read satisfies the io.Reader interface. -pub fn (mut r StreamFormatReader) read(mut buf []u8) ?int { - if r.bytes_left_in_chunk == 0 { - r.bytes_left_in_chunk = r.read_chunk_size()? - } - - mut c := 0 - - if buf.len > r.bytes_left_in_chunk { - c = r.reader.read(mut buf[..r.bytes_left_in_chunk])? - } else { - c = r.reader.read(mut buf)? - } - - r.bytes_left_in_chunk -= u32(c) - - return c -} - -// read_chunk_size advances the reader & reads the header bytes for the length -// of the next chunk. -fn (mut r StreamFormatReader) read_chunk_size() ?u32 { - mut buf := []u8{len: 8} - - r.reader.read(mut buf)? - - num := binary.big_endian_u32(buf[4..]) - - if num == 0 { - return none - } - - return num -} diff --git a/src/v.mod b/src/v.mod index 7f459174..15e3518a 100644 --- a/src/v.mod +++ b/src/v.mod @@ -1,5 +1,6 @@ Module { dependencies: [ - 'https://git.rustybever.be/vieter/vconf' + 'https://git.rustybever.be/vieter/vconf', + 'https://git.rustybever.be/vieter/vdocker' ] } From 461f22716995e51db40519312f49f08fd18d9e21 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Wed, 22 Jun 2022 09:18:47 +0200 Subject: [PATCH 090/236] refactor: use new module names --- src/build/build.v | 22 +++++++++++----------- src/console/logs/logs.v | 2 +- src/console/targets/build.v | 2 +- src/console/targets/targets.v | 2 +- src/cron/cli.v | 2 +- src/cron/daemon/daemon.v | 2 +- src/server/cli.v | 2 +- src/v.mod | 4 ++-- 8 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/build/build.v b/src/build/build.v index 6688296e..2ad70a6f 100644 --- a/src/build/build.v +++ b/src/build/build.v @@ -1,6 +1,6 @@ module build -import vieter.vdocker as docker +import vieter_v.docker import encoding.base64 import time import os @@ -59,13 +59,13 @@ pub fn create_build_image(base_image string) ?string { // We pull the provided image dd.pull_image(image_name, image_tag)? - id := dd.create_container(c)?.id + id := dd.container_create(c)?.id // id := docker.create_container(c)? - dd.start_container(id)? + dd.container_start(id)? // This loop waits until the container has stopped, so we can remove it after for { - data := dd.inspect_container(id)? + data := dd.container_inspect(id)? if !data.state.running { break @@ -80,7 +80,7 @@ pub fn create_build_image(base_image string) ?string { // conflicts. tag := time.sys_mono_now().str() image := dd.create_image_from_container(id, 'vieter-build', tag)? - dd.remove_container(id)? + dd.container_remove(id)? return image.id } @@ -125,25 +125,25 @@ pub fn build_target(address string, api_key string, base_image_id string, target user: '0:0' } - id := dd.create_container(c)?.id - dd.start_container(id)? + id := dd.container_create(c)?.id + dd.container_start(id)? - mut data := dd.inspect_container(id)? + mut data := dd.container_inspect(id)? // This loop waits until the container has stopped, so we can remove it after for data.state.running { time.sleep(1 * time.second) - data = dd.inspect_container(id)? + data = dd.container_inspect(id)? } - mut logs_stream := dd.get_container_logs(id)? + mut logs_stream := dd.container_get_logs(id)? // Read in the entire stream mut logs_builder := strings.new_builder(10 * 1024) util.reader_to_writer(mut logs_stream, mut logs_builder)? - dd.remove_container(id)? + dd.container_remove(id)? return BuildResult{ start_time: data.state.start_time diff --git a/src/console/logs/logs.v b/src/console/logs/logs.v index 5155ae88..0f023bc7 100644 --- a/src/console/logs/logs.v +++ b/src/console/logs/logs.v @@ -1,7 +1,7 @@ module logs import cli -import vieter.vconf +import vieter_v.conf as vconf import client import console import time diff --git a/src/console/targets/build.v b/src/console/targets/build.v index 7403e2ea..6337aa3e 100644 --- a/src/console/targets/build.v +++ b/src/console/targets/build.v @@ -1,7 +1,7 @@ module targets import client -import vieter.vdocker as docker +import vieter_v.docker import os import build diff --git a/src/console/targets/targets.v b/src/console/targets/targets.v index ce1cd149..66d48fb3 100644 --- a/src/console/targets/targets.v +++ b/src/console/targets/targets.v @@ -1,7 +1,7 @@ module targets import cli -import vieter.vconf +import vieter_v.conf as vconf import cron.expression { parse_expression } import client { NewTarget } import console diff --git a/src/cron/cli.v b/src/cron/cli.v index c2e3d39a..4d958339 100644 --- a/src/cron/cli.v +++ b/src/cron/cli.v @@ -1,7 +1,7 @@ module cron import cli -import vieter.vconf +import vieter_v.conf as vconf struct Config { pub: diff --git a/src/cron/daemon/daemon.v b/src/cron/daemon/daemon.v index 0ab792aa..934d35a0 100644 --- a/src/cron/daemon/daemon.v +++ b/src/cron/daemon/daemon.v @@ -6,7 +6,7 @@ import datatypes { MinHeap } import cron.expression { CronExpression, parse_expression } import math import build -import vieter.vdocker as docker +import vieter_v.docker import os import client import models { Target } diff --git a/src/server/cli.v b/src/server/cli.v index a62f56ec..6fd09c54 100644 --- a/src/server/cli.v +++ b/src/server/cli.v @@ -1,7 +1,7 @@ module server import cli -import vieter.vconf +import vieter_v.conf as vconf struct Config { pub: diff --git a/src/v.mod b/src/v.mod index 15e3518a..5b890629 100644 --- a/src/v.mod +++ b/src/v.mod @@ -1,6 +1,6 @@ Module { dependencies: [ - 'https://git.rustybever.be/vieter/vconf', - 'https://git.rustybever.be/vieter/vdocker' + 'https://git.rustybever.be/vieter-v/conf', + 'https://git.rustybever.be/vieter-v/docker' ] } From e58ac496807501ba91c7fcbb7c6a2a51c25b53ca Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Wed, 22 Jun 2022 09:27:22 +0200 Subject: [PATCH 091/236] chore: hopefully changed all URLs to new org --- .woodpecker/.arch-rel.yml | 2 +- .woodpecker/.arch.yml | 2 +- CHANGELOG.md | 16 ++++++++-------- Makefile | 2 +- PKGBUILD | 4 ++-- PKGBUILD.dev | 4 ++-- README.md | 6 +++--- docs/config.toml | 4 ++-- 8 files changed, 20 insertions(+), 20 deletions(-) diff --git a/.woodpecker/.arch-rel.yml b/.woodpecker/.arch-rel.yml index b8f4c7ae..f5f228ef 100644 --- a/.woodpecker/.arch-rel.yml +++ b/.woodpecker/.arch-rel.yml @@ -23,7 +23,7 @@ pipeline: - su builder # Due to a bug with the V compiler, we can't just use the PKGBUILD from # inside the repo - - curl -OL "https://git.rustybever.be/vieter/vieter/raw/tag/$CI_COMMIT_TAG/PKGBUILD" + - curl -OL "https://git.rustybever.be/vieter-v/vieter/raw/tag/$CI_COMMIT_TAG/PKGBUILD" - makepkg -s --noconfirm --needed when: event: tag diff --git a/.woodpecker/.arch.yml b/.woodpecker/.arch.yml index b2a59ba5..8f1a6ff0 100644 --- a/.woodpecker/.arch.yml +++ b/.woodpecker/.arch.yml @@ -23,7 +23,7 @@ pipeline: - su builder # Due to a bug with the V compiler, we can't just use the PKGBUILD from # inside the repo - - curl -o PKGBUILD -L https://git.rustybever.be/vieter/vieter/raw/branch/dev/PKGBUILD.dev + - curl -o PKGBUILD -L https://git.rustybever.be/vieter-v/vieter/raw/branch/dev/PKGBUILD.dev - makepkg -s --noconfirm --needed when: event: push diff --git a/CHANGELOG.md b/CHANGELOG.md index e5bd6c58..cf2b8293 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased](https://git.rustybever.be/vieter/vieter/src/branch/dev) +## [Unreleased](https://git.rustybever.be/vieter-v/vieter/src/branch/dev) ### Added @@ -28,11 +28,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * md5 hashes are no longer calculated for packages -## [0.3.0](https://git.rustybever.be/vieter/vieter/src/tag/0.3.0) +## [0.3.0](https://git.rustybever.be/vieter-v/vieter/src/tag/0.3.0) Nothing besides bumping the versions. -## [0.3.0-rc.1](https://git.rustybever.be/vieter/vieter/src/tag/0.3.0-rc.1) +## [0.3.0-rc.1](https://git.rustybever.be/vieter-v/vieter/src/tag/0.3.0-rc.1) ### Added @@ -60,7 +60,7 @@ Nothing besides bumping the versions. * `POST /api/logs` now correctly uses epoch timestamps instead of strings -## [0.3.0-alpha.2](https://git.rustybever.be/vieter/vieter/src/tag/0.3.0-alpha.2) +## [0.3.0-alpha.2](https://git.rustybever.be/vieter-v/vieter/src/tag/0.3.0-alpha.2) ### Added @@ -85,7 +85,7 @@ Nothing besides bumping the versions. * `vieter-git` is the latest commit on the dev branch * Full refactor of Docker socket code -## [0.3.0-alpha.1](https://git.rustybever.be/vieter/vieter/src/tag/0.3.0-alpha.1) +## [0.3.0-alpha.1](https://git.rustybever.be/vieter-v/vieter/src/tag/0.3.0-alpha.1) ### Changed @@ -104,7 +104,7 @@ Nothing besides bumping the versions. * Binary no longer panics when an env var is missing -## [0.2.0](https://git.rustybever.be/vieter/vieter/src/tag/0.2.0) +## [0.2.0](https://git.rustybever.be/vieter-v/vieter/src/tag/0.2.0) ### Changed @@ -138,13 +138,13 @@ Nothing besides bumping the versions. * Packages with unknown fields in .PKGINFO are now allowed * Old packages are now properly removed -## [0.1.0](https://git.rustybever.be/vieter/vieter/src/tag/0.1.0) +## [0.1.0](https://git.rustybever.be/vieter-v/vieter/src/tag/0.1.0) ### Changed * Improved logging -## [0.1.0-rc.1](https://git.rustybever.be/vieter/vieter/src/tag/0.1.0-rc.1) +## [0.1.0-rc.1](https://git.rustybever.be/vieter-v/vieter/src/tag/0.1.0-rc.1) ### Added diff --git a/Makefile b/Makefile index 7eb35472..96410d79 100644 --- a/Makefile +++ b/Makefile @@ -87,7 +87,7 @@ test: .PHONY: v v: v/v v/v: - git clone --single-branch https://git.rustybever.be/Chewing_Bever/v v + git clone --single-branch https://git.rustybever.be/vieter-v/v v make -C v .PHONY: clean diff --git a/PKGBUILD b/PKGBUILD index 33faec55..6b664d1f 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -9,9 +9,9 @@ pkgdesc="Vieter is a lightweight implementation of an Arch repository server." depends=('glibc' 'openssl' 'libarchive' 'sqlite') makedepends=('git' 'vieter-v') arch=('x86_64' 'aarch64') -url='https://git.rustybever.be/vieter/vieter' +url='https://git.rustybever.be/vieter-v/vieter' license=('AGPL3') -source=("$pkgname::git+https://git.rustybever.be/vieter/vieter#tag=${pkgver//_/-}") +source=("$pkgname::git+https://git.rustybever.be/vieter-v/vieter#tag=${pkgver//_/-}") md5sums=('SKIP') prepare() { diff --git a/PKGBUILD.dev b/PKGBUILD.dev index bff1459e..045e5765 100644 --- a/PKGBUILD.dev +++ b/PKGBUILD.dev @@ -9,9 +9,9 @@ pkgdesc="Vieter is a lightweight implementation of an Arch repository server." depends=('glibc' 'openssl' 'libarchive' 'sqlite') makedepends=('git' 'vieter-v') arch=('x86_64' 'aarch64') -url='https://git.rustybever.be/vieter/vieter' +url='https://git.rustybever.be/vieter-v/vieter' license=('AGPL3') -source=("$pkgname::git+https://git.rustybever.be/vieter/vieter#branch=dev") +source=("$pkgname::git+https://git.rustybever.be/vieter-v/vieter#branch=dev") md5sums=('SKIP') provides=('vieter') conflicts=('vieter') diff --git a/README.md b/README.md index 70906f13..5911ea21 100644 --- a/README.md +++ b/README.md @@ -49,9 +49,9 @@ update`. ### Compiler Vieter compiles with the standard Vlang compiler. However, I do maintain a -[mirror](https://git.rustybever.be/vieter/v). This is to ensure my CI does not -break without reason, as I control when & how frequently the mirror is updated -to reflect the official repository. +[mirror](https://git.rustybever.be/vieter-v/v). This is to ensure my CI does +not break without reason, as I control when & how frequently the mirror is +updated to reflect the official repository. If you encounter issues using the latest V compiler, try using my mirror instead. `make v` will clone the repository & build the mirror. Afterwards, diff --git a/docs/config.toml b/docs/config.toml index f8e23cdc..7d23d06a 100644 --- a/docs/config.toml +++ b/docs/config.toml @@ -38,7 +38,7 @@ enableGitInfo = true weight = 20 [[menu.after]] name = "Vieter" - url = "https://git.rustybever.be/vieter/vieter" + url = "https://git.rustybever.be/vieter-v/vieter" weight = 30 [[menu.after]] name = "Hugo Theme" @@ -70,7 +70,7 @@ enableGitInfo = true # Set source repository location. # Used for 'Last Modified' and 'Edit this page' links. - BookRepo = 'https://git.rustybever.be/vieter/vieter' + BookRepo = 'https://git.rustybever.be/vieter-v/vieter' # (Optional, default 'commit') Specifies commit portion of the link to the page's last modified # commit hash for 'doc' page type. From a4c2508fe7f42b0fe92271fb47d6ff97b82df730 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Wed, 22 Jun 2022 09:33:48 +0200 Subject: [PATCH 092/236] chore: removed unnecessary things from Makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 96410d79..ed44df92 100644 --- a/Makefile +++ b/Makefile @@ -92,7 +92,7 @@ v/v: .PHONY: clean clean: - rm -rf 'data' 'vieter' 'dvieter' 'pvieter' 'vieter.c' 'dvieterctl' 'vieterctl' 'pkg' 'src/vieter' *.pkg.tar.zst 'suvieter' 'afvieter' '$(SRC_DIR)/_docs' 'docs/public' + rm -rf 'data' 'vieter' 'dvieter' 'pvieter' 'vieter.c' 'pkg' 'src/vieter' *.pkg.tar.zst 'suvieter' 'afvieter' '$(SRC_DIR)/_docs' 'docs/public' # =====EXPERIMENTAL===== From 9dd9222a69c52aa3424878fca166b3212f00b91c Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Wed, 22 Jun 2022 09:44:25 +0200 Subject: [PATCH 093/236] fix(ci): use extended hugo; install modules for tests --- .woodpecker/.docs.yml | 2 +- .woodpecker/.test.yml | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.woodpecker/.docs.yml b/.woodpecker/.docs.yml index c342e388..051d852e 100644 --- a/.woodpecker/.docs.yml +++ b/.woodpecker/.docs.yml @@ -4,7 +4,7 @@ branches: pipeline: docs: - image: 'klakegg/hugo:alpine' + image: 'klakegg/hugo:ext-alpine' group: 'generate' commands: - apk add git diff --git a/.woodpecker/.test.yml b/.woodpecker/.test.yml index 6b7b646d..4cd6eaf0 100644 --- a/.woodpecker/.test.yml +++ b/.woodpecker/.test.yml @@ -8,6 +8,15 @@ branches: platform: ${PLATFORM} pipeline: + install-modules: + image: 'chewingbever/vlang:latest' + pull: true + commands: + - export VMODULES=$PWD/.vmodules + - 'cd src && v install' + when: + event: [pull_request] + test: image: 'chewingbever/vlang:latest' pull: true From c8fc4c6a96d75082dba977f63e230eca9d632102 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Wed, 22 Jun 2022 09:59:44 +0200 Subject: [PATCH 094/236] fix(ci): set VMODULES for tests --- .woodpecker/.test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.woodpecker/.test.yml b/.woodpecker/.test.yml index 4cd6eaf0..a19dbd42 100644 --- a/.woodpecker/.test.yml +++ b/.woodpecker/.test.yml @@ -21,6 +21,7 @@ pipeline: image: 'chewingbever/vlang:latest' pull: true commands: + - export VMODULES=$PWD/.vmodules - make test when: event: [pull_request] From 25d87fb5e6cef2e9db90dc3b2c51eff1c49c8099 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Wed, 22 Jun 2022 20:17:11 +0200 Subject: [PATCH 095/236] fix: make code compile with updated V version --- src/archive.c.v | 4 ++-- src/db/db.v | 2 +- src/package/package.v | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/archive.c.v b/src/archive.c.v index 1f0d1dd5..a40cdef8 100644 --- a/src/archive.c.v +++ b/src/archive.c.v @@ -4,7 +4,7 @@ #include "archive.h" -struct C.archive {} +pub struct C.archive {} // Create a new archive struct for reading fn C.archive_read_new() &C.archive @@ -71,7 +71,7 @@ fn C.archive_filter_code(&C.archive, int) int #include "archive_entry.h" -struct C.archive_entry {} +pub struct C.archive_entry {} // Create a new archive_entry struct fn C.archive_entry_new() &C.archive_entry diff --git a/src/db/db.v b/src/db/db.v index eaf71abb..9459c052 100644 --- a/src/db/db.v +++ b/src/db/db.v @@ -3,7 +3,7 @@ module db import sqlite import time -struct VieterDb { +pub struct VieterDb { conn sqlite.DB } diff --git a/src/package/package.v b/src/package/package.v index 86bf40ad..9eaf5a23 100644 --- a/src/package/package.v +++ b/src/package/package.v @@ -4,7 +4,7 @@ import os import util // Represents a read archive -struct Pkg { +pub struct Pkg { pub: path string [required] info PkgInfo [required] From 6336d801d3631c205b0c29ce528878a99a581fba Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Wed, 22 Jun 2022 20:43:14 +0200 Subject: [PATCH 096/236] docs: mention AUR packages --- docs/content/installation.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/content/installation.md b/docs/content/installation.md index b5bdbaf0..87b9cba9 100644 --- a/docs/content/installation.md +++ b/docs/content/installation.md @@ -96,6 +96,14 @@ SigLevel = Optional Afterwards, you can update your system & install the `vieter` package for the latest official release or `vieter-git` for the latest development release. +### AUR + +If you prefer building the packages locally (or on your own Vieter instance), +there's the `[vieter](https://aur.archlinux.org/packages/vieter)` & +`[vieter-git](https://aur.archlinux.org/packages/vieter-git)` packages on the +AUR. These packages build using the `vlang-git` compiler package, so I can't +guarantee that a compiler update won't temporarily break them. + ## Building from source The project [README](https://git.rustybever.be/vieter/vieter#building) contains From 487b2357272050d1fcabf9cead73872fa5ea524e Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Wed, 22 Jun 2022 16:19:07 +0200 Subject: [PATCH 097/236] feat(cli): add aur search command --- .gitignore | 3 +++ src/console/aur/aur.v | 26 ++++++++++++++++++++++++++ src/main.v | 2 ++ src/v.mod | 3 ++- 4 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 src/console/aur/aur.v diff --git a/.gitignore b/.gitignore index 4d9f94f2..a2804fe6 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,6 @@ gdb.txt # Generated docs _docs/ /man/ + +# VLS logs +vls.log diff --git a/src/console/aur/aur.v b/src/console/aur/aur.v new file mode 100644 index 00000000..3f20dfca --- /dev/null +++ b/src/console/aur/aur.v @@ -0,0 +1,26 @@ +module aur + +import cli +import console +import vieter_v.aur + +pub fn cmd() cli.Command { + return cli.Command{ + name: 'aur' + description: 'Interact with the AUR.' + commands: [ + cli.Command{ + name: 'search' + description: 'Search for packages.' + required_args: 1 + execute: fn (cmd cli.Command) ? { + c := aur.new() + pkgs := c.search(cmd.args[0])? + data := pkgs.map([it.name, it.description]) + + println(console.pretty_table(['name', 'description'], data)?) + } + }, + ] + } +} diff --git a/src/main.v b/src/main.v index cba410ce..4ade9305 100644 --- a/src/main.v +++ b/src/main.v @@ -7,6 +7,7 @@ import console.targets import console.logs import console.schedule import console.man +import console.aur import cron fn main() { @@ -31,6 +32,7 @@ fn main() { logs.cmd(), schedule.cmd(), man.cmd(), + aur.cmd(), ] } app.setup() diff --git a/src/v.mod b/src/v.mod index 5b890629..710c976c 100644 --- a/src/v.mod +++ b/src/v.mod @@ -1,6 +1,7 @@ Module { dependencies: [ 'https://git.rustybever.be/vieter-v/conf', - 'https://git.rustybever.be/vieter-v/docker' + 'https://git.rustybever.be/vieter-v/docker', + 'https://git.rustybever.be/vieter-v/aur' ] } From 1a940f2f98e6c2c7794e4f06b2913a32316cfbfd Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Wed, 22 Jun 2022 16:38:53 +0200 Subject: [PATCH 098/236] feat(cli): added "aur add" command --- CHANGELOG.md | 1 + src/console/aur/aur.v | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf2b8293..a03c18cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 URL to a PKGBUILD * Targets with kind 'url' can provide a direct URL to a PKGBUILD instead of providing a Git repository +* CLI commands for searching the AUR & directly adding packages ### Changed diff --git a/src/console/aur/aur.v b/src/console/aur/aur.v index 3f20dfca..c98f8e68 100644 --- a/src/console/aur/aur.v +++ b/src/console/aur/aur.v @@ -2,8 +2,16 @@ module aur import cli import console +import client import vieter_v.aur +import vieter_v.conf as vconf +struct Config { + address string [required] + api_key string [required] +} + +// cmd returns the cli module for interacting with the AUR API. pub fn cmd() cli.Command { return cli.Command{ name: 'aur' @@ -21,6 +29,34 @@ pub fn cmd() cli.Command { println(console.pretty_table(['name', 'description'], data)?) } }, + cli.Command{ + name: 'add' + usage: 'repo pkg-name [pkg-name...]' + description: 'Add the given AUR package(s) to Vieter. Non-existent packages will be silently ignored.' + required_args: 2 + execute: fn (cmd cli.Command) ? { + config_file := cmd.flags.get_string('config-file')? + conf := vconf.load(prefix: 'VIETER_', default_path: config_file)? + + c := aur.new() + pkgs := c.info(cmd.args[1..])? + + vc := client.new(conf.address, conf.api_key) + + for pkg in pkgs { + vc.add_target( + kind: 'git' + url: 'https://aur.archlinux.org/$pkg.package_base' + '.git' + repo: cmd.args[0] + ) or { + println('Failed to add $pkg.name: $err.msg()') + continue + } + + println('Added $pkg.name' + '.') + } + } + }, ] } } From 0d0fb323f235c0381d8737ccd777bfa6167a7e4b Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sun, 3 Jul 2022 14:11:29 +0200 Subject: [PATCH 099/236] chore: switched to vlang 0.3 Docker image --- .woodpecker/.build.yml | 8 ++++---- .woodpecker/.docs.yml | 2 +- .woodpecker/.gitea.yml | 2 +- .woodpecker/.lint.yml | 2 +- .woodpecker/.man.yml | 2 +- .woodpecker/.test.yml | 4 ++-- Dockerfile | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.woodpecker/.build.yml b/.woodpecker/.build.yml index 7cb7d535..580fa69e 100644 --- a/.woodpecker/.build.yml +++ b/.woodpecker/.build.yml @@ -7,7 +7,7 @@ platform: ${PLATFORM} pipeline: install-modules: - image: 'chewingbever/vlang:latest' + image: 'chewingbever/vlang:0.3' pull: true commands: - export VMODULES=$PWD/.vmodules @@ -16,7 +16,7 @@ pipeline: event: [push, pull_request] debug: - image: 'chewingbever/vlang:latest' + image: 'chewingbever/vlang:0.3' commands: - export VMODULES=$PWD/.vmodules - make @@ -26,7 +26,7 @@ pipeline: exclude: [main] prod: - image: 'chewingbever/vlang:latest' + image: 'chewingbever/vlang:0.3' environment: - LDFLAGS=-lz -lbz2 -llzma -lexpat -lzstd -llz4 -lsqlite3 -static commands: @@ -44,7 +44,7 @@ pipeline: event: [push, pull_request] upload: - image: 'chewingbever/vlang:latest' + image: 'chewingbever/vlang:0.3' secrets: [ s3_username, s3_password ] commands: # https://gist.github.com/JustinTimperio/7c7115f87b775618637d67ac911e595f diff --git a/.woodpecker/.docs.yml b/.woodpecker/.docs.yml index 051d852e..da495fcb 100644 --- a/.woodpecker/.docs.yml +++ b/.woodpecker/.docs.yml @@ -11,7 +11,7 @@ pipeline: - make docs api-docs: - image: 'chewingbever/vlang:latest' + image: 'chewingbever/vlang:0.3' pull: true group: 'generate' commands: diff --git a/.woodpecker/.gitea.yml b/.woodpecker/.gitea.yml index d0825c2c..55f991e2 100644 --- a/.woodpecker/.gitea.yml +++ b/.woodpecker/.gitea.yml @@ -8,7 +8,7 @@ skip_clone: true pipeline: prepare: - image: 'chewingbever/vlang:latest' + image: 'chewingbever/vlang:0.3' pull: true secrets: [ s3_username, s3_password ] commands: diff --git a/.woodpecker/.lint.yml b/.woodpecker/.lint.yml index e70648d8..75a81059 100644 --- a/.woodpecker/.lint.yml +++ b/.woodpecker/.lint.yml @@ -5,7 +5,7 @@ platform: 'linux/amd64' pipeline: lint: - image: 'chewingbever/vlang:latest' + image: 'chewingbever/vlang:0.3' pull: true commands: - make lint diff --git a/.woodpecker/.man.yml b/.woodpecker/.man.yml index 0b808866..1a30b03d 100644 --- a/.woodpecker/.man.yml +++ b/.woodpecker/.man.yml @@ -9,7 +9,7 @@ skip_clone: true pipeline: generate: - image: 'chewingbever/vlang:latest' + image: 'chewingbever/vlang:0.3' pull: true commands: - curl -o vieter -L "https://s3.rustybever.be/vieter/commits/$CI_COMMIT_SHA/vieter-linux-amd64" diff --git a/.woodpecker/.test.yml b/.woodpecker/.test.yml index a19dbd42..6c267fab 100644 --- a/.woodpecker/.test.yml +++ b/.woodpecker/.test.yml @@ -9,7 +9,7 @@ platform: ${PLATFORM} pipeline: install-modules: - image: 'chewingbever/vlang:latest' + image: 'chewingbever/vlang:0.3' pull: true commands: - export VMODULES=$PWD/.vmodules @@ -18,7 +18,7 @@ pipeline: event: [pull_request] test: - image: 'chewingbever/vlang:latest' + image: 'chewingbever/vlang:0.3' pull: true commands: - export VMODULES=$PWD/.vmodules diff --git a/Dockerfile b/Dockerfile index 5997adca..7aed9179 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM chewingbever/vlang:latest AS builder +FROM chewingbever/vlang:0.3 AS builder ARG TARGETPLATFORM ARG CI_COMMIT_SHA From 0f6630b9404acb2b2217f52091c38d636ffc81f0 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sun, 17 Jul 2022 13:38:46 +0200 Subject: [PATCH 100/236] chore: updated PKGBUILDs to use vlang package --- PKGBUILD | 4 ++-- PKGBUILD.dev | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 6b664d1f..14ea71b2 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -5,9 +5,9 @@ pkgbase='vieter' pkgname='vieter' pkgver='0.3.0' pkgrel=1 -pkgdesc="Vieter is a lightweight implementation of an Arch repository server." +pkgdesc="Lightweight Arch repository server & package build system" depends=('glibc' 'openssl' 'libarchive' 'sqlite') -makedepends=('git' 'vieter-v') +makedepends=('git' 'vlang') arch=('x86_64' 'aarch64') url='https://git.rustybever.be/vieter-v/vieter' license=('AGPL3') diff --git a/PKGBUILD.dev b/PKGBUILD.dev index 045e5765..79c7f37f 100644 --- a/PKGBUILD.dev +++ b/PKGBUILD.dev @@ -5,9 +5,9 @@ pkgbase='vieter-git' pkgname='vieter-git' pkgver=0.2.0.r25.g20112b8 pkgrel=1 -pkgdesc="Vieter is a lightweight implementation of an Arch repository server." +pkgdesc="Lightweight Arch repository server & package build system (development version)" depends=('glibc' 'openssl' 'libarchive' 'sqlite') -makedepends=('git' 'vieter-v') +makedepends=('git' 'vlang') arch=('x86_64' 'aarch64') url='https://git.rustybever.be/vieter-v/vieter' license=('AGPL3') From 49ddb312dea5d298952ad460922ed3f518677214 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Thu, 11 Aug 2022 19:07:54 +0200 Subject: [PATCH 101/236] feat(server): added endpoint to remove package from arch-repo --- docs/api/source/includes/_repository.md | 29 +++++++++++++++++++++++++ src/repo/repo.v | 2 +- src/server/routes.v | 23 ++++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/docs/api/source/includes/_repository.md b/docs/api/source/includes/_repository.md index fbbc329a..9764e014 100644 --- a/docs/api/source/includes/_repository.md +++ b/docs/api/source/includes/_repository.md @@ -93,3 +93,32 @@ other already present arch-repos. Parameter | Description --------- | ----------- repo | Repository to publish package to + +## Remove package from arch-repo + + + +```shell +curl \ + -H 'X-Api-Key: secret' \ + -XDELETE \ + https://example.com/vieter/x86_64/mike +``` + +This endpoint allows you to remove a package from a given arch-repo. + +### HTTP Request + +`DELETE /:repo/:arch/:pkg` + +### URL Parameters + +Parameter | Description +--------- | ----------- +repo | Repository to delete package from +arch | Specific arch-repo to remove package from +pkg | Name of package to remove (without any version information) diff --git a/src/repo/repo.v b/src/repo/repo.v index c4b85c0b..7de12cdb 100644 --- a/src/repo/repo.v +++ b/src/repo/repo.v @@ -158,7 +158,7 @@ fn (r &RepoGroupManager) add_pkg_in_arch_repo(repo string, arch string, pkg &pac // remove_pkg_from_arch_repo removes a package from an arch-repo's database. It // returns false if the package wasn't present in the database. It also // optionally re-syncs the repo archives. -fn (r &RepoGroupManager) remove_pkg_from_arch_repo(repo string, arch string, pkg_name string, sync bool) ?bool { +pub fn (r &RepoGroupManager) remove_pkg_from_arch_repo(repo string, arch string, pkg_name string, sync bool) ?bool { repo_dir := os.join_path(r.repos_dir, repo, arch) // If the repository doesn't exist yet, the result is automatically false diff --git a/src/server/routes.v b/src/server/routes.v index fbf37dfb..3b86e20b 100644 --- a/src/server/routes.v +++ b/src/server/routes.v @@ -57,6 +57,29 @@ fn (mut app App) get_repo_file(repo string, arch string, filename string) web.Re return app.file(full_path) } +['/:repo/:arch/:pkg'; delete] +fn (mut app App) delete_package(repo string, arch string, pkg string) web.Result { + if !app.is_authorized() { + return app.json(http.Status.unauthorized, new_response('Unauthorized.')) + } + + res := app.repo.remove_pkg_from_arch_repo(repo, arch, pkg, true) or { + app.lerror('Error while deleting package: $err.msg()') + + return app.json(http.Status.internal_server_error, new_response('Failed to delete package.')) + } + + if res { + app.linfo("Removed package '$pkg' from '$repo/$arch'") + + return app.json(http.Status.ok, new_response('Package removed.')) + } else { + app.linfo("Tried removing package '$pkg' from '$repo/$arch', but it doesn't exist.") + + return app.json(http.Status.not_found, new_response('Package not found.')) + } +} + // put_package handles publishing a package to a repository. ['/:repo/publish'; post] fn (mut app App) put_package(repo string) web.Result { From 6283cbea9cf0c655ea43a146bada31aef5ee9d7f Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Thu, 11 Aug 2022 19:16:38 +0200 Subject: [PATCH 102/236] feat(repo): added function to remove arch-repo --- src/repo/{repo.v => add.v} | 48 --------------------------- src/repo/remove.v | 68 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 48 deletions(-) rename src/repo/{repo.v => add.v} (77%) create mode 100644 src/repo/remove.v diff --git a/src/repo/repo.v b/src/repo/add.v similarity index 77% rename from src/repo/repo.v rename to src/repo/add.v index 7de12cdb..0985c7a6 100644 --- a/src/repo/repo.v +++ b/src/repo/add.v @@ -154,51 +154,3 @@ fn (r &RepoGroupManager) add_pkg_in_arch_repo(repo string, arch string, pkg &pac return true } - -// remove_pkg_from_arch_repo removes a package from an arch-repo's database. It -// returns false if the package wasn't present in the database. It also -// optionally re-syncs the repo archives. -pub fn (r &RepoGroupManager) remove_pkg_from_arch_repo(repo string, arch string, pkg_name string, sync bool) ?bool { - repo_dir := os.join_path(r.repos_dir, repo, arch) - - // If the repository doesn't exist yet, the result is automatically false - if !os.exists(repo_dir) { - return false - } - - // We iterate over every directory in the repo dir - // TODO filter so we only check directories - for d in os.ls(repo_dir)? { - // Because a repository only allows a single version of each package, - // we need only compare whether the name of the package is the same, - // not the version. - name := d.split('-')#[..-2].join('-') - - if name == pkg_name { - // We lock the mutex here to prevent other routines from creating a - // new archive while we remove an entry - lock r.mutex { - os.rmdir_all(os.join_path_single(repo_dir, d))? - } - - // Also remove the package archive - repo_pkg_dir := os.join_path(r.pkg_dir, repo, arch) - - archives := os.ls(repo_pkg_dir)?.filter(it.split('-')#[..-3].join('-') == name) - - for archive_name in archives { - full_path := os.join_path_single(repo_pkg_dir, archive_name) - os.rm(full_path)? - } - - // Sync the db archives if requested - if sync { - r.sync(repo, arch)? - } - - return true - } - } - - return false -} diff --git a/src/repo/remove.v b/src/repo/remove.v new file mode 100644 index 00000000..dd4f4004 --- /dev/null +++ b/src/repo/remove.v @@ -0,0 +1,68 @@ +module repo + +import os + +// remove_pkg_from_arch_repo removes a package from an arch-repo's database. It +// returns false if the package wasn't present in the database. It also +// optionally re-syncs the repo archives. +pub fn (r &RepoGroupManager) remove_pkg_from_arch_repo(repo string, arch string, pkg_name string, sync bool) ?bool { + repo_dir := os.join_path(r.repos_dir, repo, arch) + + // If the repository doesn't exist yet, the result is automatically false + if !os.exists(repo_dir) { + return false + } + + // We iterate over every directory in the repo dir + // TODO filter so we only check directories + for d in os.ls(repo_dir)? { + // Because a repository only allows a single version of each package, + // we need only compare whether the name of the package is the same, + // not the version. + name := d.split('-')#[..-2].join('-') + + if name == pkg_name { + // We lock the mutex here to prevent other routines from creating a + // new archive while we remove an entry + lock r.mutex { + os.rmdir_all(os.join_path_single(repo_dir, d))? + } + + // Also remove the package archive + repo_pkg_dir := os.join_path(r.pkg_dir, repo, arch) + + archives := os.ls(repo_pkg_dir)?.filter(it.split('-')#[..-3].join('-') == name) + + for archive_name in archives { + full_path := os.join_path_single(repo_pkg_dir, archive_name) + os.rm(full_path)? + } + + // Sync the db archives if requested + if sync { + r.sync(repo, arch)? + } + + return true + } + } + + return false +} + +// remove_arch_repo removes an arch-repo & its packages. +pub fn (r &RepoGroupManager) remove_arch_repo(repo string, arch string) ?bool { + repo_dir := os.join_path(r.repos_dir, repo, arch) + + // If the repository doesn't exist yet, the result is automatically false + if !os.exists(repo_dir) { + return false + } + + os.rmdir_all(repo_dir)? + + pkg_dir := os.join_path(r.pkg_dir, repo, arch) + os.rmdir_all(pkg_dir)? + + return true +} From 68b7e5e71ec636fe1fdcb7338f02dd3f70e922ed Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Thu, 11 Aug 2022 19:28:17 +0200 Subject: [PATCH 103/236] feat(server): added routes for removing arch-repo & repo --- docs/api/source/includes/_repository.md | 55 ++++++++++++++++++ src/repo/remove.v | 17 ++++++ src/server/{routes.v => repo.v} | 23 -------- src/server/repo_remove.v | 77 +++++++++++++++++++++++++ 4 files changed, 149 insertions(+), 23 deletions(-) rename src/server/{routes.v => repo.v} (82%) create mode 100644 src/server/repo_remove.v diff --git a/docs/api/source/includes/_repository.md b/docs/api/source/includes/_repository.md index 9764e014..a8469043 100644 --- a/docs/api/source/includes/_repository.md +++ b/docs/api/source/includes/_repository.md @@ -122,3 +122,58 @@ Parameter | Description repo | Repository to delete package from arch | Specific arch-repo to remove package from pkg | Name of package to remove (without any version information) + +## Remove arch-repo + + + +```shell +curl \ + -H 'X-Api-Key: secret' \ + -XDELETE \ + https://example.com/vieter/x86_64 +``` + +This endpoint allows remove an entire arch-repo. + +### HTTP Request + +`DELETE /:repo/:arch` + +### URL Parameters + +Parameter | Description +--------- | ----------- +repo | Repository to delete arch-repo from +arch | Specific architecture to remove package + +## Remove repo + + + +```shell +curl \ + -H 'X-Api-Key: secret' \ + -XDELETE \ + https://example.com/vieter +``` + +This endpoint allows remove an entire repo. + +### HTTP Request + +`DELETE /:repo` + +### URL Parameters + +Parameter | Description +--------- | ----------- +repo | Repository to delete diff --git a/src/repo/remove.v b/src/repo/remove.v index dd4f4004..add921cb 100644 --- a/src/repo/remove.v +++ b/src/repo/remove.v @@ -66,3 +66,20 @@ pub fn (r &RepoGroupManager) remove_arch_repo(repo string, arch string) ?bool { return true } + +// remove_repo removes a repo & its packages. +pub fn (r &RepoGroupManager) remove_repo(repo string) ?bool { + repo_dir := os.join_path_single(r.repos_dir, repo) + + // If the repository doesn't exist yet, the result is automatically false + if !os.exists(repo_dir) { + return false + } + + os.rmdir_all(repo_dir)? + + pkg_dir := os.join_path_single(r.pkg_dir, repo) + os.rmdir_all(pkg_dir)? + + return true +} diff --git a/src/server/routes.v b/src/server/repo.v similarity index 82% rename from src/server/routes.v rename to src/server/repo.v index 3b86e20b..fbf37dfb 100644 --- a/src/server/routes.v +++ b/src/server/repo.v @@ -57,29 +57,6 @@ fn (mut app App) get_repo_file(repo string, arch string, filename string) web.Re return app.file(full_path) } -['/:repo/:arch/:pkg'; delete] -fn (mut app App) delete_package(repo string, arch string, pkg string) web.Result { - if !app.is_authorized() { - return app.json(http.Status.unauthorized, new_response('Unauthorized.')) - } - - res := app.repo.remove_pkg_from_arch_repo(repo, arch, pkg, true) or { - app.lerror('Error while deleting package: $err.msg()') - - return app.json(http.Status.internal_server_error, new_response('Failed to delete package.')) - } - - if res { - app.linfo("Removed package '$pkg' from '$repo/$arch'") - - return app.json(http.Status.ok, new_response('Package removed.')) - } else { - app.linfo("Tried removing package '$pkg' from '$repo/$arch', but it doesn't exist.") - - return app.json(http.Status.not_found, new_response('Package not found.')) - } -} - // put_package handles publishing a package to a repository. ['/:repo/publish'; post] fn (mut app App) put_package(repo string) web.Result { diff --git a/src/server/repo_remove.v b/src/server/repo_remove.v new file mode 100644 index 00000000..642f26f2 --- /dev/null +++ b/src/server/repo_remove.v @@ -0,0 +1,77 @@ +module server + +import web +import net.http +import response { new_response } + +// delete_package tries to remove the given package. +['/:repo/:arch/:pkg'; delete] +fn (mut app App) delete_package(repo string, arch string, pkg string) web.Result { + if !app.is_authorized() { + return app.json(http.Status.unauthorized, new_response('Unauthorized.')) + } + + res := app.repo.remove_pkg_from_arch_repo(repo, arch, pkg, true) or { + app.lerror('Error while deleting package: $err.msg()') + + return app.json(http.Status.internal_server_error, new_response('Failed to delete package.')) + } + + if res { + app.linfo("Removed package '$pkg' from '$repo/$arch'") + + return app.json(http.Status.ok, new_response('Package removed.')) + } else { + app.linfo("Tried removing package '$pkg' from '$repo/$arch', but it doesn't exist.") + + return app.json(http.Status.not_found, new_response('Package not found.')) + } +} + +// delete_arch_repo tries to remove the given arch-repo. +['/:repo/:arch'; delete] +fn (mut app App) delete_arch_repo(repo string, arch string) web.Result { + if !app.is_authorized() { + return app.json(http.Status.unauthorized, new_response('Unauthorized.')) + } + + res := app.repo.remove_arch_repo(repo, arch) or { + app.lerror('Error while deleting arch-repo: $err.msg()') + + return app.json(http.Status.internal_server_error, new_response('Failed to delete arch-repo.')) + } + + if res { + app.linfo("Removed '$repo/$arch'") + + return app.json(http.Status.ok, new_response('Arch-repo removed.')) + } else { + app.linfo("Tried removing '$repo/$arch', but it doesn't exist.") + + return app.json(http.Status.not_found, new_response('Arch-repo not found.')) + } +} + +// delete_repo tries to remove the given repo. +['/:repo'; delete] +fn (mut app App) delete_repo(repo string) web.Result { + if !app.is_authorized() { + return app.json(http.Status.unauthorized, new_response('Unauthorized.')) + } + + res := app.repo.remove_repo(repo) or { + app.lerror('Error while deleting repo: $err.msg()') + + return app.json(http.Status.internal_server_error, new_response('Failed to delete repo.')) + } + + if res { + app.linfo("Removed '$repo'") + + return app.json(http.Status.ok, new_response('Repo removed.')) + } else { + app.linfo("Tried removing '$repo', but it doesn't exist.") + + return app.json(http.Status.not_found, new_response('Repo not found.')) + } +} From ba3b00572b3ddf31490f054b5bbf2288913da5af Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Thu, 11 Aug 2022 19:44:22 +0200 Subject: [PATCH 104/236] chore: updated changelog [CI SKIP] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a03c18cc..b7138a84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Targets with kind 'url' can provide a direct URL to a PKGBUILD instead of providing a Git repository * CLI commands for searching the AUR & directly adding packages +* HTTP routes for removing packages, arch-repos & repos ### Changed From 50918da67214ff69a29343f04a68f52e28f602dc Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Thu, 11 Aug 2022 19:47:26 +0200 Subject: [PATCH 105/236] docs: fixed some typos --- docs/api/source/includes/_repository.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/api/source/includes/_repository.md b/docs/api/source/includes/_repository.md index a8469043..ff17f719 100644 --- a/docs/api/source/includes/_repository.md +++ b/docs/api/source/includes/_repository.md @@ -138,7 +138,7 @@ curl \ https://example.com/vieter/x86_64 ``` -This endpoint allows remove an entire arch-repo. +This endpoint allows removing an entire arch-repo. ### HTTP Request @@ -149,7 +149,7 @@ This endpoint allows remove an entire arch-repo. Parameter | Description --------- | ----------- repo | Repository to delete arch-repo from -arch | Specific architecture to remove package +arch | Specific architecture to remove ## Remove repo @@ -166,7 +166,7 @@ curl \ https://example.com/vieter ``` -This endpoint allows remove an entire repo. +This endpoint allows removing an entire repo. ### HTTP Request From 3a73ea0632df8c804d3a6d7a3ed7ae59f001ff65 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Fri, 12 Aug 2022 14:39:42 +0200 Subject: [PATCH 106/236] refactor(web): simplified web framework in general --- src/web/consts.v | 125 +++++++++++++++++++++ src/web/web.v | 275 +++++++++++++++-------------------------------- 2 files changed, 209 insertions(+), 191 deletions(-) create mode 100644 src/web/consts.v diff --git a/src/web/consts.v b/src/web/consts.v new file mode 100644 index 00000000..7b1d2b47 --- /dev/null +++ b/src/web/consts.v @@ -0,0 +1,125 @@ +module web + +import net.http + +// A dummy structure that returns from routes to indicate that you actually sent something to a user +[noinit] +pub struct Result {} + +pub const ( + methods_with_form = [http.Method.post, .put, .patch] + headers_close = http.new_custom_header_from_map({ + 'Server': 'VWeb' + http.CommonHeader.connection.str(): 'close' + }) or { panic('should never fail') } + + http_302 = http.new_response( + status: .found + body: '302 Found' + header: headers_close + ) + http_400 = http.new_response( + status: .bad_request + body: '400 Bad Request' + header: http.new_header( + key: .content_type + value: 'text/plain' + ).join(headers_close) + ) + http_404 = http.new_response( + status: .not_found + body: '404 Not Found' + header: http.new_header( + key: .content_type + value: 'text/plain' + ).join(headers_close) + ) + http_500 = http.new_response( + status: .internal_server_error + body: '500 Internal Server Error' + header: http.new_header( + key: .content_type + value: 'text/plain' + ).join(headers_close) + ) + mime_types = { + '.aac': 'audio/aac' + '.abw': 'application/x-abiword' + '.arc': 'application/x-freearc' + '.avi': 'video/x-msvideo' + '.azw': 'application/vnd.amazon.ebook' + '.bin': 'application/octet-stream' + '.bmp': 'image/bmp' + '.bz': 'application/x-bzip' + '.bz2': 'application/x-bzip2' + '.cda': 'application/x-cdf' + '.csh': 'application/x-csh' + '.css': 'text/css' + '.csv': 'text/csv' + '.doc': 'application/msword' + '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + '.eot': 'application/vnd.ms-fontobject' + '.epub': 'application/epub+zip' + '.gz': 'application/gzip' + '.gif': 'image/gif' + '.htm': 'text/html' + '.html': 'text/html' + '.ico': 'image/vnd.microsoft.icon' + '.ics': 'text/calendar' + '.jar': 'application/java-archive' + '.jpeg': 'image/jpeg' + '.jpg': 'image/jpeg' + '.js': 'text/javascript' + '.json': 'application/json' + '.jsonld': 'application/ld+json' + '.mid': 'audio/midi audio/x-midi' + '.midi': 'audio/midi audio/x-midi' + '.mjs': 'text/javascript' + '.mp3': 'audio/mpeg' + '.mp4': 'video/mp4' + '.mpeg': 'video/mpeg' + '.mpkg': 'application/vnd.apple.installer+xml' + '.odp': 'application/vnd.oasis.opendocument.presentation' + '.ods': 'application/vnd.oasis.opendocument.spreadsheet' + '.odt': 'application/vnd.oasis.opendocument.text' + '.oga': 'audio/ogg' + '.ogv': 'video/ogg' + '.ogx': 'application/ogg' + '.opus': 'audio/opus' + '.otf': 'font/otf' + '.png': 'image/png' + '.pdf': 'application/pdf' + '.php': 'application/x-httpd-php' + '.ppt': 'application/vnd.ms-powerpoint' + '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation' + '.rar': 'application/vnd.rar' + '.rtf': 'application/rtf' + '.sh': 'application/x-sh' + '.svg': 'image/svg+xml' + '.swf': 'application/x-shockwave-flash' + '.tar': 'application/x-tar' + '.tif': 'image/tiff' + '.tiff': 'image/tiff' + '.ts': 'video/mp2t' + '.ttf': 'font/ttf' + '.txt': 'text/plain' + '.vsd': 'application/vnd.visio' + '.wav': 'audio/wav' + '.weba': 'audio/webm' + '.webm': 'video/webm' + '.webp': 'image/webp' + '.woff': 'font/woff' + '.woff2': 'font/woff2' + '.xhtml': 'application/xhtml+xml' + '.xls': 'application/vnd.ms-excel' + '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + '.xml': 'application/xml' + '.xul': 'application/vnd.mozilla.xul+xml' + '.zip': 'application/zip' + '.3gp': 'video/3gpp' + '.3g2': 'video/3gpp2' + '.7z': 'application/x-7z-compressed' + } + max_http_post_size = 1024 * 1024 + default_port = 8080 +) diff --git a/src/web/web.v b/src/web/web.v index b053904d..9fe0ddcb 100644 --- a/src/web/web.v +++ b/src/web/web.v @@ -12,146 +12,23 @@ import time import json import log -// A dummy structure that returns from routes to indicate that you actually sent something to a user -[noinit] -pub struct Result {} - -pub const ( - methods_with_form = [http.Method.post, .put, .patch] - headers_close = http.new_custom_header_from_map({ - 'Server': 'VWeb' - http.CommonHeader.connection.str(): 'close' - }) or { panic('should never fail') } - - http_302 = http.new_response( - status: .found - body: '302 Found' - header: headers_close - ) - http_400 = http.new_response( - status: .bad_request - body: '400 Bad Request' - header: http.new_header( - key: .content_type - value: 'text/plain' - ).join(headers_close) - ) - http_404 = http.new_response( - status: .not_found - body: '404 Not Found' - header: http.new_header( - key: .content_type - value: 'text/plain' - ).join(headers_close) - ) - http_500 = http.new_response( - status: .internal_server_error - body: '500 Internal Server Error' - header: http.new_header( - key: .content_type - value: 'text/plain' - ).join(headers_close) - ) - mime_types = { - '.aac': 'audio/aac' - '.abw': 'application/x-abiword' - '.arc': 'application/x-freearc' - '.avi': 'video/x-msvideo' - '.azw': 'application/vnd.amazon.ebook' - '.bin': 'application/octet-stream' - '.bmp': 'image/bmp' - '.bz': 'application/x-bzip' - '.bz2': 'application/x-bzip2' - '.cda': 'application/x-cdf' - '.csh': 'application/x-csh' - '.css': 'text/css' - '.csv': 'text/csv' - '.doc': 'application/msword' - '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' - '.eot': 'application/vnd.ms-fontobject' - '.epub': 'application/epub+zip' - '.gz': 'application/gzip' - '.gif': 'image/gif' - '.htm': 'text/html' - '.html': 'text/html' - '.ico': 'image/vnd.microsoft.icon' - '.ics': 'text/calendar' - '.jar': 'application/java-archive' - '.jpeg': 'image/jpeg' - '.jpg': 'image/jpeg' - '.js': 'text/javascript' - '.json': 'application/json' - '.jsonld': 'application/ld+json' - '.mid': 'audio/midi audio/x-midi' - '.midi': 'audio/midi audio/x-midi' - '.mjs': 'text/javascript' - '.mp3': 'audio/mpeg' - '.mp4': 'video/mp4' - '.mpeg': 'video/mpeg' - '.mpkg': 'application/vnd.apple.installer+xml' - '.odp': 'application/vnd.oasis.opendocument.presentation' - '.ods': 'application/vnd.oasis.opendocument.spreadsheet' - '.odt': 'application/vnd.oasis.opendocument.text' - '.oga': 'audio/ogg' - '.ogv': 'video/ogg' - '.ogx': 'application/ogg' - '.opus': 'audio/opus' - '.otf': 'font/otf' - '.png': 'image/png' - '.pdf': 'application/pdf' - '.php': 'application/x-httpd-php' - '.ppt': 'application/vnd.ms-powerpoint' - '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation' - '.rar': 'application/vnd.rar' - '.rtf': 'application/rtf' - '.sh': 'application/x-sh' - '.svg': 'image/svg+xml' - '.swf': 'application/x-shockwave-flash' - '.tar': 'application/x-tar' - '.tif': 'image/tiff' - '.tiff': 'image/tiff' - '.ts': 'video/mp2t' - '.ttf': 'font/ttf' - '.txt': 'text/plain' - '.vsd': 'application/vnd.visio' - '.wav': 'audio/wav' - '.weba': 'audio/webm' - '.webm': 'video/webm' - '.webp': 'image/webp' - '.woff': 'font/woff' - '.woff2': 'font/woff2' - '.xhtml': 'application/xhtml+xml' - '.xls': 'application/vnd.ms-excel' - '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' - '.xml': 'application/xml' - '.xul': 'application/vnd.mozilla.xul+xml' - '.zip': 'application/zip' - '.3gp': 'video/3gpp' - '.3g2': 'video/3gpp2' - '.7z': 'application/x-7z-compressed' - } - max_http_post_size = 1024 * 1024 - default_port = 8080 -) - // The Context struct represents the Context which hold the HTTP request and response. // It has fields for the query, form, files. pub struct Context { -mut: - content_type string = 'text/plain' - status http.Status = http.Status.ok pub: // HTTP Request req http.Request // TODO Response pub mut: - done bool - // time.ticks() from start of web connection handle. - // You can use it to determine how much time is spent on your request. - page_gen_start i64 // TCP connection to client. // But beware, do not store it for further use, after request processing web will close connection. - conn &net.TcpConn + conn &net.TcpConn + // Gives access to a shared logger object + logger shared log.Log + // REQUEST + // time.ticks() from start of web connection handle. + // You can use it to determine how much time is spent on your request. + page_gen_start i64 static_files map[string]string static_mime_types map[string]string // Map containing query params for the route. @@ -161,14 +38,13 @@ pub mut: form map[string]string // Files from multipart-form. files map[string][]http.FileData - - header http.Header // response headers - // ? It doesn't seem to be used anywhere - form_error string // Allows reading the request body reader io.BufferedReader - // Gives access to a shared logger object - logger shared log.Log + // RESPONSE + status http.Status = http.Status.ok + content_type string = 'text/plain' + // response headers + header http.Header } struct FileData { @@ -188,40 +64,68 @@ struct Route { // 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())? +// send_string writes the given string to the TCP connection socket. +fn (mut ctx Context) send_string(s string) ? { + ctx.conn.write(s.bytes())? } -// send_response_to_client sends a response to the client -[manualfree] -pub fn (mut ctx Context) send_response_to_client(mimetype string, res string) bool { - if ctx.done { - return false +// send_reader reads at most `size` bytes from the given reader & writes them +// to the TCP connection socket. Internally, a 10KB buffer is used, to avoid +// having to store all bytes in memory at once. +fn (mut ctx Context) send_reader(mut reader io.Reader, size u64) ? { + mut buf := []u8{len: 10_000} + mut bytes_left := size + + // Repeat as long as the stream still has data + for bytes_left > 0 { + bytes_read := reader.read(mut buf)? + bytes_left -= u64(bytes_read) + + mut to_write := bytes_read + + for to_write > 0 { + // TODO don't just loop infinitely here + bytes_written := ctx.conn.write(buf[bytes_read - to_write..bytes_read]) or { continue } + + to_write = to_write - bytes_written + } } - ctx.done = true - - // build header - header := http.new_header_from_map({ - http.CommonHeader.content_type: mimetype - http.CommonHeader.content_length: res.len.str() - }).join(ctx.header) +} +// send_response_header constructs a valid HTTP response with an empty body & +// sends it to the client. +pub fn (mut ctx Context) send_response_header() ? { mut resp := http.Response{ - header: header.join(web.headers_close) - body: res + header: ctx.header.join(headers_close) } resp.set_version(.v1_1) resp.set_status(ctx.status) - send_string(mut ctx.conn, resp.bytestr()) or { return false } + ctx.send_string(resp.bytestr())? +} + +// send_response constructs the resulting HTTP response with the given body +// string & sends it to the client. +pub fn (mut ctx Context) send_response(res string) bool { + ctx.send_response_header() or { return false } + ctx.send_string(res) or { return false } + + return true +} + +// send_reader_response constructs the resulting HTTP response with the given +// body & streams the reader's contents to the client. +pub fn (mut ctx Context) send_reader_response(mut reader io.Reader, size u64) bool { + ctx.send_response_header() or { return false } + ctx.send_reader(mut reader, size) 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 - - ctx.send_response_to_client('text/plain', s) + ctx.content_type = 'text/plain' + ctx.send_response(s) return Result{} } @@ -229,9 +133,10 @@ pub fn (mut ctx Context) text(status http.Status, s string) 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 + ctx.content_type = 'application/json' json_s := json.encode(j) - ctx.send_response_to_client('application/json', json_s) + ctx.send_response(json_s) return Result{} } @@ -239,10 +144,6 @@ pub fn (mut ctx Context) json(status http.Status, j T) Result { // file Response HTTP_OK with file as payload // This function manually implements responses because it needs to stream the file contents pub fn (mut ctx Context) file(f_path string) Result { - if ctx.done { - return Result{} - } - if !os.is_file(f_path) { return ctx.not_found() } @@ -266,7 +167,7 @@ pub fn (mut ctx Context) file(f_path string) Result { // We open the file before sending the headers in case reading fails file_size := os.file_size(f_path) - file := os.open(f_path) or { + mut file := os.open(f_path) or { eprintln(err.msg()) ctx.server_error(500) return Result{} @@ -279,32 +180,32 @@ pub fn (mut ctx Context) file(f_path string) Result { }).join(ctx.header) mut resp := http.Response{ - header: header.join(web.headers_close) + header: header.join(headers_close) } resp.set_version(.v1_1) resp.set_status(ctx.status) - send_string(mut ctx.conn, resp.bytestr()) or { return Result{} } + ctx.send_string(resp.bytestr()) or { return Result{} } + ctx.send_reader(mut file, file_size) or { return Result{} } - mut buf := []u8{len: 1_000_000} - mut bytes_left := file_size + // mut buf := []u8{len: 1_000_000} + // mut bytes_left := file_size - // Repeat as long as the stream still has data - for bytes_left > 0 { - // TODO check if just breaking here is safe - bytes_read := file.read(mut buf) or { break } - bytes_left -= u64(bytes_read) + // // Repeat as long as the stream still has data + // for bytes_left > 0 { + // // TODO check if just breaking here is safe + // bytes_read := file.read(mut buf) or { break } + // bytes_left -= u64(bytes_read) - mut to_write := bytes_read + // mut to_write := bytes_read - for to_write > 0 { - // TODO don't just loop infinitely here - bytes_written := ctx.conn.write(buf[bytes_read - to_write..bytes_read]) or { continue } + // for to_write > 0 { + // // TODO don't just loop infinitely here + // bytes_written := ctx.conn.write(buf[bytes_read - to_write..bytes_read]) or { continue } - to_write = to_write - bytes_written - } - } + // to_write = to_write - bytes_written + // } + // } - ctx.done = true return Result{} } @@ -319,23 +220,16 @@ pub fn (mut ctx Context) server_error(ecode int) Result { $if debug { eprintln('> ctx.server_error ecode: $ecode') } - if ctx.done { - return Result{} - } - send_string(mut ctx.conn, web.http_500.bytestr()) or {} + ctx.send_string(http_500.bytestr()) or {} return Result{} } // redirect Redirect to an url pub fn (mut ctx Context) redirect(url string) Result { - if ctx.done { - return Result{} - } - ctx.done = true - mut resp := web.http_302 + mut resp := http_302 resp.header = resp.header.join(ctx.header) resp.header.add(.location, url) - send_string(mut ctx.conn, resp.bytestr()) or { return Result{} } + ctx.send_string(resp.bytestr()) or { return Result{} } return Result{} } @@ -532,7 +426,7 @@ fn handle_conn(mut conn net.TcpConn, mut app T, routes map[string]Route) { } } // Route not found - conn.write(web.http_404.bytes()) or {} + conn.write(http_404.bytes()) or {} } // route_matches returns wether a route matches @@ -597,7 +491,6 @@ pub fn (ctx &Context) ip() string { // error Set s to the form error pub fn (mut ctx Context) error(s string) { println('web error: $s') - ctx.form_error = s } // filter Do not delete. From e7b45bf251736cbd60fcb49aa45bd2b7012125fe Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Fri, 12 Aug 2022 15:08:05 +0200 Subject: [PATCH 107/236] feat(web): file() now handles HEAD requests --- src/server/repo.v | 9 ----- src/web/web.v | 90 +++++++++++------------------------------------ 2 files changed, 20 insertions(+), 79 deletions(-) diff --git a/src/server/repo.v b/src/server/repo.v index fbf37dfb..4a417fb0 100644 --- a/src/server/repo.v +++ b/src/server/repo.v @@ -45,15 +45,6 @@ fn (mut app App) get_repo_file(repo string, arch string, filename string) web.Re full_path = os.join_path(app.repo.repos_dir, repo, arch, filename, 'desc') } - // 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.not_found() - } - return app.file(full_path) } diff --git a/src/web/web.v b/src/web/web.v index 9fe0ddcb..c4cfee76 100644 --- a/src/web/web.v +++ b/src/web/web.v @@ -25,10 +25,10 @@ pub mut: conn &net.TcpConn // Gives access to a shared logger object logger shared log.Log - // REQUEST // time.ticks() from start of web connection handle. // You can use it to determine how much time is spent on your request. page_gen_start i64 + // REQUEST static_files map[string]string static_mime_types map[string]string // Map containing query params for the route. @@ -103,6 +103,12 @@ pub fn (mut ctx Context) send_response_header() ? { ctx.send_string(resp.bytestr())? } +// send is a convenience function for sending the HTTP response with an empty +// body. +pub fn (mut ctx Context) send() bool { + return ctx.send_response('') +} + // send_response constructs the resulting HTTP response with the given body // string & sends it to the client. pub fn (mut ctx Context) send_response(res string) bool { @@ -144,67 +150,32 @@ pub fn (mut ctx Context) json(status http.Status, j T) Result { // file Response HTTP_OK with file as payload // This function manually implements responses because it needs to stream the file contents pub fn (mut ctx Context) file(f_path string) Result { + // If the file doesn't exist, just respond with a 404 if !os.is_file(f_path) { - return ctx.not_found() + ctx.status = .not_found + ctx.send() + + return Result{} } - // ext := os.file_ext(f_path) - // data := os.read_file(f_path) or { - // eprint(err.msg()) - // ctx.server_error(500) - // return Result{} - // } - // content_type := web.mime_types[ext] - // if content_type == '' { - // eprintln('no MIME type found for extension $ext') - // ctx.server_error(500) + file_size := os.file_size(f_path) + ctx.header.add(http.CommonHeader.content_length, file_size.str()) - // return Result{} - // } + // A HEAD request only returns the size of the file. + if ctx.req.method == .head { + ctx.send() - // First, we return the headers for the request + return Result{} + } // We open the file before sending the headers in case reading fails - file_size := os.file_size(f_path) - mut file := os.open(f_path) or { eprintln(err.msg()) ctx.server_error(500) return Result{} } - // build header - header := http.new_header_from_map({ - // http.CommonHeader.content_type: content_type - http.CommonHeader.content_length: file_size.str() - }).join(ctx.header) - - mut resp := http.Response{ - header: header.join(headers_close) - } - resp.set_version(.v1_1) - resp.set_status(ctx.status) - ctx.send_string(resp.bytestr()) or { return Result{} } - ctx.send_reader(mut file, file_size) or { return Result{} } - - // mut buf := []u8{len: 1_000_000} - // mut bytes_left := file_size - - // // Repeat as long as the stream still has data - // for bytes_left > 0 { - // // TODO check if just breaking here is safe - // bytes_read := file.read(mut buf) or { break } - // bytes_left -= u64(bytes_read) - - // mut to_write := bytes_read - - // for to_write > 0 { - // // TODO don't just loop infinitely here - // bytes_written := ctx.conn.write(buf[bytes_read - to_write..bytes_read]) or { continue } - - // to_write = to_write - bytes_written - // } - // } + ctx.send_reader_response(mut file, file_size) return Result{} } @@ -472,27 +443,6 @@ fn route_matches(url_words []string, route_words []string) ?[]string { return params } -// 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 { '' } - if ip == '' { - ip = ctx.req.header.get_custom('X-Real-Ip') or { '' } - } - - if ip.contains(',') { - ip = ip.all_before(',') - } - if ip == '' { - ip = ctx.conn.peer_ip() or { '' } - } - return ip -} - -// error Set s to the form error -pub fn (mut ctx Context) error(s string) { - println('web error: $s') -} - // 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 cc5df95a1a9b742ca3945bfda16c1df53b294a0c Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Fri, 12 Aug 2022 17:11:44 +0200 Subject: [PATCH 108/236] feat(web): file() now supports HTTP byte range --- src/server/repo.v | 2 +- src/web/web.v | 64 ++++++++++++++++++++++++++++++++++++----------- 2 files changed, 50 insertions(+), 16 deletions(-) diff --git a/src/server/repo.v b/src/server/repo.v index 4a417fb0..2253a441 100644 --- a/src/server/repo.v +++ b/src/server/repo.v @@ -13,7 +13,7 @@ import response { new_response } // 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.json(.ok, new_response('Healthy.')) } // get_repo_file handles all Pacman-related routes. It returns both the diff --git a/src/web/web.v b/src/web/web.v index c4cfee76..51acd2f2 100644 --- a/src/web/web.v +++ b/src/web/web.v @@ -27,7 +27,7 @@ pub mut: logger shared log.Log // time.ticks() from start of web connection handle. // You can use it to determine how much time is spent on your request. - page_gen_start i64 + page_gen_start i64 // REQUEST static_files map[string]string static_mime_types map[string]string @@ -84,8 +84,7 @@ fn (mut ctx Context) send_reader(mut reader io.Reader, size u64) ? { mut to_write := bytes_read for to_write > 0 { - // TODO don't just loop infinitely here - bytes_written := ctx.conn.write(buf[bytes_read - to_write..bytes_read]) or { continue } + bytes_written := ctx.conn.write(buf[bytes_read - to_write..bytes_read]) or { break } to_write = to_write - bytes_written } @@ -127,15 +126,6 @@ pub fn (mut ctx Context) send_reader_response(mut reader io.Reader, size u64) 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 - ctx.content_type = 'text/plain' - ctx.send_response(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 @@ -158,6 +148,8 @@ pub fn (mut ctx Context) file(f_path string) Result { return Result{} } + ctx.header.add(.accept_ranges, 'bytes') + file_size := os.file_size(f_path) ctx.header.add(http.CommonHeader.content_length, file_size.str()) @@ -168,14 +160,53 @@ pub fn (mut ctx Context) file(f_path string) Result { return Result{} } - // We open the file before sending the headers in case reading fails mut file := os.open(f_path) or { eprintln(err.msg()) ctx.server_error(500) return Result{} } - ctx.send_reader_response(mut file, file_size) + defer { + file.close() + } + + if range_str := ctx.req.header.get(.range) { + mut parts := range_str.split_nth('=', 2) + + if parts[0] != 'bytes' { + ctx.status = .requested_range_not_satisfiable + ctx.header.delete(.content_length) + ctx.send() + return Result{} + } + + parts = parts[1].split_nth('-', 2) + + start := parts[0].i64() + end := if parts[1] == '' { file_size - 1 } else { parts[1].u64() } + + // Either the actual number 0 or the result of an invalid integer + if end == 0 { + ctx.status = .requested_range_not_satisfiable + ctx.header.delete(.content_length) + ctx.send() + return Result{} + } + + // Move cursor to start of data to read + file.seek(start, .start) or { + ctx.server_error(500) + return Result{} + } + + length := end - u64(start) + 1 + + ctx.status = .partial_content + ctx.header.set(.content_length, length.str()) + ctx.send_reader_response(mut file, length) + } else { + ctx.send_reader_response(mut file, file_size) + } return Result{} } @@ -183,7 +214,10 @@ pub fn (mut ctx Context) file(f_path string) 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, '') + ctx.status = status + ctx.send() + + return Result{} } // server_error Response a server error From e23635a1d394f135c60b67a6517d78db0ed0b579 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sat, 13 Aug 2022 13:16:31 +0200 Subject: [PATCH 109/236] refactor: moved response module to web.response --- README.md | 11 ++--------- src/client/client.v | 2 +- src/client/logs.v | 2 +- src/client/targets.v | 2 +- src/server/api_logs.v | 2 +- src/server/api_targets.v | 2 +- src/server/repo.v | 2 +- src/server/repo_remove.v | 2 +- src/{ => web}/response/response.v | 0 9 files changed, 9 insertions(+), 16 deletions(-) rename src/{ => web}/response/response.v (100%) diff --git a/README.md b/README.md index 5911ea21..29ec0f01 100644 --- a/README.md +++ b/README.md @@ -48,15 +48,8 @@ update`. ### Compiler -Vieter compiles with the standard Vlang compiler. However, I do maintain a -[mirror](https://git.rustybever.be/vieter-v/v). This is to ensure my CI does -not break without reason, as I control when & how frequently the mirror is -updated to reflect the official repository. - -If you encounter issues using the latest V compiler, try using my mirror -instead. `make v` will clone the repository & build the mirror. Afterwards, -prepending any make command with `V_PATH=v/v` tells make to use the locally -compiled mirror instead. +I used to maintain a mirror that tracked the latest master, but nowadays, I +solely target V 0.3 as a compiler. ## Contributing diff --git a/src/client/client.v b/src/client/client.v index 2bb1ac20..24e44448 100644 --- a/src/client/client.v +++ b/src/client/client.v @@ -2,7 +2,7 @@ module client import net.http { Method } import net.urllib -import response { Response } +import web.response { Response } import json pub struct Client { diff --git a/src/client/logs.v b/src/client/logs.v index f242f6e8..b52c3d09 100644 --- a/src/client/logs.v +++ b/src/client/logs.v @@ -2,7 +2,7 @@ module client import models { BuildLog, BuildLogFilter } import net.http { Method } -import response { Response } +import web.response { Response } import time // get_build_logs returns all build logs. diff --git a/src/client/targets.v b/src/client/targets.v index 82c78780..f5258a43 100644 --- a/src/client/targets.v +++ b/src/client/targets.v @@ -2,7 +2,7 @@ module client import models { Target, TargetFilter } import net.http { Method } -import response { Response } +import web.response { Response } // get_targets returns a list of targets, given a filter object. pub fn (c &Client) get_targets(filter TargetFilter) ?[]Target { diff --git a/src/server/api_logs.v b/src/server/api_logs.v index fa3338ea..6728392c 100644 --- a/src/server/api_logs.v +++ b/src/server/api_logs.v @@ -3,7 +3,7 @@ module server import web import net.http import net.urllib -import response { new_data_response, new_response } +import web.response { new_data_response, new_response } import db import time import os diff --git a/src/server/api_targets.v b/src/server/api_targets.v index 3867c94b..4cc3a584 100644 --- a/src/server/api_targets.v +++ b/src/server/api_targets.v @@ -2,7 +2,7 @@ module server import web import net.http -import response { new_data_response, new_response } +import web.response { new_data_response, new_response } import db import models { Target, TargetArch, TargetFilter } diff --git a/src/server/repo.v b/src/server/repo.v index 2253a441..242fd2df 100644 --- a/src/server/repo.v +++ b/src/server/repo.v @@ -7,7 +7,7 @@ import time import rand import util import net.http -import response { new_response } +import web.response { new_response } // healthcheck just returns a string, but can be used to quickly check if the // server is still responsive. diff --git a/src/server/repo_remove.v b/src/server/repo_remove.v index 642f26f2..5d5ef156 100644 --- a/src/server/repo_remove.v +++ b/src/server/repo_remove.v @@ -2,7 +2,7 @@ module server import web import net.http -import response { new_response } +import web.response { new_response } // delete_package tries to remove the given package. ['/:repo/:arch/:pkg'; delete] diff --git a/src/response/response.v b/src/web/response/response.v similarity index 100% rename from src/response/response.v rename to src/web/response/response.v From 9268ef0302d9ec3d73dc1fa7f38809655f2adbeb Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sat, 13 Aug 2022 17:49:05 +0200 Subject: [PATCH 110/236] refactor(web): some small cleanup --- CHANGELOG.md | 2 ++ src/server/repo_remove.v | 2 +- src/web/consts.v | 2 +- src/web/web.v | 33 ++++++++++++++++----------------- 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7138a84..5aa0e434 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 providing a Git repository * CLI commands for searching the AUR & directly adding packages * HTTP routes for removing packages, arch-repos & repos +* All endpoints serving files now support HTTP byte range requests ### Changed @@ -25,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Branch name for 'git' targets is now optional; if not provided, the repository will be cloned with the default branch * Build containers now explicitely set the PATH variable +* Refactor of web framework ### Removed diff --git a/src/server/repo_remove.v b/src/server/repo_remove.v index 5d5ef156..316b3872 100644 --- a/src/server/repo_remove.v +++ b/src/server/repo_remove.v @@ -8,7 +8,7 @@ import web.response { new_response } ['/:repo/:arch/:pkg'; delete] fn (mut app App) delete_package(repo string, arch string, pkg string) web.Result { if !app.is_authorized() { - return app.json(http.Status.unauthorized, new_response('Unauthorized.')) + return app.json(.unauthorized, new_response('Unauthorized.')) } res := app.repo.remove_pkg_from_arch_repo(repo, arch, pkg, true) or { diff --git a/src/web/consts.v b/src/web/consts.v index 7b1d2b47..1b5bf08e 100644 --- a/src/web/consts.v +++ b/src/web/consts.v @@ -9,7 +9,7 @@ pub struct Result {} pub const ( methods_with_form = [http.Method.post, .put, .patch] headers_close = http.new_custom_header_from_map({ - 'Server': 'VWeb' + 'Server': 'Vieter' http.CommonHeader.connection.str(): 'close' }) or { panic('should never fail') } diff --git a/src/web/web.v b/src/web/web.v index 51acd2f2..8434a803 100644 --- a/src/web/web.v +++ b/src/web/web.v @@ -91,15 +91,22 @@ fn (mut ctx Context) send_reader(mut reader io.Reader, size u64) ? { } } +// send_custom_response sends the given http.Response to the client. It can be +// used to overwrite the Context object & send a completely custom +// http.Response instead. +fn (mut ctx Context) send_custom_response(resp &http.Response) ? { + ctx.send_string(resp.bytestr())? +} + // send_response_header constructs a valid HTTP response with an empty body & // sends it to the client. pub fn (mut ctx Context) send_response_header() ? { mut resp := http.Response{ header: ctx.header.join(headers_close) } - resp.set_version(.v1_1) resp.set_status(ctx.status) - ctx.send_string(resp.bytestr())? + + ctx.send_custom_response(resp)? } // send is a convenience function for sending the HTTP response with an empty @@ -222,10 +229,8 @@ pub fn (mut ctx Context) status(status http.Status) Result { // server_error Response a server error pub fn (mut ctx Context) server_error(ecode int) Result { - $if debug { - eprintln('> ctx.server_error ecode: $ecode') - } - ctx.send_string(http_500.bytestr()) or {} + ctx.send_custom_response(http_500) or {} + return Result{} } @@ -234,23 +239,17 @@ pub fn (mut ctx Context) redirect(url string) Result { mut resp := http_302 resp.header = resp.header.join(ctx.header) resp.header.add(.location, url) - ctx.send_string(resp.bytestr()) or { return Result{} } + + ctx.send_custom_response(resp) or {} + return Result{} } // not_found Send an not_found response pub fn (mut ctx Context) not_found() Result { - return ctx.status(http.Status.not_found) -} + ctx.send_custom_response(http_404) or {} -// add_header Adds an header to the response with key and val -pub fn (mut ctx Context) add_header(key string, val string) { - ctx.header.add_custom(key, val) or {} -} - -// get_header Returns the header data from the key -pub fn (ctx &Context) get_header(key string) string { - return ctx.req.header.get_custom(key) or { '' } + return Result{} } interface DbInterface { From 4887af26d3e7733ce3be5db570a4861590aade33 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sun, 4 Sep 2022 19:32:22 +0200 Subject: [PATCH 111/236] feat(web): added authentication as function attribute --- src/server/server.v | 1 + src/web/consts.v | 8 +++++++ src/web/parse.v | 6 ++++- src/web/web.v | 53 ++++++++++++++++++++++++++++----------------- 4 files changed, 47 insertions(+), 21 deletions(-) diff --git a/src/server/server.v b/src/server/server.v index 1a9df3f8..9903cea9 100644 --- a/src/server/server.v +++ b/src/server/server.v @@ -73,6 +73,7 @@ pub fn server(conf Config) ? { web.run(&App{ logger: logger + api_key: conf.api_key conf: conf repo: repo db: db diff --git a/src/web/consts.v b/src/web/consts.v index 1b5bf08e..df8cdb25 100644 --- a/src/web/consts.v +++ b/src/web/consts.v @@ -26,6 +26,14 @@ pub const ( value: 'text/plain' ).join(headers_close) ) + http_401 = http.new_response( + status: .unauthorized + body: '401 Unauthorized' + header: http.new_header( + key: .content_type + value: 'text/plain' + ).join(headers_close) + ) http_404 = http.new_response( status: .not_found body: '404 Not Found' diff --git a/src/web/parse.v b/src/web/parse.v index a095f0c9..ee7a72cc 100644 --- a/src/web/parse.v +++ b/src/web/parse.v @@ -3,6 +3,10 @@ module web import net.urllib import net.http +// Method attributes that should be ignored when parsing, as they're used +// elsewhere. +const attrs_to_ignore = ['auth'] + // Parsing function attributes for methods and path. fn parse_attrs(name string, attrs []string) ?([]http.Method, string) { if attrs.len == 0 { @@ -32,7 +36,7 @@ fn parse_attrs(name string, attrs []string) ?([]http.Method, string) { } i++ } - if x.len > 0 { + if x.len > 0 && x.any(!web.attrs_to_ignore.contains(it)) { return IError(http.UnexpectedExtraAttributeError{ attributes: x }) diff --git a/src/web/web.v b/src/web/web.v index 8434a803..0847c7e6 100644 --- a/src/web/web.v +++ b/src/web/web.v @@ -18,6 +18,8 @@ pub struct Context { pub: // HTTP Request req http.Request + // API key used when authenticating requests + api_key string // TODO Response pub mut: // TCP connection to client. @@ -101,9 +103,10 @@ fn (mut ctx Context) send_custom_response(resp &http.Response) ? { // send_response_header constructs a valid HTTP response with an empty body & // sends it to the client. pub fn (mut ctx Context) send_response_header() ? { - mut resp := http.Response{ + mut resp := http.new_response( header: ctx.header.join(headers_close) - } + ) + resp.header.add(.content_type, ctx.content_type) resp.set_status(ctx.status) ctx.send_custom_response(resp)? @@ -133,6 +136,15 @@ pub fn (mut ctx Context) send_reader_response(mut reader io.Reader, size u64) bo return true } +// is_authenticated checks whether the request passes a correct API key. +pub fn (ctx &Context) is_authenticated() bool { + if provided_key := ctx.req.header.get_custom('X-Api-Key') { + return provided_key == ctx.api_key + } + + return false +} + // 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 @@ -177,9 +189,12 @@ pub fn (mut ctx Context) file(f_path string) Result { file.close() } + // Currently, this only supports a single provided range, e.g. + // bytes=0-1023, and not multiple ranges, e.g. bytes=0-50, 100-150 if range_str := ctx.req.header.get(.range) { mut parts := range_str.split_nth('=', 2) + // We only support the 'bytes' range type if parts[0] != 'bytes' { ctx.status = .requested_range_not_satisfiable ctx.header.delete(.content_length) @@ -376,8 +391,10 @@ fn handle_conn(mut conn net.TcpConn, mut app T, routes map[string]Route) { static_mime_types: app.static_mime_types reader: reader logger: app.logger + api_key: app.api_key } + // Calling middleware... app.before_request() @@ -394,31 +411,27 @@ fn handle_conn(mut conn net.TcpConn, mut app T, routes map[string]Route) { // Used for route matching route_words := route.path.split('/').filter(it != '') - // Route immediate matches first + // Route immediate matches & index files first // For example URL `/register` matches route `/:user`, but `fn register()` // should be called first. - if !route.path.contains('/:') && url_words == route_words { - // We found a match - if head.method == .post && method.args.len > 0 { - // TODO implement POST requests - // Populate method args with form values - // mut args := []string{cap: method.args.len} - // for param in method.args { - // args << form[param.name] - // } - // app.$method(args) - } else { - app.$method() + if (!route.path.contains('/:') && url_words == route_words) + || (url_words.len == 0 && route_words == ['index'] && method.name == 'index') { + // Check whether the request is authorised + if 'auth' in method.attrs && !app.is_authenticated() { + conn.write(http_401.bytes()) or {} + return } - return - } - if url_words.len == 0 && route_words == ['index'] && method.name == 'index' { + // We found a match app.$method() return - } + } else if params := route_matches(url_words, route_words) { + // Check whether the request is authorised + if 'auth' in method.attrs && !app.is_authenticated() { + conn.write(http_401.bytes()) or {} + return + } - if params := route_matches(url_words, route_words) { method_args := params.clone() if method_args.len != method.args.len { eprintln('warning: uneven parameters count ($method.args.len) in `$method.name`, compared to the web route `$method.attrs` ($method_args.len)') From 272f14b2645a56aedd608fd0880a768d43bfa75e Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sun, 4 Sep 2022 19:36:08 +0200 Subject: [PATCH 112/236] refactor(server): migrated all routes to new auth system --- src/server/api_logs.v | 24 ++++-------------------- src/server/api_targets.v | 30 +++++------------------------- src/server/auth.v | 12 ------------ src/server/repo.v | 6 +----- src/server/repo_remove.v | 18 +++--------------- src/web/web.v | 1 - 6 files changed, 13 insertions(+), 78 deletions(-) delete mode 100644 src/server/auth.v diff --git a/src/server/api_logs.v b/src/server/api_logs.v index 6728392c..021c1aca 100644 --- a/src/server/api_logs.v +++ b/src/server/api_logs.v @@ -12,12 +12,8 @@ import models { BuildLog, BuildLogFilter } // v1_get_logs returns all build logs in the database. A 'target' query param can // optionally be added to limit the list of build logs to that repository. -['/api/v1/logs'; get] +['/api/v1/logs'; auth; get] fn (mut app App) v1_get_logs() web.Result { - if !app.is_authorized() { - return app.json(http.Status.unauthorized, new_response('Unauthorized.')) - } - filter := models.from_params(app.query) or { return app.json(http.Status.bad_request, new_response('Invalid query parameters.')) } @@ -27,24 +23,16 @@ fn (mut app App) v1_get_logs() web.Result { } // v1_get_single_log returns the build log with the given id. -['/api/v1/logs/:id'; get] +['/api/v1/logs/:id'; auth; get] fn (mut app App) v1_get_single_log(id int) web.Result { - if !app.is_authorized() { - return app.json(http.Status.unauthorized, new_response('Unauthorized.')) - } - log := app.db.get_build_log(id) or { return app.not_found() } return app.json(http.Status.ok, new_data_response(log)) } // v1_get_log_content returns the actual build log file for the given id. -['/api/v1/logs/:id/content'; get] +['/api/v1/logs/:id/content'; auth; get] fn (mut app App) v1_get_log_content(id int) web.Result { - if !app.is_authorized() { - return app.json(http.Status.unauthorized, new_response('Unauthorized.')) - } - log := app.db.get_build_log(id) or { return app.not_found() } file_name := log.start_time.custom_format('YYYY-MM-DD_HH-mm-ss') full_path := os.join_path(app.conf.data_dir, logs_dir_name, log.target_id.str(), log.arch, @@ -63,12 +51,8 @@ fn parse_query_time(query string) ?time.Time { } // v1_post_log adds a new log to the database. -['/api/v1/logs'; post] +['/api/v1/logs'; auth; post] fn (mut app App) v1_post_log() web.Result { - if !app.is_authorized() { - return app.json(http.Status.unauthorized, new_response('Unauthorized.')) - } - // Parse query params start_time_int := app.query['startTime'].int() diff --git a/src/server/api_targets.v b/src/server/api_targets.v index 4cc3a584..c9e7963d 100644 --- a/src/server/api_targets.v +++ b/src/server/api_targets.v @@ -7,12 +7,8 @@ import db import models { Target, TargetArch, TargetFilter } // v1_get_targets returns the current list of targets. -['/api/v1/targets'; get] +['/api/v1/targets'; auth; get] fn (mut app App) v1_get_targets() web.Result { - if !app.is_authorized() { - return app.json(http.Status.unauthorized, new_response('Unauthorized.')) - } - filter := models.from_params(app.query) or { return app.json(http.Status.bad_request, new_response('Invalid query parameters.')) } @@ -22,24 +18,16 @@ fn (mut app App) v1_get_targets() web.Result { } // v1_get_single_target returns the information for a single target. -['/api/v1/targets/:id'; get] +['/api/v1/targets/:id'; auth; get] fn (mut app App) v1_get_single_target(id int) web.Result { - if !app.is_authorized() { - return app.json(http.Status.unauthorized, new_response('Unauthorized.')) - } - repo := app.db.get_target(id) or { return app.not_found() } return app.json(http.Status.ok, new_data_response(repo)) } // v1_post_target creates a new target from the provided query string. -['/api/v1/targets'; post] +['/api/v1/targets'; auth; post] fn (mut app App) v1_post_target() web.Result { - if !app.is_authorized() { - return app.json(http.Status.unauthorized, new_response('Unauthorized.')) - } - mut params := app.query.clone() // If a repo is created without specifying the arch, we assume it's meant @@ -63,24 +51,16 @@ fn (mut app App) v1_post_target() web.Result { } // v1_delete_target removes a given target from the server's list. -['/api/v1/targets/:id'; delete] +['/api/v1/targets/:id'; auth; delete] fn (mut app App) v1_delete_target(id int) web.Result { - if !app.is_authorized() { - return app.json(http.Status.unauthorized, new_response('Unauthorized.')) - } - app.db.delete_target(id) return app.json(http.Status.ok, new_response('Repo removed successfully.')) } // v1_patch_target updates a target's data with the given query params. -['/api/v1/targets/:id'; patch] +['/api/v1/targets/:id'; auth; patch] fn (mut app App) v1_patch_target(id int) web.Result { - if !app.is_authorized() { - return app.json(http.Status.unauthorized, new_response('Unauthorized.')) - } - app.db.update_target(id, app.query) if 'arch' in app.query { diff --git a/src/server/auth.v b/src/server/auth.v deleted file mode 100644 index 7c8a676f..00000000 --- a/src/server/auth.v +++ /dev/null @@ -1,12 +0,0 @@ -module server - -import net.http - -// is_authorized checks whether the provided API key is correct. -fn (mut app App) is_authorized() bool { - x_header := app.req.header.get_custom('X-Api-Key', http.HeaderQueryConfig{ exact: true }) or { - return false - } - - return x_header.trim_space() == app.conf.api_key -} diff --git a/src/server/repo.v b/src/server/repo.v index 242fd2df..5ed5d15c 100644 --- a/src/server/repo.v +++ b/src/server/repo.v @@ -49,12 +49,8 @@ fn (mut app App) get_repo_file(repo string, arch string, filename string) web.Re } // put_package handles publishing a package to a repository. -['/:repo/publish'; post] +['/:repo/publish'; auth; post] fn (mut app App) put_package(repo string) web.Result { - if !app.is_authorized() { - return app.json(http.Status.unauthorized, new_response('Unauthorized.')) - } - mut pkg_path := '' if length := app.req.header.get(.content_length) { diff --git a/src/server/repo_remove.v b/src/server/repo_remove.v index 316b3872..fdc40e87 100644 --- a/src/server/repo_remove.v +++ b/src/server/repo_remove.v @@ -5,12 +5,8 @@ import net.http import web.response { new_response } // delete_package tries to remove the given package. -['/:repo/:arch/:pkg'; delete] +['/:repo/:arch/:pkg'; auth; delete] fn (mut app App) delete_package(repo string, arch string, pkg string) web.Result { - if !app.is_authorized() { - return app.json(.unauthorized, new_response('Unauthorized.')) - } - res := app.repo.remove_pkg_from_arch_repo(repo, arch, pkg, true) or { app.lerror('Error while deleting package: $err.msg()') @@ -29,12 +25,8 @@ fn (mut app App) delete_package(repo string, arch string, pkg string) web.Result } // delete_arch_repo tries to remove the given arch-repo. -['/:repo/:arch'; delete] +['/:repo/:arch'; auth; delete] fn (mut app App) delete_arch_repo(repo string, arch string) web.Result { - if !app.is_authorized() { - return app.json(http.Status.unauthorized, new_response('Unauthorized.')) - } - res := app.repo.remove_arch_repo(repo, arch) or { app.lerror('Error while deleting arch-repo: $err.msg()') @@ -53,12 +45,8 @@ fn (mut app App) delete_arch_repo(repo string, arch string) web.Result { } // delete_repo tries to remove the given repo. -['/:repo'; delete] +['/:repo'; auth; delete] fn (mut app App) delete_repo(repo string) web.Result { - if !app.is_authorized() { - return app.json(http.Status.unauthorized, new_response('Unauthorized.')) - } - res := app.repo.remove_repo(repo) or { app.lerror('Error while deleting repo: $err.msg()') diff --git a/src/web/web.v b/src/web/web.v index 0847c7e6..1d1480fb 100644 --- a/src/web/web.v +++ b/src/web/web.v @@ -394,7 +394,6 @@ fn handle_conn(mut conn net.TcpConn, mut app T, routes map[string]Route) { api_key: app.api_key } - // Calling middleware... app.before_request() From 9dfdfbf72446f04b38eb6962bfd98600d380e7ca Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sun, 4 Sep 2022 20:16:58 +0200 Subject: [PATCH 113/236] chore: update README [CI SKIP] --- Makefile | 7 ------- README.md | 4 ++-- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index ed44df92..69bd7957 100644 --- a/Makefile +++ b/Makefile @@ -83,13 +83,6 @@ fmt: test: $(V) test $(SRC_DIR) -# Build & patch the V compiler -.PHONY: v -v: v/v -v/v: - git clone --single-branch https://git.rustybever.be/vieter-v/v v - make -C v - .PHONY: clean clean: rm -rf 'data' 'vieter' 'dvieter' 'pvieter' 'vieter.c' 'pkg' 'src/vieter' *.pkg.tar.zst 'suvieter' 'afvieter' '$(SRC_DIR)/_docs' 'docs/public' diff --git a/README.md b/README.md index 29ec0f01..b9fff692 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,6 @@ that. Besides a V installer, Vieter also requires the following libraries to work: -* gc * libarchive * openssl * sqlite3 @@ -49,7 +48,8 @@ update`. ### Compiler I used to maintain a mirror that tracked the latest master, but nowadays, I -solely target V 0.3 as a compiler. +maintain a Docker image containing the specific compiler version that Vieter +builds with. Currently, this is V 0.3. ## Contributing From 7b59277931172997759572ac5a00930d677a4d67 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Mon, 5 Sep 2022 10:10:02 +0200 Subject: [PATCH 114/236] feat: adding target returns id of added entry --- src/client/targets.v | 4 ++-- src/console/targets/targets.v | 2 +- src/db/targets.v | 6 +++++- src/server/api_targets.v | 6 +++--- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/client/targets.v b/src/client/targets.v index f5258a43..c5e44fea 100644 --- a/src/client/targets.v +++ b/src/client/targets.v @@ -49,9 +49,9 @@ pub struct NewTarget { } // add_target adds a new target to the server. -pub fn (c &Client) add_target(t NewTarget) ?Response { +pub fn (c &Client) add_target(t NewTarget) ?Response { params := models.params_from(t) - data := c.send_request(Method.post, '/api/v1/targets', params)? + data := c.send_request(Method.post, '/api/v1/targets', params)? return data } diff --git a/src/console/targets/targets.v b/src/console/targets/targets.v index 66d48fb3..a5559360 100644 --- a/src/console/targets/targets.v +++ b/src/console/targets/targets.v @@ -206,7 +206,7 @@ fn add(conf Config, t &NewTarget) ? { c := client.new(conf.address, conf.api_key) res := c.add_target(t)? - println(res.message) + println("Target added with id $res.data") } // remove removes a repository from the server's list. diff --git a/src/db/targets.v b/src/db/targets.v index 91020337..a705ebb5 100644 --- a/src/db/targets.v +++ b/src/db/targets.v @@ -38,10 +38,14 @@ pub fn (db &VieterDb) get_target(target_id int) ?Target { } // add_target inserts the given target into the database. -pub fn (db &VieterDb) add_target(repo Target) { +pub fn (db &VieterDb) add_target(repo Target) int { sql db.conn { insert repo into Target } + + inserted_id := db.conn.last_id() as int + + return inserted_id } // delete_target deletes the target with the given id from the database. diff --git a/src/server/api_targets.v b/src/server/api_targets.v index c9e7963d..5757d76b 100644 --- a/src/server/api_targets.v +++ b/src/server/api_targets.v @@ -2,7 +2,7 @@ module server import web import net.http -import web.response { new_data_response, new_response } +import web.response { new_data_response, new_response, new_full_response } import db import models { Target, TargetArch, TargetFilter } @@ -45,9 +45,9 @@ fn (mut app App) v1_post_target() web.Result { return app.json(http.Status.bad_request, new_response('Invalid kind.')) } - app.db.add_target(new_repo) + id := app.db.add_target(new_repo) - return app.json(http.Status.ok, new_response('Repo added successfully.')) + return app.json(http.Status.ok, new_data_response(id)) } // v1_delete_target removes a given target from the server's list. From 210508f1ee251b3cb61ab5bf146805f10b600c1c Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sun, 11 Sep 2022 20:34:02 +0200 Subject: [PATCH 115/236] feat: logs api now also returns id --- CHANGELOG.md | 1 + src/client/logs.v | 4 ++-- src/console/targets/targets.v | 2 +- src/db/logs.v | 6 +++++- src/server/api_logs.v | 9 +++++---- src/server/api_targets.v | 10 +++++----- 6 files changed, 19 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5aa0e434..00a3539c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 repository will be cloned with the default branch * Build containers now explicitely set the PATH variable * Refactor of web framework +* API endpoints now return id of newly created entries ### Removed diff --git a/src/client/logs.v b/src/client/logs.v index b52c3d09..b4142450 100644 --- a/src/client/logs.v +++ b/src/client/logs.v @@ -39,7 +39,7 @@ pub fn (c &Client) get_build_log_content(id int) ?string { } // add_build_log adds a new build log to the server. -pub fn (c &Client) add_build_log(target_id int, start_time time.Time, end_time time.Time, arch string, exit_code int, content string) ?Response { +pub fn (c &Client) add_build_log(target_id int, start_time time.Time, end_time time.Time, arch string, exit_code int, content string) ?Response { params := { 'target': target_id.str() 'startTime': start_time.unix_time().str() @@ -48,7 +48,7 @@ pub fn (c &Client) add_build_log(target_id int, start_time time.Time, end_time t 'exitCode': exit_code.str() } - data := c.send_request_with_body(Method.post, '/api/v1/logs', params, content)? + data := c.send_request_with_body(Method.post, '/api/v1/logs', params, content)? return data } diff --git a/src/console/targets/targets.v b/src/console/targets/targets.v index a5559360..2784dbc3 100644 --- a/src/console/targets/targets.v +++ b/src/console/targets/targets.v @@ -206,7 +206,7 @@ fn add(conf Config, t &NewTarget) ? { c := client.new(conf.address, conf.api_key) res := c.add_target(t)? - println("Target added with id $res.data") + println('Target added with id $res.data') } // remove removes a repository from the server's list. diff --git a/src/db/logs.v b/src/db/logs.v index af5f53c3..923dde2d 100644 --- a/src/db/logs.v +++ b/src/db/logs.v @@ -79,10 +79,14 @@ pub fn (db &VieterDb) get_build_log(id int) ?BuildLog { } // add_build_log inserts the given BuildLog into the database. -pub fn (db &VieterDb) add_build_log(log BuildLog) { +pub fn (db &VieterDb) add_build_log(log BuildLog) int { sql db.conn { insert log into BuildLog } + + inserted_id := db.conn.last_id() as int + + return inserted_id } // delete_build_log delete the BuildLog with the given ID from the database. diff --git a/src/server/api_logs.v b/src/server/api_logs.v index 021c1aca..287755a9 100644 --- a/src/server/api_logs.v +++ b/src/server/api_logs.v @@ -19,7 +19,7 @@ fn (mut app App) v1_get_logs() web.Result { } logs := app.db.get_build_logs(filter) - return app.json(http.Status.ok, new_data_response(logs)) + return app.json(.ok, new_data_response(logs)) } // v1_get_single_log returns the build log with the given id. @@ -27,7 +27,7 @@ fn (mut app App) v1_get_logs() web.Result { fn (mut app App) v1_get_single_log(id int) web.Result { log := app.db.get_build_log(id) or { return app.not_found() } - return app.json(http.Status.ok, new_data_response(log)) + return app.json(.ok, new_data_response(log)) } // v1_get_log_content returns the actual build log file for the given id. @@ -95,7 +95,8 @@ fn (mut app App) v1_post_log() web.Result { exit_code: exit_code } - app.db.add_build_log(log) + // id of newly created log + log_id := app.db.add_build_log(log) repo_logs_dir := os.join_path(app.conf.data_dir, logs_dir_name, target_id.str(), arch) @@ -122,5 +123,5 @@ fn (mut app App) v1_post_log() web.Result { return app.status(http.Status.length_required) } - return app.json(http.Status.ok, new_response('Logs added successfully.')) + return app.json(.ok, new_data_response(log_id)) } diff --git a/src/server/api_targets.v b/src/server/api_targets.v index 5757d76b..6f284af6 100644 --- a/src/server/api_targets.v +++ b/src/server/api_targets.v @@ -2,7 +2,7 @@ module server import web import net.http -import web.response { new_data_response, new_response, new_full_response } +import web.response { new_data_response, new_response } import db import models { Target, TargetArch, TargetFilter } @@ -14,7 +14,7 @@ fn (mut app App) v1_get_targets() web.Result { } repos := app.db.get_targets(filter) - return app.json(http.Status.ok, new_data_response(repos)) + return app.json(.ok, new_data_response(repos)) } // v1_get_single_target returns the information for a single target. @@ -22,7 +22,7 @@ fn (mut app App) v1_get_targets() web.Result { fn (mut app App) v1_get_single_target(id int) web.Result { repo := app.db.get_target(id) or { return app.not_found() } - return app.json(http.Status.ok, new_data_response(repo)) + return app.json(.ok, new_data_response(repo)) } // v1_post_target creates a new target from the provided query string. @@ -55,7 +55,7 @@ fn (mut app App) v1_post_target() web.Result { fn (mut app App) v1_delete_target(id int) web.Result { app.db.delete_target(id) - return app.json(http.Status.ok, new_response('Repo removed successfully.')) + return app.status(.ok) } // v1_patch_target updates a target's data with the given query params. @@ -69,5 +69,5 @@ fn (mut app App) v1_patch_target(id int) web.Result { app.db.update_target_archs(id, arch_objs) } - return app.json(http.Status.ok, new_response('Repo updated successfully.')) + return app.status(.ok) } From b6cd2f0bc280dfddfa85a91188ebe05433eabbf8 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sun, 11 Sep 2022 21:24:29 +0200 Subject: [PATCH 116/236] feat(server): repo POST requests now return information --- CHANGELOG.md | 1 + src/repo/add.v | 43 +++++++++++++++++++------------------------ src/server/repo.v | 21 ++++++--------------- 3 files changed, 26 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00a3539c..c875f2e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Build containers now explicitely set the PATH variable * Refactor of web framework * API endpoints now return id of newly created entries +* Repo POST requests now return information on published package ### Removed diff --git a/src/repo/add.v b/src/repo/add.v index 0985c7a6..608ca501 100644 --- a/src/repo/add.v +++ b/src/repo/add.v @@ -23,8 +23,9 @@ pub: pub struct RepoAddResult { pub: - added bool [required] - pkg &package.Pkg [required] + name string + version string + archs []string } // new creates a new RepoGroupManager & creates the directories as needed @@ -53,10 +54,10 @@ pub fn (r &RepoGroupManager) add_pkg_from_path(repo string, pkg_path string) ?Re return error('Failed to read package file: $err.msg()') } - added := r.add_pkg_in_repo(repo, pkg)? + archs := r.add_pkg_in_repo(repo, pkg)? // If the add was successful, we move the file to the packages directory - for arch in added { + for arch in archs { repo_pkg_path := os.real_path(os.join_path(r.pkg_dir, repo, arch)) dest_path := os.join_path_single(repo_pkg_path, pkg.filename()) @@ -71,8 +72,9 @@ pub fn (r &RepoGroupManager) add_pkg_from_path(repo string, pkg_path string) ?Re os.rm(pkg_path)? return RepoAddResult{ - added: added.len > 0 - pkg: &pkg + name: pkg.info.name + version: pkg.info.version + archs: archs } } @@ -87,11 +89,9 @@ fn (r &RepoGroupManager) add_pkg_in_repo(repo string, pkg &package.Pkg) ?[]strin // A package not of arch 'any' can be handled easily by adding it to the // respective repo if pkg.info.arch != 'any' { - if r.add_pkg_in_arch_repo(repo, pkg.info.arch, pkg)? { - return [pkg.info.arch] - } else { - return [] - } + r.add_pkg_in_arch_repo(repo, pkg.info.arch, pkg)? + + return [pkg.info.arch] } mut arch_repos := []string{} @@ -113,25 +113,22 @@ fn (r &RepoGroupManager) add_pkg_in_repo(repo string, pkg &package.Pkg) ?[]strin arch_repos << r.default_arch } - mut added := []string{} - - // We add the package to each repository. If any of the repositories - // return true, the result of the function is also true. + // Add the package to each found architecture + // NOTE: if any of these fail, the function fails. This means the user does + // not know which arch-repositories did succeed in adding the package, if + // any. for arch in arch_repos { - if r.add_pkg_in_arch_repo(repo, arch, pkg)? { - added << arch - } + r.add_pkg_in_arch_repo(repo, arch, pkg)? } - return added + return arch_repos } // add_pkg_in_arch_repo is the function that actually adds a package to a given // arch-repo. It records the package's data in the arch-repo's desc & files // files, and afterwards updates the db & files archives to reflect these -// changes. The function returns false if the package was already present in -// the repo, and true otherwise. -fn (r &RepoGroupManager) add_pkg_in_arch_repo(repo string, arch string, pkg &package.Pkg) ?bool { +// changes. +fn (r &RepoGroupManager) add_pkg_in_arch_repo(repo string, arch string, pkg &package.Pkg) ? { pkg_dir := os.join_path(r.repos_dir, repo, arch, '$pkg.info.name-$pkg.info.version') // Remove the previous version of the package, if present @@ -151,6 +148,4 @@ fn (r &RepoGroupManager) add_pkg_in_arch_repo(repo string, arch string, pkg &pac } r.sync(repo, arch)? - - return true } diff --git a/src/server/repo.v b/src/server/repo.v index 5ed5d15c..526d4e76 100644 --- a/src/server/repo.v +++ b/src/server/repo.v @@ -6,8 +6,7 @@ import repo import time import rand import util -import net.http -import web.response { new_response } +import web.response { new_data_response, new_response } // healthcheck just returns a string, but can be used to quickly check if the // server is still responsive. @@ -65,7 +64,7 @@ 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.status(.internal_server_error) } sw.stop() @@ -74,7 +73,7 @@ fn (mut app App) put_package(repo string) web.Result { app.lwarn('Tried to upload package without specifying a Content-Length.') // length required - return app.status(http.Status.length_required) + return app.status(.length_required) } res := app.repo.add_pkg_from_path(repo, pkg_path) or { @@ -82,18 +81,10 @@ 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.status(.internal_server_error) } - if !res.added { - os.rm(pkg_path) or { app.lerror("Failed to remove download '$pkg_path': $err.msg()") } + app.linfo("Added '$res.name-$res.version' to '$repo (${res.archs.join(',')})'.") - app.lwarn("Duplicate package '$res.pkg.full_name()' in repo '$repo'.") - - return app.json(http.Status.bad_request, new_response('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.json(.ok, new_data_response(res)) } From cf67b46df0cbd69444f2e6046937c74583021d84 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sun, 11 Sep 2022 21:28:37 +0200 Subject: [PATCH 117/236] feat(server): less verbose repo DELETE responses --- src/server/repo_remove.v | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/server/repo_remove.v b/src/server/repo_remove.v index fdc40e87..694f0854 100644 --- a/src/server/repo_remove.v +++ b/src/server/repo_remove.v @@ -1,8 +1,6 @@ module server import web -import net.http -import web.response { new_response } // delete_package tries to remove the given package. ['/:repo/:arch/:pkg'; auth; delete] @@ -10,17 +8,17 @@ fn (mut app App) delete_package(repo string, arch string, pkg string) web.Result res := app.repo.remove_pkg_from_arch_repo(repo, arch, pkg, true) or { app.lerror('Error while deleting package: $err.msg()') - return app.json(http.Status.internal_server_error, new_response('Failed to delete package.')) + return app.status(.internal_server_error) } if res { app.linfo("Removed package '$pkg' from '$repo/$arch'") - return app.json(http.Status.ok, new_response('Package removed.')) + return app.status(.ok) } else { app.linfo("Tried removing package '$pkg' from '$repo/$arch', but it doesn't exist.") - return app.json(http.Status.not_found, new_response('Package not found.')) + return app.status(.not_found) } } @@ -30,17 +28,17 @@ fn (mut app App) delete_arch_repo(repo string, arch string) web.Result { res := app.repo.remove_arch_repo(repo, arch) or { app.lerror('Error while deleting arch-repo: $err.msg()') - return app.json(http.Status.internal_server_error, new_response('Failed to delete arch-repo.')) + return app.status(.internal_server_error) } if res { - app.linfo("Removed '$repo/$arch'") + app.linfo("Removed arch-repo '$repo/$arch'") - return app.json(http.Status.ok, new_response('Arch-repo removed.')) + return app.status(.ok) } else { app.linfo("Tried removing '$repo/$arch', but it doesn't exist.") - return app.json(http.Status.not_found, new_response('Arch-repo not found.')) + return app.status(.not_found) } } @@ -50,16 +48,16 @@ fn (mut app App) delete_repo(repo string) web.Result { res := app.repo.remove_repo(repo) or { app.lerror('Error while deleting repo: $err.msg()') - return app.json(http.Status.internal_server_error, new_response('Failed to delete repo.')) + return app.status(.internal_server_error) } if res { - app.linfo("Removed '$repo'") + app.linfo("Removed repo '$repo'") - return app.json(http.Status.ok, new_response('Repo removed.')) + return app.status(.ok) } else { app.linfo("Tried removing '$repo', but it doesn't exist.") - return app.json(http.Status.not_found, new_response('Repo not found.')) + return app.status(.not_found) } } From 8a08788935bb9514007f41d17d18cb2d1b879ecf Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sun, 11 Sep 2022 21:50:29 +0200 Subject: [PATCH 118/236] feat(console): tabled outputs now optionally return without decorations --- src/console/console.v | 5 +++++ src/console/logs/logs.v | 20 +++++++++++++------- src/console/targets/targets.v | 12 +++++++++--- src/main.v | 7 +++++++ 4 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/console/console.v b/src/console/console.v index 7d782bac..caf4cca7 100644 --- a/src/console/console.v +++ b/src/console/console.v @@ -5,6 +5,11 @@ import strings import cli import os +// tabbed_table returns a simple textual table, with tabs as separators. +pub fn tabbed_table(data [][]string) string { + return data.map(it.join('\t')).join('\n') +} + // pretty_table converts a list of string data into a pretty table. Many thanks // to @hungrybluedev in the Vlang Discord for providing this code! // https://ptb.discord.com/channels/592103645835821068/592106336838352923/970278787143045192 diff --git a/src/console/logs/logs.v b/src/console/logs/logs.v index 0f023bc7..41830c25 100644 --- a/src/console/logs/logs.v +++ b/src/console/logs/logs.v @@ -133,7 +133,9 @@ pub fn cmd() cli.Command { ] } - list(conf, filter)? + raw := cmd.flags.get_bool('raw')? + + list(conf, filter, raw)? } }, cli.Command{ @@ -167,27 +169,31 @@ pub fn cmd() cli.Command { } // print_log_list prints a list of logs. -fn print_log_list(logs []BuildLog) ? { +fn print_log_list(logs []BuildLog, raw bool) ? { data := logs.map([it.id.str(), it.target_id.str(), it.start_time.local().str(), it.exit_code.str()]) - println(console.pretty_table(['id', 'target', 'start time', 'exit code'], data)?) + if raw { + println(console.tabbed_table(data)) + } else { + println(console.pretty_table(['id', 'target', 'start time', 'exit code'], data)?) + } } // list prints a list of all build logs. -fn list(conf Config, filter BuildLogFilter) ? { +fn list(conf Config, filter BuildLogFilter, raw bool) ? { c := client.new(conf.address, conf.api_key) logs := c.get_build_logs(filter)?.data - print_log_list(logs)? + print_log_list(logs, raw)? } // list prints a list of all build logs for a given target. -fn list_for_target(conf Config, target_id int) ? { +fn list_for_target(conf Config, target_id int, raw bool) ? { c := client.new(conf.address, conf.api_key) logs := c.get_build_logs_for_target(target_id)?.data - print_log_list(logs)? + print_log_list(logs, raw)? } // info print the detailed info for a given build log. diff --git a/src/console/targets/targets.v b/src/console/targets/targets.v index 2784dbc3..198e0626 100644 --- a/src/console/targets/targets.v +++ b/src/console/targets/targets.v @@ -60,7 +60,9 @@ pub fn cmd() cli.Command { filter.repo = repo } - list(conf, filter)? + raw := cmd.flags.get_bool('raw')? + + list(conf, filter, raw)? } }, cli.Command{ @@ -193,12 +195,16 @@ pub fn cmd() cli.Command { // ID. If multiple or none are found, an error is raised. // list prints out a list of all repositories. -fn list(conf Config, filter TargetFilter) ? { +fn list(conf Config, filter TargetFilter, raw bool) ? { c := client.new(conf.address, conf.api_key) repos := c.get_targets(filter)? data := repos.map([it.id.str(), it.kind, it.url, it.repo]) - println(console.pretty_table(['id', 'kind', 'url', 'repo'], data)?) + if raw { + println(console.tabbed_table(data)) + } else { + println(console.pretty_table(['id', 'kind', 'url', 'repo'], data)?) + } } // add adds a new repository to the server's list. diff --git a/src/main.v b/src/main.v index 4ade9305..0e98bd26 100644 --- a/src/main.v +++ b/src/main.v @@ -24,6 +24,13 @@ fn main() { global: true default_value: [os.expand_tilde_to_home('~/.vieterrc')] }, + cli.Flag{ + flag: cli.FlagType.bool + name: 'raw' + abbrev: 'r' + description: 'Only output minimal information (no formatted tables, etc.)' + global: true + }, ] commands: [ server.cmd(), From fab8ca20b82e7f088d95efdc945a600050fefd6b Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sat, 1 Oct 2022 16:02:43 +0200 Subject: [PATCH 119/236] cli: targets add now supports raw flag --- src/console/targets/targets.v | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/console/targets/targets.v b/src/console/targets/targets.v index 198e0626..56400117 100644 --- a/src/console/targets/targets.v +++ b/src/console/targets/targets.v @@ -94,7 +94,9 @@ pub fn cmd() cli.Command { branch: cmd.flags.get_string('branch') or { '' } } - add(conf, t)? + raw := cmd.flags.get_bool('raw')? + + add(conf, t, raw)? } }, cli.Command{ @@ -208,11 +210,15 @@ fn list(conf Config, filter TargetFilter, raw bool) ? { } // add adds a new repository to the server's list. -fn add(conf Config, t &NewTarget) ? { +fn add(conf Config, t &NewTarget, raw bool) ? { c := client.new(conf.address, conf.api_key) res := c.add_target(t)? - println('Target added with id $res.data') + if raw { + println(res.data) + } else { + println('Target added with id $res.data') + } } // remove removes a repository from the server's list. From 851a446a954e619e474251533960b00a4b64bef3 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sat, 1 Oct 2022 16:21:49 +0200 Subject: [PATCH 120/236] chore: updated changelog [CI SKIP] --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c875f2e7..60b70b60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * CLI commands for searching the AUR & directly adding packages * HTTP routes for removing packages, arch-repos & repos * All endpoints serving files now support HTTP byte range requests +* Better CLI UX + * When adding targets, the ID of the created target is returned + * The `-r` flag only shows raw data of action + * When adding a target, only ID is shown and not surrounding text + * Tabled output returns a tab-separated list (easy to script using + `cut`) ### Changed From 575c04189da2088261b6c3d5921ded948312e568 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sun, 11 Sep 2022 22:20:02 +0200 Subject: [PATCH 121/236] fix(client): allow empty values as params --- src/client/client.v | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/client/client.v b/src/client/client.v index 24e44448..d68ff18f 100644 --- a/src/client/client.v +++ b/src/client/client.v @@ -30,12 +30,10 @@ fn (c &Client) send_request_raw(method Method, url string, params map[string]str // Escape each query param for k, v in params { // An empty parameter should be the same as not providing it at all - if v != '' { - params_escaped[k] = urllib.query_escape(v) - } + params_escaped[k] = urllib.query_escape(v) } - params_str := params_escaped.keys().map('$it=${params[it]}').join('&') + params_str := params_escaped.keys().map('$it=${params_escaped[it]}').join('&') full_url = '$full_url?$params_str' } From 95d32e2d515edf27a919e847fef5f5459f3f7234 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sun, 11 Sep 2022 22:24:29 +0200 Subject: [PATCH 122/236] fix(server): prevent `api` as a repository name --- CHANGELOG.md | 2 ++ src/server/repo.v | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60b70b60..79ceed49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Refactor of web framework * API endpoints now return id of newly created entries * Repo POST requests now return information on published package +* `api` can no longer be used as a repository name +* CLI client now allows setting values to an empty value ### Removed diff --git a/src/server/repo.v b/src/server/repo.v index 526d4e76..06ab72e6 100644 --- a/src/server/repo.v +++ b/src/server/repo.v @@ -50,6 +50,12 @@ fn (mut app App) get_repo_file(repo string, arch string, filename string) web.Re // put_package handles publishing a package to a repository. ['/:repo/publish'; auth; post] fn (mut app App) put_package(repo string) web.Result { + // api is a reserved keyword for api routes & should never be allowed to be + // a repository. + if repo.to_lower() == 'api' { + return app.json(.bad_request, new_response("'api' is a reserved keyword & cannot be used as a repository name.")) + } + mut pkg_path := '' if length := app.req.header.get(.content_length) { From 847d77b2bc6e1547da489048b196dd2cb641c4f5 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Thu, 15 Sep 2022 13:42:49 +0200 Subject: [PATCH 123/236] chore(ci): refactor ci configs a bit --- .woodpecker/{.arch-rel.yml => arch-rel.yml} | 0 .woodpecker/{.arch.yml => arch.yml} | 0 .woodpecker/{.build.yml => build.yml} | 11 +++++++---- .woodpecker/{.deploy.yml => deploy.yml} | 0 .woodpecker/{.docker.yml => docker.yml} | 0 .woodpecker/{.docs.yml => docs.yml} | 5 ++++- .woodpecker/{.gitea.yml => gitea.yml} | 5 ++++- .woodpecker/{.lint.yml => lint.yml} | 5 ++++- .woodpecker/{.man.yml => man.yml} | 5 ++++- .woodpecker/{.test.yml => test.yml} | 7 +++++-- 10 files changed, 28 insertions(+), 10 deletions(-) rename .woodpecker/{.arch-rel.yml => arch-rel.yml} (100%) rename .woodpecker/{.arch.yml => arch.yml} (100%) rename .woodpecker/{.build.yml => build.yml} (92%) rename .woodpecker/{.deploy.yml => deploy.yml} (100%) rename .woodpecker/{.docker.yml => docker.yml} (100%) rename .woodpecker/{.docs.yml => docs.yml} (93%) rename .woodpecker/{.gitea.yml => gitea.yml} (89%) rename .woodpecker/{.lint.yml => lint.yml} (73%) rename .woodpecker/{.man.yml => man.yml} (92%) rename .woodpecker/{.test.yml => test.yml} (80%) diff --git a/.woodpecker/.arch-rel.yml b/.woodpecker/arch-rel.yml similarity index 100% rename from .woodpecker/.arch-rel.yml rename to .woodpecker/arch-rel.yml diff --git a/.woodpecker/.arch.yml b/.woodpecker/arch.yml similarity index 100% rename from .woodpecker/.arch.yml rename to .woodpecker/arch.yml diff --git a/.woodpecker/.build.yml b/.woodpecker/build.yml similarity index 92% rename from .woodpecker/.build.yml rename to .woodpecker/build.yml index 580fa69e..9ee80853 100644 --- a/.woodpecker/.build.yml +++ b/.woodpecker/build.yml @@ -1,3 +1,6 @@ +variables: + - &vlang_image 'chewingbever/vlang:0.3' + matrix: PLATFORM: - 'linux/amd64' @@ -7,7 +10,7 @@ platform: ${PLATFORM} pipeline: install-modules: - image: 'chewingbever/vlang:0.3' + image: *vlang_image pull: true commands: - export VMODULES=$PWD/.vmodules @@ -16,7 +19,7 @@ pipeline: event: [push, pull_request] debug: - image: 'chewingbever/vlang:0.3' + image: *vlang_image commands: - export VMODULES=$PWD/.vmodules - make @@ -26,7 +29,7 @@ pipeline: exclude: [main] prod: - image: 'chewingbever/vlang:0.3' + image: *vlang_image environment: - LDFLAGS=-lz -lbz2 -llzma -lexpat -lzstd -llz4 -lsqlite3 -static commands: @@ -44,7 +47,7 @@ pipeline: event: [push, pull_request] upload: - image: 'chewingbever/vlang:0.3' + image: *vlang_image secrets: [ s3_username, s3_password ] commands: # https://gist.github.com/JustinTimperio/7c7115f87b775618637d67ac911e595f diff --git a/.woodpecker/.deploy.yml b/.woodpecker/deploy.yml similarity index 100% rename from .woodpecker/.deploy.yml rename to .woodpecker/deploy.yml diff --git a/.woodpecker/.docker.yml b/.woodpecker/docker.yml similarity index 100% rename from .woodpecker/.docker.yml rename to .woodpecker/docker.yml diff --git a/.woodpecker/.docs.yml b/.woodpecker/docs.yml similarity index 93% rename from .woodpecker/.docs.yml rename to .woodpecker/docs.yml index da495fcb..048b1ad9 100644 --- a/.woodpecker/.docs.yml +++ b/.woodpecker/docs.yml @@ -1,3 +1,6 @@ +variables: + - &vlang_image 'chewingbever/vlang:0.3' + platform: 'linux/amd64' branches: exclude: [ main ] @@ -11,7 +14,7 @@ pipeline: - make docs api-docs: - image: 'chewingbever/vlang:0.3' + image: *vlang_image pull: true group: 'generate' commands: diff --git a/.woodpecker/.gitea.yml b/.woodpecker/gitea.yml similarity index 89% rename from .woodpecker/.gitea.yml rename to .woodpecker/gitea.yml index 55f991e2..8e3b9d44 100644 --- a/.woodpecker/.gitea.yml +++ b/.woodpecker/gitea.yml @@ -1,3 +1,6 @@ +variables: + - &vlang_image 'chewingbever/vlang:0.3' + platform: 'linux/amd64' branches: [ 'main' ] depends_on: @@ -8,7 +11,7 @@ skip_clone: true pipeline: prepare: - image: 'chewingbever/vlang:0.3' + image: *vlang_image pull: true secrets: [ s3_username, s3_password ] commands: diff --git a/.woodpecker/.lint.yml b/.woodpecker/lint.yml similarity index 73% rename from .woodpecker/.lint.yml rename to .woodpecker/lint.yml index 75a81059..c80ce338 100644 --- a/.woodpecker/.lint.yml +++ b/.woodpecker/lint.yml @@ -1,3 +1,6 @@ +variables: + - &vlang_image 'chewingbever/vlang:0.3' + # These checks already get performed on the feature branches branches: exclude: [ main ] @@ -5,7 +8,7 @@ platform: 'linux/amd64' pipeline: lint: - image: 'chewingbever/vlang:0.3' + image: *vlang_image pull: true commands: - make lint diff --git a/.woodpecker/.man.yml b/.woodpecker/man.yml similarity index 92% rename from .woodpecker/.man.yml rename to .woodpecker/man.yml index 1a30b03d..86a1bd8c 100644 --- a/.woodpecker/.man.yml +++ b/.woodpecker/man.yml @@ -1,3 +1,6 @@ +variables: + - &vlang_image 'chewingbever/vlang:0.3' + platform: 'linux/amd64' branches: exclude: [ main ] @@ -9,7 +12,7 @@ skip_clone: true pipeline: generate: - image: 'chewingbever/vlang:0.3' + image: *vlang_image pull: true commands: - curl -o vieter -L "https://s3.rustybever.be/vieter/commits/$CI_COMMIT_SHA/vieter-linux-amd64" diff --git a/.woodpecker/.test.yml b/.woodpecker/test.yml similarity index 80% rename from .woodpecker/.test.yml rename to .woodpecker/test.yml index 6c267fab..08b7534f 100644 --- a/.woodpecker/.test.yml +++ b/.woodpecker/test.yml @@ -1,3 +1,6 @@ +variables: + - &vlang_image 'chewingbever/vlang:0.3' + matrix: PLATFORM: - 'linux/amd64' @@ -9,7 +12,7 @@ platform: ${PLATFORM} pipeline: install-modules: - image: 'chewingbever/vlang:0.3' + image: *vlang_image pull: true commands: - export VMODULES=$PWD/.vmodules @@ -18,7 +21,7 @@ pipeline: event: [pull_request] test: - image: 'chewingbever/vlang:0.3' + image: *vlang_image pull: true commands: - export VMODULES=$PWD/.vmodules From 559ef3e505495c9b87b1f6dfb781c7230a7caa1a Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sun, 11 Sep 2022 20:34:02 +0200 Subject: [PATCH 124/236] feat: logs api now also returns id --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79ceed49..b4efd58b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Repo POST requests now return information on published package * `api` can no longer be used as a repository name * CLI client now allows setting values to an empty value +* API endpoints now return id of newly created entries ### Removed From ae98c3e717431952eb248cac3fdc99b85e946d7a Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sun, 11 Sep 2022 21:24:29 +0200 Subject: [PATCH 125/236] feat(server): repo POST requests now return information --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4efd58b..16ed2a7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `api` can no longer be used as a repository name * CLI client now allows setting values to an empty value * API endpoints now return id of newly created entries +* Repo POST requests now return information on published package ### Removed From f34eefd59bfcfdc996f12779cd3101d740037d24 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sat, 1 Oct 2022 16:35:43 +0200 Subject: [PATCH 126/236] fix(server): prevent `api` as a repository name --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16ed2a7f..113dc21d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * CLI client now allows setting values to an empty value * API endpoints now return id of newly created entries * Repo POST requests now return information on published package +* `api` can no longer be used as a repository name +* CLI client now allows setting values to an empty value ### Removed From 15c2d7274310de6fa77c99933ba6891493ec3e01 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sat, 1 Oct 2022 16:53:16 +0200 Subject: [PATCH 127/236] docs: updated json response types for create routes [CI SKIP] --- docs/api/source/includes/_logs.md | 11 +++++++++++ docs/api/source/includes/_targets.md | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/docs/api/source/includes/_logs.md b/docs/api/source/includes/_logs.md index 2797e60d..1c14e71a 100644 --- a/docs/api/source/includes/_logs.md +++ b/docs/api/source/includes/_logs.md @@ -112,6 +112,17 @@ id | ID of requested log ## Publish build log +> JSON output format + +```json +{ + "message": "", + "data": { + "id": 15 + } +} +``` +