├── .gitignore ├── clippy.toml ├── systemd └── stfed.service ├── release ├── .pre-commit-config.yaml ├── .github └── workflows │ └── ci.yml ├── cliff.toml ├── Cargo.toml ├── src ├── hook.rs ├── config.rs ├── main.rs ├── syncthing.rs └── syncthing_rest.rs ├── README.md ├── CHANGELOG.md ├── Cargo.lock └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | allow-expect-in-tests = true 2 | allow-panic-in-tests = true 3 | allow-unwrap-in-tests = true 4 | avoid-breaking-exported-api = false 5 | -------------------------------------------------------------------------------- /systemd/stfed.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Syncthing Folder Event Daemon 3 | PartOf=syncthing.service 4 | After=syncthing.service 5 | 6 | [Service] 7 | ExecStart=/usr/local/bin/stfed 8 | Restart=on-failure 9 | RestartSec=5 10 | 11 | # Hardening 12 | SystemCallArchitectures=native 13 | MemoryDenyWriteExecute=true 14 | NoNewPrivileges=true 15 | 16 | [Install] 17 | WantedBy=default.target 18 | -------------------------------------------------------------------------------- /release: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | set -o pipefail 4 | 5 | readonly VERSION="${1:?}" 6 | 7 | cd "$(git rev-parse --show-toplevel)" 8 | 9 | cargo set-version "${VERSION}" 10 | 11 | cargo upgrade 12 | cargo update 13 | 14 | cargo check 15 | cargo test 16 | 17 | git add Cargo.{toml,lock} 18 | 19 | git commit -m "chore: version ${VERSION}" 20 | git tag -m "Version ${VERSION}" "${VERSION}" 21 | 22 | git cliff 1.0.0..HEAD > CHANGELOG.md 23 | git add CHANGELOG.md 24 | git commit --amend --no-edit 25 | 26 | git tag -f -m "Version ${VERSION}" "${VERSION}" 27 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # https://pre-commit.com 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v5.0.0 5 | hooks: 6 | - id: check-added-large-files 7 | - id: check-case-conflict 8 | - id: check-executables-have-shebangs 9 | - id: check-json 10 | - id: check-merge-conflict 11 | - id: check-symlinks 12 | - id: check-toml 13 | - id: check-vcs-permalinks 14 | - id: check-xml 15 | - id: check-yaml 16 | - id: end-of-file-fixer 17 | - id: fix-byte-order-marker 18 | - id: mixed-line-ending 19 | args: 20 | - --fix=no 21 | 22 | - repo: https://github.com/doublify/pre-commit-rust 23 | rev: v1.0 24 | hooks: 25 | - id: cargo-check 26 | - id: clippy 27 | - id: fmt 28 | 29 | - repo: https://github.com/shellcheck-py/shellcheck-py 30 | rev: v0.10.0.1 31 | hooks: 32 | - id: shellcheck 33 | 34 | - repo: https://github.com/hukkin/mdformat 35 | rev: 0.7.21 36 | hooks: 37 | - id: mdformat 38 | 39 | - repo: https://github.com/compilerla/conventional-pre-commit 40 | rev: v4.0.0 41 | hooks: 42 | - id: conventional-pre-commit 43 | stages: [commit-msg] 44 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | CARGO_TERM_COLOR: always 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions-rs/toolchain@v1 14 | with: 15 | profile: minimal 16 | toolchain: stable 17 | - run: cargo build --verbose 18 | 19 | test: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: actions-rs/toolchain@v1 24 | with: 25 | profile: minimal 26 | toolchain: stable 27 | - run: cargo test --verbose 28 | 29 | clippy: 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v4 33 | - uses: actions-rs/toolchain@v1 34 | with: 35 | profile: minimal 36 | toolchain: stable 37 | components: clippy 38 | - run: cargo clippy -- -D warnings 39 | 40 | fmt: 41 | runs-on: ubuntu-latest 42 | steps: 43 | - uses: actions/checkout@v4 44 | - uses: actions-rs/toolchain@v1 45 | with: 46 | profile: minimal 47 | toolchain: stable 48 | components: rustfmt 49 | - run: cargo fmt --all -- --check 50 | -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | # https://git-cliff.org/docs/configuration 2 | 3 | [changelog] 4 | # template for the changelog header 5 | header = "# Changelog" 6 | # template for the changelog body 7 | # https://keats.github.io/tera/docs/#introduction 8 | body = """ 9 | 10 | {% if version %}\ 11 | ## {{ version }} - {{ timestamp | date(format="%Y-%m-%d") }} 12 | {% else %}\ 13 | ## Unreleased 14 | {% endif %}\ 15 | {% for group, commits in commits | group_by(attribute="group") %} 16 | ### {{ group | upper_first }} 17 | {% for commit in commits %} 18 | - {% if commit.scope %}{{ commit.scope }}: {% endif %}\ 19 | {{ commit.message | split(pat="\n") | first | upper_first | trim }} \ 20 | ([{{ commit.id | truncate(length=7, end="") }}](https://github.com/desbma/stfed/commit/{{ commit.id }}) by {{ commit.author.name }})\ 21 | {% endfor %} 22 | {% endfor %} 23 | ______________________________________________________________________ 24 | 25 | """ 26 | # template for the changelog footer 27 | footer = "" 28 | # remove the leading and trailing whitespace from the templates 29 | trim = true 30 | 31 | [git] 32 | # parse the commits based on https://www.conventionalcommits.org 33 | conventional_commits = true 34 | # filter out the commits that are not conventional 35 | filter_unconventional = false 36 | # regex for parsing and grouping commits 37 | commit_parsers = [ 38 | { message = "^feat", group = "💡 Features" }, 39 | { message = "^fix", group = "🐛 Bug fixes" }, 40 | { message = "^perf", group = "🏃 Performance" }, 41 | { message = "^doc", group = "📗 Documentation" }, 42 | { message = "^test", group = "🧪 Testing" }, 43 | { message = "^refactor", group = "🚜 Refactor" }, 44 | { message = "^style", group = "🎨 Styling" }, 45 | { message = "^build", group = "🏗 Build" }, 46 | { message = "^ci", group = "🤖 Continuous integration" }, 47 | { message = "^chore: version ", skip = true }, 48 | { message = "^chore", group = "🧰 Miscellaneous tasks" }, 49 | { message = "^revert", group = "🧰 Miscellaneous tasks", default_scope = "revert" }, 50 | { body = ".*security", group = "🛡️ Security" }, 51 | ] 52 | # filter out the commits that are not matched by commit parsers 53 | filter_commits = false 54 | # sort the tags topologically 55 | topo_order = false 56 | # sort the commits inside sections by oldest/newest order 57 | sort_commits = "oldest" 58 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stfed" 3 | version = "1.1.0" 4 | edition = "2021" 5 | rust-version = "1.82" 6 | 7 | [profile.release] 8 | lto = true 9 | codegen-units = 1 10 | strip = true 11 | 12 | [dependencies] 13 | anyhow = { version = "1.0.95", default-features = false, features = ["std", "backtrace"] } 14 | globset = { version = "0.4.15", default-features = false } 15 | log = { version = "0.4.25", default-features = false, features = ["max_level_trace", "release_max_level_debug", "std"] } 16 | quick-xml = { version = "0.37.2", default-features = false, features = ["serialize"] } 17 | serde = { version = "1.0.217", default-features = false, features = ["std", "derive"] } 18 | serde_json = { version = "1.0.135", default-features = false, features = ["std", "raw_value"] } 19 | shlex = { version = "1.3.0", default-features = false } 20 | simple-expand-tilde = { version = "0.4.6", default-features = false } 21 | simple_logger = { version = "5.0.0", default-features = false, features = ["colors", "stderr"] } 22 | thiserror = { version = "2.0.11", default-features = false } 23 | toml = { version = "0.8.19", default-features = false, features = ["parse"] } 24 | ureq = { version = "2.12.1", default-features = false } 25 | url = { version = "2.5.4", default-features = false, features = ["serde"] } 26 | xdg = { version = "2.5.2", default-features = false } 27 | 28 | [lints.rust] 29 | # https://doc.rust-lang.org/rustc/lints/listing/allowed-by-default.html 30 | explicit_outlives_requirements = "warn" 31 | missing_docs = "warn" 32 | non_ascii_idents = "deny" 33 | redundant-lifetimes = "warn" 34 | single-use-lifetimes = "warn" 35 | unit-bindings = "warn" 36 | unreachable_pub = "warn" 37 | unused_crate_dependencies = "warn" 38 | unused-lifetimes = "warn" 39 | unused-qualifications = "warn" 40 | 41 | [lints.clippy] 42 | pedantic = { level = "warn", priority = -1 } 43 | # below lints are from clippy::restriction, and assume clippy >= 1.84 44 | # https://rust-lang.github.io/rust-clippy/master/index.html#/?levels=allow&groups=restriction 45 | allow_attributes = "warn" 46 | clone_on_ref_ptr = "warn" 47 | dbg_macro = "warn" 48 | empty_enum_variants_with_brackets = "warn" 49 | # expect_used = "warn" 50 | field_scoped_visibility_modifiers = "warn" 51 | fn_to_numeric_cast_any = "warn" 52 | format_push_string = "warn" 53 | if_then_some_else_none = "warn" 54 | impl_trait_in_params = "warn" 55 | infinite_loop = "warn" 56 | lossy_float_literal = "warn" 57 | map_with_unused_argument_over_ranges = "warn" 58 | missing_docs_in_private_items = "warn" 59 | mixed_read_write_in_expression = "warn" 60 | module_name_repetitions = "warn" 61 | multiple_inherent_impl = "warn" 62 | needless_raw_strings = "warn" 63 | non_zero_suggestions = "warn" 64 | panic = "warn" 65 | pathbuf_init_then_push = "warn" 66 | pub_without_shorthand = "warn" 67 | redundant_type_annotations = "warn" 68 | ref_patterns = "warn" 69 | renamed_function_params = "warn" 70 | rest_pat_in_fully_bound_structs = "warn" 71 | same_name_method = "warn" 72 | self_named_module_files = "warn" 73 | semicolon_inside_block = "warn" 74 | shadow_unrelated = "warn" 75 | str_to_string = "warn" 76 | string_slice = "warn" 77 | string_to_string = "warn" 78 | tests_outside_test_module = "warn" 79 | try_err = "warn" 80 | undocumented_unsafe_blocks = "warn" 81 | unnecessary_safety_comment = "warn" 82 | unnecessary_safety_doc = "warn" 83 | unneeded_field_pattern = "warn" 84 | unseparated_literal_suffix = "warn" 85 | unused_result_ok = "warn" 86 | unused_trait_names = "warn" 87 | unwrap_used = "warn" 88 | verbose_file_reads = "warn" 89 | -------------------------------------------------------------------------------- /src/hook.rs: -------------------------------------------------------------------------------- 1 | //! Code to run hooks commands 2 | 3 | use std::{ 4 | collections::HashSet, 5 | path::{Path, PathBuf}, 6 | process::{Child, Command, Stdio}, 7 | ptr, 8 | sync::{mpsc, Arc, Mutex}, 9 | time::Duration, 10 | }; 11 | 12 | use crate::config; 13 | 14 | /// Unique identifier for a folder hook 15 | #[derive(Clone, Eq, Hash, PartialEq)] 16 | pub(crate) struct FolderHookId(usize); 17 | 18 | impl FolderHookId { 19 | /// Create unique identifier for hook 20 | pub(crate) fn from_hook(hook: &config::FolderHook) -> Self { 21 | let val = ptr::from_ref(hook) as usize; 22 | Self(val) 23 | } 24 | } 25 | 26 | /// Run a given hook for a given path/folder 27 | pub(crate) fn run( 28 | hook: &config::FolderHook, 29 | path: Option<&Path>, 30 | folder: &Path, 31 | reaper_tx: &mpsc::Sender<(FolderHookId, Child)>, 32 | running_hooks: &Arc>>, 33 | ) -> anyhow::Result<()> { 34 | let allow_concurrent = hook.allow_concurrent.unwrap_or(false); 35 | let hook_id = FolderHookId::from_hook(hook); 36 | let mut running_hooks_locked = running_hooks 37 | .lock() 38 | .map_err(|_| anyhow::anyhow!("Failed to take lock"))?; 39 | if allow_concurrent || !running_hooks_locked.contains(&hook_id) { 40 | running_hooks_locked.insert(hook_id.clone()); 41 | drop(running_hooks_locked); 42 | 43 | log::info!( 44 | "Running hook: {:?} with path {:?} and folder {:?}", 45 | hook, 46 | path, 47 | folder 48 | ); 49 | 50 | let child = Command::new(&hook.command[0]) 51 | .args(&hook.command[1..]) 52 | .env("STFED_PATH", path.unwrap_or(&PathBuf::from(""))) 53 | .env("STFED_FOLDER", folder) 54 | .stdin(Stdio::null()) 55 | .spawn()?; 56 | 57 | reaper_tx.send((hook_id, child))?; 58 | } else { 59 | log::warn!("A process is already running for this hook, and allow_concurrent is set for false, ignoring"); 60 | } 61 | 62 | Ok(()) 63 | } 64 | 65 | /// Reaper thread function, that waits for started processes 66 | pub(crate) fn reaper( 67 | rx: &mpsc::Receiver<(FolderHookId, Child)>, 68 | running_hooks: &Arc>>, 69 | ) -> anyhow::Result<()> { 70 | let mut watched = Vec::new(); 71 | loop { 72 | /// Wait delay for channel recv, only effective if having at least 1 process to watch 73 | const REAPER_WAIT_DELAY: Duration = Duration::from_millis(500); 74 | if watched.is_empty() { 75 | let new = rx.recv()?; 76 | watched.push(new); 77 | } else if let Ok(new) = rx.recv_timeout(REAPER_WAIT_DELAY) { 78 | watched.push(new); 79 | } 80 | loop { 81 | let mut do_loop = false; 82 | for (i, (hook_id, child)) in watched.iter_mut().enumerate() { 83 | if let Some(rc) = child.try_wait()? { 84 | log::info!("Process exited with code {:?}", rc.code()); 85 | { 86 | let mut running_hooks_locked = running_hooks 87 | .lock() 88 | .map_err(|_| anyhow::anyhow!("Failed to take lock"))?; 89 | running_hooks_locked.remove(hook_id); 90 | } 91 | watched.swap_remove(i); 92 | do_loop = true; 93 | break; 94 | } 95 | } 96 | if !do_loop { 97 | break; 98 | } 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **S**ync**t**hing **F**older **E**vent **D**aemon (stfed) 2 | 3 | [![Build status](https://github.com/desbma/stfed/actions/workflows/ci.yml/badge.svg)](https://github.com/desbma/stfed/actions) 4 | [![AUR version](https://img.shields.io/aur/version/stfed.svg?style=flat)](https://aur.archlinux.org/packages/stfed/) 5 | [![License](https://img.shields.io/github/license/desbma/stfed.svg?style=flat)](https://github.com/desbma/stfed/blob/master/LICENSE) 6 | 7 | **S**ync**t**hing **F**older **E**vent **D**aemon, aka `stfed`, is a small companion daemon to run alongside [Syncthing](https://syncthing.net/), which allows running custom commands when certain folder events happen. 8 | 9 | I wrote this to replace a bunch of inefficient and unreliable scripts that were using [inotifywait](https://man.archlinux.org/man/community/inotify-tools/inotifywait.1.en) to watch specific files/folders. Instead of watching at the file level, `stfed` uses the Syncthing API to be notified when files are synchronized. 10 | 11 | It is very light on ressource usage and is therefore suitable for use in all contexts: desktop PC, headless servers, home automation setups, etc. 12 | 13 | ## Features 14 | 15 | - can react to custom events 16 | - folder synchronisation finished 17 | - file synchronisation finished 18 | - synchronisation conflict 19 | - light on system ressources 20 | - no runtime dependency outside of Syncthing 21 | 22 | ## Installation 23 | 24 | ### From source 25 | 26 | You need a Rust build environment for example from [rustup](https://rustup.rs/). 27 | 28 | ``` 29 | cargo build --release 30 | install -Dm 755 -t /usr/local/bin target/release/stfed 31 | ``` 32 | 33 | A systemd service is provided for convenience, to use it: 34 | 35 | ``` 36 | install -D -t ~/.config/systemd/user ./systemd/stfed.service 37 | systemctl --user daemon-reload 38 | systemctl --user enable --now stfed.service 39 | ``` 40 | 41 | ### From the AUR 42 | 43 | Arch Linux users can install the [stfed AUR package](https://aur.archlinux.org/packages/stfed/). 44 | 45 | ## Configuration 46 | 47 | Configuration file are stored in a directory following the [XDG specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html), so typically `~/.config/stfed`. 48 | 49 | ### Main configuration 50 | 51 | `config.toml` 52 | 53 | **This file is optional, if absent, `stfed` will read Syncthing configuration and build its own configuration from it. You typically only need to use this file if using a non local Syncthing instance.** 54 | 55 | Sample content: 56 | 57 | ``` 58 | url = "http://127.0.0.1:8384/" # Syncthing URL 59 | api_key = "xyz" # Syncthing API key 60 | ``` 61 | 62 | ### Hooks 63 | 64 | `hooks.toml` 65 | 66 | This file defines hooks, ie. events you want to react to, and what commands to run when they occur. 67 | 68 | Sample section for a single hook: 69 | 70 | ``` 71 | [[hooks]] 72 | 73 | # Syncthing folder path 74 | folder = "/absolute/path/of/the/folder" 75 | 76 | # Event type, one of: 77 | # file_down_sync_done: triggers when a file has been fully synchronized locally (see filter to match for a specific file) 78 | # folder_down_sync_done: triggers when a folder has been fully synchronized locally 79 | # file_conflict: triggers when Syncthing creates a conflict file due to a local synchronization conflict 80 | # remote_file_conflict: triggers when Syncthing creates a conflict file due to a remote synchronization conflict 81 | event = "file_down_sync_done" 82 | 83 | # glob rule for specific file matching for file_down_sync_done events 84 | filter = "shopping-list.txt" 85 | 86 | # command to run when event triggers 87 | command = "notify-send 'stfef event triggered!'" 88 | 89 | # Whether to allow several commands for the same hook to run simultaneously 90 | # if false, and a burst of events comes, the commands will be skipped while the previous one is still running 91 | # optional, defaults to false 92 | allow_concurrent = false 93 | ``` 94 | 95 | ## License 96 | 97 | [GPLv3](https://www.gnu.org/licenses/gpl-3.0-standalone.html) 98 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | 5 | ### 🐛 Bug fixes 6 | 7 | - Elided lifetime compile error ([780c61f](https://github.com/desbma/stfed/commit/780c61ffb6c2b6e7a6e8c63e81a8b677038df71b) by desbma) 8 | 9 | ### 🚜 Refactor 10 | 11 | - Use Option::transpose ([e92e523](https://github.com/desbma/stfed/commit/e92e5238e16a8119c187c2190c9a5e074310ecbb) by desbma) 12 | - Lazy anyhow context ([4d63889](https://github.com/desbma/stfed/commit/4d63889af98b1714d6fe1b876f2bc529884ae5d4) by desbma) 13 | 14 | ### 🧰 Miscellaneous tasks 15 | 16 | - Update lints ([4b48cb5](https://github.com/desbma/stfed/commit/4b48cb54bfdfed61cbc48af699cde40b47d49fff) by desbma) 17 | - Update pre-commit hooks ([1a6be64](https://github.com/desbma/stfed/commit/1a6be6456a7890342594b0ea973a3d0d86c4bc42) by desbma) 18 | 19 | ______________________________________________________________________ 20 | 21 | ## 1.1.0 - 2024-11-06 22 | 23 | ### 💡 Features 24 | 25 | - Support remote file conflict hook ([f930f66](https://github.com/desbma/stfed/commit/f930f661f08143331e2fb0b31340814b0d403878) by desbma) 26 | 27 | ### 🚜 Refactor 28 | 29 | - Simplify FolderHookId type ([f5868e8](https://github.com/desbma/stfed/commit/f5868e8f90526dca0ed913756f3fb296a993fff5) by desbma) 30 | 31 | ### 🧰 Miscellaneous tasks 32 | 33 | - Enable more lints ([d77c857](https://github.com/desbma/stfed/commit/d77c857ebbebe8d7eba4ff93fe314b2476216d03) by desbma) 34 | - Update release script ([972edcf](https://github.com/desbma/stfed/commit/972edcfc9e7b250a839467f832f3bebbbc45ee26) by desbma) 35 | 36 | ______________________________________________________________________ 37 | 38 | ## 1.0.4 - 2024-09-29 39 | 40 | ### 🐛 Bug fixes 41 | 42 | - Http timeout firing before API timeout ([ed8d17c](https://github.com/desbma/stfed/commit/ed8d17cb6b6c5de60390116b7b23411bfccc42c8) by desbma) 43 | 44 | ______________________________________________________________________ 45 | 46 | ## 1.0.3 - 2024-09-12 47 | 48 | ### 🐛 Bug fixes 49 | 50 | - Update for new possible Syncthing config dir ([02336ce](https://github.com/desbma/stfed/commit/02336ceec087f19111650cab7088a4d0c6e59b5e) by desbma) 51 | 52 | ### 📗 Documentation 53 | 54 | - README: Add AUR reference ([4db64b5](https://github.com/desbma/stfed/commit/4db64b5d6089bc58ffe719d74f21634598977bba) by desbma) 55 | 56 | ### 🧰 Miscellaneous tasks 57 | 58 | - Bump simple_logger dependency ([e533aa4](https://github.com/desbma/stfed/commit/e533aa4ae55b5c034d56d4aca2a063ad7b357623) by desbma) 59 | - Update dependencies ([26bc51c](https://github.com/desbma/stfed/commit/26bc51cb1fe1ab027bd0f64da8a8425191b10749) by desbma) 60 | - Lint ([3dfce86](https://github.com/desbma/stfed/commit/3dfce86ea44f99a168052bf1a65ce82517c766b7) by desbma) 61 | - Add more clippy lints ([f670315](https://github.com/desbma/stfed/commit/f6703154aff7775d38206a552976794b45b73f4e) by desbma) 62 | 63 | ______________________________________________________________________ 64 | 65 | ## 1.0.2 - 2023-10-19 66 | 67 | ### 🐛 Bug fixes 68 | 69 | - Error message spacing chars ([c59d19c](https://github.com/desbma/stfed/commit/c59d19c55c52197182dfaf094f6f2a394b9aef2b) by desbma) 70 | 71 | ### 🧰 Miscellaneous tasks 72 | 73 | - Lint ([16b7383](https://github.com/desbma/stfed/commit/16b7383a1ec4d6e0e28b00f5b20ded99022ad9df) by desbma) 74 | 75 | ______________________________________________________________________ 76 | 77 | ## 1.0.1 - 2023-01-10 78 | 79 | ### 💡 Features 80 | 81 | - Improve retry logic to also retry if first connection fails ([1ebe996](https://github.com/desbma/stfed/commit/1ebe996ab6b61fb3701244a9aea5abd74e5abca3) by desbma) 82 | - Add git cliff config ([2d556c5](https://github.com/desbma/stfed/commit/2d556c59f7cd4901bd715f7f89cdd81aded47215) by desbma) 83 | 84 | ### 🐛 Bug fixes 85 | 86 | - Change system service dependency to syncthing.service ([2f5c142](https://github.com/desbma/stfed/commit/2f5c142393beff19190e3c1d90cfc02bbb5b19ac) by desbma) 87 | - Syncthing config parsed when not needed ([93633b2](https://github.com/desbma/stfed/commit/93633b2c778b0953f1dd07302d86f78180bdc5be) by desbma) 88 | 89 | ______________________________________________________________________ 90 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | //! Local configuration 2 | 3 | use std::{ 4 | fs, io, 5 | ops::Deref, 6 | path::{Path, PathBuf}, 7 | }; 8 | 9 | use anyhow::Context as _; 10 | use serde::de::Deserialize as _; 11 | use simple_expand_tilde::expand_tilde; 12 | 13 | /// Local configuration 14 | #[derive(Debug, serde::Deserialize)] 15 | pub(crate) struct Config { 16 | /// Syncthing base URL 17 | pub url: url::Url, 18 | /// Syncthing API key 19 | pub api_key: String, 20 | } 21 | 22 | /// Root local Syncthing configuration 23 | #[derive(serde::Deserialize)] 24 | struct SyncthingXmlConfig { 25 | /// "GUI" configuration part, whatever that means 26 | gui: SyncthingXmlConfigGui, 27 | } 28 | 29 | /// GUI local Syncthing configuration 30 | #[derive(serde::Deserialize)] 31 | struct SyncthingXmlConfigGui { 32 | /// Listening address 33 | address: String, 34 | /// API key 35 | apikey: String, 36 | } 37 | 38 | impl Config { 39 | /// Try to generate a valid default configuration from the local Syncthing configuration 40 | fn default_from_syncthing_config() -> anyhow::Result { 41 | // Read Syncthing config to get address & API key 42 | let xdg_dirs = xdg::BaseDirectories::with_prefix("syncthing") 43 | .context("Unable fo find Synthing config directory")?; 44 | let st_config_filepath = xdg_dirs 45 | .find_state_file("config.xml") 46 | .or_else(|| xdg_dirs.find_config_file("config.xml")) 47 | .context("Unable fo find Synthing config file")?; 48 | log::debug!("Found Syncthing config in {:?}", st_config_filepath); 49 | let st_config_xml = fs::read_to_string(st_config_filepath)?; 50 | let st_config: SyncthingXmlConfig = quick_xml::de::from_str(&st_config_xml)?; 51 | 52 | Ok(Self { 53 | url: url::Url::parse(&format!("http://{}", st_config.gui.address))?, 54 | api_key: st_config.gui.apikey, 55 | }) 56 | } 57 | } 58 | 59 | impl Default for Config { 60 | fn default() -> Self { 61 | #[expect(clippy::unwrap_used)] 62 | Self::default_from_syncthing_config() 63 | .with_context(|| { 64 | format!( 65 | "Unable to guess {} configuration field values from Synthing config, \ 66 | please write a config file", 67 | env!("CARGO_PKG_NAME") 68 | ) 69 | }) 70 | .unwrap() 71 | } 72 | } 73 | 74 | /// Folder hooks configurations 75 | #[derive(Debug, serde::Deserialize)] 76 | pub(crate) struct FolderConfig { 77 | /// Hooks array 78 | pub hooks: Vec, 79 | } 80 | 81 | #[derive(Clone, Debug, Hash, PartialEq, Eq)] 82 | pub(crate) struct NormalizedPath(PathBuf); 83 | 84 | impl<'de> serde::Deserialize<'de> for NormalizedPath { 85 | fn deserialize>(deserializer: D) -> Result { 86 | let pb = PathBuf::deserialize(deserializer)?; 87 | pb.as_path().try_into().map_err(serde::de::Error::custom) 88 | } 89 | } 90 | 91 | impl TryFrom<&Path> for NormalizedPath { 92 | type Error = io::Error; 93 | 94 | fn try_from(path: &Path) -> Result { 95 | let path = expand_tilde(path) 96 | .ok_or_else(|| io::Error::other("User not found"))? 97 | .canonicalize()?; 98 | Ok(Self(path)) 99 | } 100 | } 101 | 102 | impl Deref for NormalizedPath { 103 | type Target = Path; 104 | 105 | fn deref(&self) -> &Self::Target { 106 | self.0.as_path() 107 | } 108 | } 109 | 110 | /// Configuration for a folder hook 111 | #[derive(Clone, Debug, serde::Deserialize)] 112 | pub(crate) struct FolderHook { 113 | /// Absolute path of the folder 114 | pub folder: NormalizedPath, 115 | /// Event to hook 116 | pub event: FolderEvent, 117 | /// Event filter 118 | #[serde(default)] 119 | #[serde(deserialize_with = "deserialize_glob")] 120 | pub filter: Option, 121 | /// Command 122 | #[serde(deserialize_with = "deserialize_command")] 123 | pub command: Vec, 124 | /// Allow concurrent runs for the same hook 125 | pub allow_concurrent: Option, 126 | } 127 | 128 | /// Deserialize filter into a glob matcher to validate glob expression 129 | fn deserialize_glob<'de, D>(deserializer: D) -> Result, D::Error> 130 | where 131 | D: serde::Deserializer<'de>, 132 | { 133 | let opt: Option = Option::deserialize(deserializer)?; 134 | opt.map(|s| { 135 | globset::GlobBuilder::new(&s) 136 | .literal_separator(true) 137 | .build() 138 | .map(|g| g.compile_matcher()) 139 | .map_err(serde::de::Error::custom) 140 | }) 141 | .transpose() 142 | } 143 | 144 | /// Deserialize command string into a vec directly usable by `std::Command` 145 | fn deserialize_command<'de, D>(deserializer: D) -> Result, D::Error> 146 | where 147 | D: serde::Deserializer<'de>, 148 | { 149 | let s = String::deserialize(deserializer)?; 150 | shlex::split(&s).ok_or_else(|| serde::de::Error::custom(format!("Invalid command: {s:?}"))) 151 | } 152 | 153 | /// Folder event kind 154 | #[derive(Clone, Debug, Eq, Hash, PartialEq, serde::Deserialize)] 155 | #[serde(rename_all = "snake_case")] 156 | pub(crate) enum FolderEvent { 157 | /// A whole folder has been synced down 158 | FolderDownSyncDone, 159 | /// A file has been synced down 160 | FileDownSyncDone, 161 | /// A conflict has occured locally 162 | FileConflict, 163 | /// A conflict has occured remotely 164 | RemoteFileConflict, 165 | } 166 | 167 | /// Parse local configuration 168 | pub(crate) fn parse() -> anyhow::Result<(Config, FolderConfig)> { 169 | let binary_name = env!("CARGO_PKG_NAME"); 170 | let xdg_dirs = xdg::BaseDirectories::with_prefix(binary_name)?; 171 | let config_filepath = xdg_dirs.find_config_file("config.toml"); 172 | 173 | let config = if let Some(config_filepath) = config_filepath { 174 | log::debug!("Config filepath: {:?}", config_filepath); 175 | 176 | let toml_data = fs::read_to_string(config_filepath)?; 177 | log::trace!("Config data: {:?}", toml_data); 178 | 179 | toml::from_str(&toml_data)? 180 | } else { 181 | log::warn!("Unable to find config file, using default config"); 182 | Config::default() 183 | }; 184 | 185 | log::trace!("Config: {:?}", config); 186 | 187 | let hooks_filepath = xdg_dirs 188 | .find_config_file("hooks.toml") 189 | .ok_or_else(|| anyhow::anyhow!("Unable to find hooks file"))?; 190 | log::debug!("Hooks filepath: {:?}", hooks_filepath); 191 | 192 | let toml_data = fs::read_to_string(hooks_filepath)?; 193 | log::trace!("Hooks data: {:?}", toml_data); 194 | let hooks = toml::from_str(&toml_data)?; 195 | 196 | log::trace!("Hooks: {:?}", hooks); 197 | 198 | Ok((config, hooks)) 199 | } 200 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | //! Syncthing Folder Event Daemon 2 | 3 | use std::{ 4 | collections::{ 5 | hash_map::{Entry, HashMap}, 6 | HashSet, 7 | }, 8 | io, 9 | rc::Rc, 10 | sync::{mpsc, Arc, LazyLock, Mutex}, 11 | thread, 12 | time::Duration, 13 | }; 14 | 15 | use anyhow::Context as _; 16 | use config::NormalizedPath; 17 | 18 | mod config; 19 | mod hook; 20 | mod syncthing; 21 | mod syncthing_rest; 22 | 23 | /// Delay to wait for before trying to reconnect to Synthing server 24 | const RECONNECT_DELAY: Duration = Duration::from_secs(5); 25 | 26 | /// Glob matcher for a conflict file 27 | static CONFLICT_MATCHER: LazyLock = LazyLock::new(|| { 28 | #[expect(clippy::unwrap_used)] 29 | globset::Glob::new("*.sync-conflict-*") 30 | .unwrap() 31 | .compile_matcher() 32 | }); 33 | 34 | #[expect(clippy::too_many_lines)] 35 | fn main() -> anyhow::Result<()> { 36 | // Init logger 37 | simple_logger::SimpleLogger::new() 38 | .env() 39 | .init() 40 | .context("Failed to init logger")?; 41 | 42 | // Parse config 43 | let (cfg, hooks) = config::parse().context("Failed to read local config")?; 44 | 45 | // Build hook map for fast matching 46 | let mut hooks_map: HashMap<(config::FolderEvent, Rc), Vec> = 47 | HashMap::new(); 48 | for hook in &hooks.hooks { 49 | match hooks_map.entry((hook.event.clone(), Rc::new(hook.folder.clone()))) { 50 | Entry::Occupied(mut e) => { 51 | e.get_mut().push(hook.clone()); 52 | } 53 | Entry::Vacant(e) => { 54 | e.insert(vec![hook.clone()]); 55 | } 56 | } 57 | } 58 | 59 | // Setup running hooks state 60 | let running_hooks: Arc>> = 61 | Arc::new(Mutex::new(HashSet::new())); 62 | let running_hooks_reaper = Arc::clone(&running_hooks); 63 | 64 | // Create reaper thread and channel 65 | let (reaper_tx, reaper_rx) = mpsc::channel(); 66 | thread::Builder::new() 67 | .name("reaper".to_owned()) 68 | .spawn(move || -> anyhow::Result<()> { hook::reaper(&reaper_rx, &running_hooks_reaper) })?; 69 | 70 | loop { 71 | // Setup client 72 | let client_res = syncthing::Client::new(&cfg); 73 | match client_res { 74 | Ok(client) => { 75 | // Event loop 76 | for event in client.iter_events() { 77 | // Handle special events 78 | let event = match &event { 79 | Err(err) => { 80 | if let Some(err) = err.downcast_ref::() { 81 | log::warn!( 82 | "Syncthing server is gone, will restart main loop. {:?}", 83 | err 84 | ); 85 | break; 86 | } else if let Some(err) = 87 | err.downcast_ref::() 88 | { 89 | log::warn!( 90 | "Syncthing server configuration changed, will restart main loop. {:?}", 91 | err 92 | ); 93 | break; 94 | } 95 | event?; 96 | unreachable!(); 97 | } 98 | Ok(event) => event, 99 | }; 100 | log::info!("New event: {:?}", event); 101 | 102 | // Dispatch event 103 | match event { 104 | syncthing::Event::FileDownSyncDone { path, folder } => { 105 | let folder: Rc = Rc::new(folder.as_path().try_into()?); 106 | for hook in hooks_map 107 | .get(&(config::FolderEvent::FileDownSyncDone, Rc::clone(&folder))) 108 | .unwrap_or(&vec![]) 109 | { 110 | if hook.filter.as_ref().is_none_or(|g| g.is_match(path)) { 111 | hook::run( 112 | hook, 113 | Some(path), 114 | &folder, 115 | &reaper_tx, 116 | &running_hooks, 117 | )?; 118 | } 119 | } 120 | for hook in hooks_map 121 | .get(&(config::FolderEvent::RemoteFileConflict, Rc::clone(&folder))) 122 | .unwrap_or(&vec![]) 123 | { 124 | if CONFLICT_MATCHER.is_match(path) { 125 | hook::run( 126 | hook, 127 | Some(path), 128 | &folder, 129 | &reaper_tx, 130 | &running_hooks, 131 | )?; 132 | } 133 | } 134 | } 135 | syncthing::Event::FolderDownSyncDone { folder } => { 136 | let folder: Rc = Rc::new(folder.as_path().try_into()?); 137 | for hook in hooks_map 138 | .get(&(config::FolderEvent::FolderDownSyncDone, Rc::clone(&folder))) 139 | .unwrap_or(&vec![]) 140 | { 141 | hook::run(hook, None, &folder, &reaper_tx, &running_hooks)?; 142 | } 143 | } 144 | syncthing::Event::FileConflict { path, folder } => { 145 | let folder: Rc = Rc::new(folder.as_path().try_into()?); 146 | for hook in hooks_map 147 | .get(&(config::FolderEvent::FileConflict, Rc::clone(&folder))) 148 | .unwrap_or(&vec![]) 149 | { 150 | hook::run(hook, Some(path), &folder, &reaper_tx, &running_hooks)?; 151 | } 152 | } 153 | } 154 | } 155 | } 156 | #[expect(clippy::ref_patterns)] 157 | Err(ref err) => match err.root_cause().downcast_ref::() { 158 | Some(err2) if err2.kind() == io::ErrorKind::ConnectionRefused => { 159 | log::warn!( 160 | "Syncthing server connection failed, will restart main loop. {:?}", 161 | err 162 | ); 163 | } 164 | _ => { 165 | client_res?; 166 | } 167 | }, 168 | } 169 | 170 | log::info!("Will reconnect in {:?}", RECONNECT_DELAY); 171 | thread::sleep(RECONNECT_DELAY); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/syncthing.rs: -------------------------------------------------------------------------------- 1 | //! Syncthing related code 2 | 3 | use std::{ 4 | collections::hash_map::{Entry, HashMap}, 5 | io, 6 | path::PathBuf, 7 | sync::LazyLock, 8 | time::Duration, 9 | }; 10 | 11 | use crate::{config, syncthing_rest}; 12 | 13 | /// Error when server vanished 14 | #[derive(thiserror::Error, Debug)] 15 | #[error(transparent)] 16 | pub(crate) struct ServerGone { 17 | /// Inner error 18 | #[from] 19 | inner: io::Error, 20 | } 21 | 22 | /// Error when server config changed 23 | #[derive(thiserror::Error, Debug)] 24 | pub(crate) enum ServerConfigChanged { 25 | /// Server initiated config changed notification via event 26 | #[error("Server sent ConfigSaved event")] 27 | ConfigSaved, 28 | } 29 | 30 | /// Syncthing client used to interact with the Syncthing REST API 31 | pub(crate) struct Client { 32 | /// Syncthing URL 33 | base_url: url::Url, 34 | /// API key 35 | api_key: String, 36 | /// HTTP session 37 | session: ureq::Agent, 38 | /// Folder id to path 39 | folder_map: HashMap, 40 | } 41 | 42 | /// API timeout for long event requests 43 | const REST_TIMEOUT_EVENT_STREAM: Duration = Duration::from_secs(60 * 60); 44 | /// HTTP timeout for normal requests 45 | const HTTP_TIMEOUT: Duration = Duration::from_secs(10); 46 | /// HTTP timeout for long event requests 47 | static HTTP_TIMEOUT_EVENT_STREAM: LazyLock = 48 | LazyLock::new(|| REST_TIMEOUT_EVENT_STREAM + HTTP_TIMEOUT); 49 | /// Header key value for Synthing API key 50 | const HEADER_API_KEY: &str = "X-API-Key"; 51 | 52 | impl Client { 53 | /// Constructor 54 | pub(crate) fn new(cfg: &config::Config) -> anyhow::Result { 55 | // Build session 56 | let session = ureq::AgentBuilder::new() 57 | .timeout_connect(HTTP_TIMEOUT) 58 | .timeout_read(*HTTP_TIMEOUT_EVENT_STREAM) 59 | .timeout_write(HTTP_TIMEOUT) 60 | .user_agent(&format!( 61 | "{}/{}", 62 | env!("CARGO_PKG_NAME"), 63 | env!("CARGO_PKG_VERSION") 64 | )) 65 | .build(); 66 | 67 | // Get system config to build folder map 68 | let base_url = cfg.url.clone(); 69 | let url = base_url.join("rest/system/config")?; 70 | log::debug!("GET {:?}", url); 71 | let json_str = session 72 | .get(url.as_ref()) 73 | .timeout(HTTP_TIMEOUT) 74 | .set(HEADER_API_KEY, &cfg.api_key) 75 | .call()? 76 | .into_string()?; 77 | log::trace!("{}", json_str); 78 | let system_config: syncthing_rest::SystemConfig = serde_json::from_str(&json_str)?; 79 | 80 | // Build folder map 81 | let folder_map = system_config 82 | .folders 83 | .into_iter() 84 | .map(|f| (f.id, PathBuf::from(f.path))) 85 | .collect(); 86 | 87 | Ok(Self { 88 | base_url, 89 | session, 90 | api_key: cfg.api_key.clone(), 91 | folder_map, 92 | }) 93 | } 94 | 95 | /// Iterator over infinite stream of events 96 | pub(crate) fn iter_events(&self) -> FolderEventIterator<'_> { 97 | FolderEventIterator::new(self) 98 | } 99 | 100 | /// Get a single event, no filtering is done at this level 101 | fn event(&self, since: u64, evt_types: &[&str]) -> anyhow::Result { 102 | // See https://docs.syncthing.net/dev/events.html 103 | let mut url = self.base_url.clone(); 104 | url.path_segments_mut() 105 | .map_err(|()| anyhow::anyhow!("Invalid URL {}", self.base_url))? 106 | .push("rest") 107 | .push("events"); 108 | url.query_pairs_mut() 109 | .append_pair("since", &since.to_string()) 110 | .append_pair("limit", "1") 111 | .append_pair("events", &evt_types.join(",")); 112 | url.query_pairs_mut() 113 | .append_pair("timeout", &REST_TIMEOUT_EVENT_STREAM.as_secs().to_string()); 114 | loop { 115 | log::debug!("GET {:?}", url.to_string()); 116 | let response = self 117 | .session 118 | .get(url.as_ref()) 119 | .set(HEADER_API_KEY, &self.api_key) 120 | .call()? 121 | .into_string(); 122 | let json_str = match response { 123 | // ureq sends InvalidInput error when socket closes unexpectedly 124 | Err(err) if err.kind() == io::ErrorKind::InvalidInput => { 125 | return Err(ServerGone { inner: err }.into()); 126 | } 127 | Err(_) => response?, 128 | Ok(resp) => resp, 129 | }; 130 | log::trace!("{}", json_str); 131 | let mut events: Vec = serde_json::from_str(&json_str)?; 132 | assert!(events.len() <= 1); 133 | if let Some(event) = events.pop() { 134 | return Ok(event); 135 | } 136 | } 137 | } 138 | } 139 | 140 | /// Iterator of Syncthing events 141 | pub(crate) struct FolderEventIterator<'a> { 142 | /// API client 143 | client: &'a Client, 144 | /// Last event id 145 | last_id: u64, 146 | /// Last state change for folder to avoid duplicates 147 | folder_state_change_time: HashMap, 148 | } 149 | 150 | impl<'a> FolderEventIterator<'a> { 151 | /// Constructor 152 | fn new(client: &'a Client) -> Self { 153 | Self { 154 | client, 155 | last_id: 0, 156 | folder_state_change_time: HashMap::new(), 157 | } 158 | } 159 | } 160 | 161 | impl Iterator for FolderEventIterator<'_> { 162 | type Item = anyhow::Result; 163 | 164 | fn next(&mut self) -> Option { 165 | loop { 166 | // TODO subscribe to ItemFinished/FolderSummary only if needed 167 | // Notes: 168 | // DownloadProgress is not emitted for small downloads 169 | // FolderCompletion is for remote device progress 170 | let new_evt_res = self.client.event( 171 | self.last_id, 172 | &[ 173 | "ItemFinished", 174 | "FolderSummary", 175 | "LocalChangeDetected", 176 | "ConfigSaved", 177 | ], 178 | ); 179 | return match new_evt_res { 180 | Ok(new_evt) => { 181 | // Update last id 182 | self.last_id = new_evt.id; 183 | 184 | match new_evt.data { 185 | syncthing_rest::EventData::ItemFinished(evt_data) => { 186 | let folder_path = self 187 | .client 188 | .folder_map 189 | .get(&evt_data.folder) 190 | .expect("Unknown folder id"); 191 | Some(Ok(Event::FileDownSyncDone { 192 | path: PathBuf::from(evt_data.item), 193 | folder: folder_path.to_owned(), 194 | })) 195 | } 196 | syncthing_rest::EventData::FolderSummary(evt_data) => { 197 | if evt_data.summary.need_total_items > 0 { 198 | // Not complete 199 | continue; 200 | } 201 | let changed = evt_data.summary.state_changed; 202 | match self.folder_state_change_time.entry(evt_data.folder.clone()) { 203 | Entry::Occupied(mut e) => { 204 | if e.get() == &changed { 205 | // Duplicate event 206 | continue; 207 | } 208 | e.insert(changed); 209 | } 210 | Entry::Vacant(e) => { 211 | e.insert(changed); 212 | } 213 | } 214 | let folder_path = self 215 | .client 216 | .folder_map 217 | .get(&evt_data.folder) 218 | .expect("Unknown folder id"); 219 | Some(Ok(Event::FolderDownSyncDone { 220 | folder: folder_path.to_owned(), 221 | })) 222 | } 223 | syncthing_rest::EventData::LocalChangeDetected(evt_data) => { 224 | // see https://github.com/syncthing/syncthing/issues/6121#issuecomment-549077477 225 | if (evt_data.item_type == "file") 226 | && (evt_data.action == "modified") 227 | && (evt_data.path.contains(".sync-conflict-")) 228 | { 229 | let folder_path = self 230 | .client 231 | .folder_map 232 | .get(&evt_data.folder) 233 | .expect("Unknown folder id"); 234 | Some(Ok(Event::FileConflict { 235 | path: PathBuf::from(evt_data.path), 236 | folder: folder_path.to_owned(), 237 | })) 238 | } else { 239 | continue; 240 | } 241 | } 242 | syncthing_rest::EventData::ConfigSaved(_) => { 243 | Some(Err(ServerConfigChanged::ConfigSaved.into())) 244 | } 245 | _ => unimplemented!(), 246 | } 247 | } 248 | 249 | // Propagate error 250 | Err(e) => Some(Err(e)), 251 | }; 252 | } 253 | } 254 | } 255 | 256 | /// Syncthing event, see `config::FolderEvent` for meaning of each event 257 | #[expect(clippy::missing_docs_in_private_items)] 258 | #[derive(Debug)] 259 | pub(crate) enum Event { 260 | FileDownSyncDone { path: PathBuf, folder: PathBuf }, 261 | FolderDownSyncDone { folder: PathBuf }, 262 | FileConflict { path: PathBuf, folder: PathBuf }, 263 | } 264 | -------------------------------------------------------------------------------- /src/syncthing_rest.rs: -------------------------------------------------------------------------------- 1 | //! Syncthing types, taken from , with some additional fixes 2 | 3 | #![allow( 4 | dead_code, 5 | clippy::enum_glob_use, 6 | clippy::missing_docs_in_private_items 7 | )] 8 | 9 | use std::{collections::HashMap, convert::TryFrom}; 10 | 11 | use serde::{Deserialize, Serialize}; 12 | 13 | // 14 | // Events 15 | // 16 | 17 | type FileName = String; 18 | type DeviceID = String; 19 | type FolderName = String; 20 | type Folder = HashMap; 21 | 22 | #[derive(Debug, Deserialize)] 23 | #[serde(rename_all(deserialize = "camelCase"))] 24 | pub(crate) struct File { 25 | pub total: u64, 26 | pub pulling: u64, 27 | pub copied_from_origin: u64, 28 | pub reused: u64, 29 | pub copied_from_elsewhere: u64, 30 | pub pulled: u64, 31 | pub bytes_total: u64, 32 | pub bytes_done: u64, 33 | } 34 | 35 | #[derive(Debug, Deserialize)] 36 | pub(crate) struct ConfigSavedEvent { 37 | pub version: u64, 38 | } 39 | 40 | #[derive(Debug, Deserialize)] 41 | #[serde(rename_all(deserialize = "camelCase"))] 42 | pub(crate) struct DeviceConnectedEvent { 43 | pub addr: String, 44 | #[serde(rename = "id")] 45 | pub device_id: DeviceID, 46 | pub device_name: String, 47 | pub client_name: String, 48 | pub client_version: String, 49 | #[serde(rename = "type")] 50 | pub client_type: String, 51 | } 52 | 53 | #[derive(Debug, Deserialize)] 54 | pub(crate) struct DeviceDisconnectedEvent { 55 | #[serde(rename = "id")] 56 | pub device_id: DeviceID, 57 | pub error: String, 58 | } 59 | 60 | #[derive(Debug, Deserialize)] 61 | pub(crate) struct DeviceDiscoveredEvent { 62 | #[serde(rename = "device")] 63 | pub device_id: DeviceID, 64 | pub addrs: Vec, 65 | } 66 | 67 | #[derive(Debug, Deserialize)] 68 | pub(crate) struct DevicePausedEvent { 69 | #[serde(rename = "device")] 70 | pub device_id: DeviceID, 71 | } 72 | 73 | #[derive(Debug, Deserialize)] 74 | pub(crate) struct DeviceRejectedEvent { 75 | #[serde(rename = "device")] 76 | pub device_id: DeviceID, 77 | pub name: String, 78 | pub address: String, 79 | } 80 | 81 | #[derive(Debug, Deserialize)] 82 | pub(crate) struct DeviceResumedEvent { 83 | #[serde(rename = "device")] 84 | pub device_id: DeviceID, 85 | } 86 | 87 | #[derive(Debug, Deserialize)] 88 | #[serde(rename_all(deserialize = "camelCase"))] 89 | pub(crate) struct FolderCompletionEvent { 90 | #[serde(rename = "device")] 91 | pub device_id: DeviceID, 92 | #[serde(rename = "folder")] 93 | pub folder_id: String, 94 | pub completion: f64, 95 | pub global_bytes: u64, 96 | pub need_bytes: u64, 97 | pub need_deletes: u64, 98 | pub need_items: u64, 99 | } 100 | 101 | #[derive(Debug, Deserialize)] 102 | pub(crate) struct FolderErrorsEvent { 103 | pub folder: String, 104 | pub errors: Vec, 105 | } 106 | 107 | #[derive(Debug, Deserialize)] 108 | pub(crate) struct FolderError { 109 | pub error: String, 110 | pub path: String, 111 | } 112 | 113 | #[derive(Debug, Deserialize)] 114 | pub(crate) struct FolderRejectedEvent { 115 | #[serde(rename = "device")] 116 | pub device_id: DeviceID, 117 | #[serde(rename = "folder")] 118 | pub folder_id: String, 119 | #[serde(rename = "folderLabel")] 120 | pub folder_label: String, 121 | } 122 | 123 | #[derive(Debug, Deserialize)] 124 | pub(crate) struct FolderScanProgressEvent { 125 | pub total: u64, 126 | pub rate: u64, 127 | pub current: u64, 128 | #[serde(rename = "folder")] 129 | pub folder_id: String, 130 | } 131 | 132 | #[derive(Debug, Deserialize)] 133 | pub(crate) struct FolderSummaryEvent { 134 | pub folder: String, 135 | pub summary: FolderSummaryData, 136 | } 137 | 138 | #[derive(Debug, Deserialize)] 139 | #[serde(rename_all(deserialize = "camelCase"))] 140 | pub(crate) struct FolderSummaryData { 141 | pub global_bytes: u64, 142 | pub global_deleted: u64, 143 | pub global_directories: u64, 144 | pub global_files: u64, 145 | pub global_symlinks: u64, 146 | pub global_total_items: u64, 147 | pub ignore_patterns: bool, 148 | pub in_sync_bytes: u64, 149 | pub in_sync_files: u64, 150 | pub invalid: Option, 151 | pub local_bytes: u64, 152 | pub local_deleted: u64, 153 | pub local_directories: u64, 154 | pub local_files: u64, 155 | pub local_symlinks: u64, 156 | pub local_total_items: u64, 157 | pub need_bytes: u64, 158 | pub need_deletes: u64, 159 | pub need_directories: u64, 160 | pub need_files: u64, 161 | pub need_symlinks: u64, 162 | pub need_total_items: u64, 163 | pub pull_errors: u64, 164 | pub sequence: u64, 165 | pub state: String, 166 | pub state_changed: String, 167 | pub version: u64, 168 | } 169 | 170 | #[derive(Debug, Deserialize)] 171 | #[serde(rename_all(deserialize = "lowercase"))] 172 | pub(crate) enum ItemAction { 173 | Update, 174 | Metadata, 175 | Delete, 176 | } 177 | 178 | #[derive(Debug, Deserialize)] 179 | pub(crate) struct ItemFinishedEvent { 180 | pub item: String, 181 | pub folder: String, 182 | pub error: Option, 183 | #[serde(rename = "type")] 184 | pub item_type: String, 185 | pub action: ItemAction, 186 | } 187 | 188 | #[derive(Debug, Deserialize)] 189 | pub(crate) struct ItemStartedEvent { 190 | pub item: String, 191 | pub folder: String, 192 | #[serde(rename = "type")] 193 | pub item_type: String, 194 | pub action: ItemAction, 195 | } 196 | 197 | #[derive(Debug, Deserialize)] 198 | pub(crate) struct ListenAddressesChangedEvent {} 199 | 200 | #[derive(Debug, Deserialize)] 201 | pub(crate) struct LocalChangeDetectedEvent { 202 | pub action: String, 203 | pub folder: String, 204 | pub label: String, 205 | #[serde(rename = "type")] 206 | pub item_type: String, 207 | pub path: String, 208 | } 209 | 210 | #[derive(Debug, Deserialize)] 211 | pub(crate) struct LocalIndexUpdatedEvent { 212 | #[serde(rename = "folder")] 213 | pub folder_id: String, 214 | pub items: u64, 215 | pub version: u64, 216 | pub filenames: Vec, 217 | } 218 | 219 | #[derive(Debug, Deserialize)] 220 | pub(crate) struct LoginAttemptEvent { 221 | pub username: String, 222 | pub success: bool, 223 | } 224 | #[derive(Debug, Deserialize)] 225 | pub(crate) struct RemoteChangeDetectedEvent { 226 | pub action: String, 227 | #[serde(rename = "folderID")] 228 | pub folder_id: String, 229 | pub label: String, 230 | pub path: String, 231 | #[serde(rename = "type")] 232 | pub item_type: String, 233 | #[serde(rename = "modifiedBy")] 234 | pub modified_by: String, 235 | } 236 | 237 | #[derive(Debug, Deserialize)] 238 | pub(crate) struct RemoteDownloadProgressEvent { 239 | #[serde(rename = "device")] 240 | pub device_id: DeviceID, 241 | pub folder: String, 242 | pub state: HashMap, 243 | } 244 | 245 | #[derive(Debug, Deserialize)] 246 | pub(crate) struct RemoteIndexUpdatedEvent { 247 | #[serde(rename = "device")] 248 | pub device_id: DeviceID, 249 | #[serde(rename = "folder")] 250 | pub folder_id: String, 251 | pub items: u64, 252 | pub version: u64, 253 | } 254 | 255 | #[derive(Debug, Deserialize)] 256 | pub(crate) struct StartingEvent { 257 | #[serde(rename = "myID")] 258 | pub device_id: DeviceID, 259 | pub home: String, 260 | } 261 | 262 | #[derive(Debug, Deserialize)] 263 | #[serde(rename_all(deserialize = "kebab-case"))] 264 | pub(crate) enum FolderState { 265 | Idle, 266 | Scanning, 267 | ScanWaiting, 268 | SyncPreparing, 269 | Syncing, 270 | Error, 271 | Unknown, 272 | } 273 | 274 | #[derive(Debug, Deserialize)] 275 | pub(crate) struct StateChangedEvent { 276 | #[serde(rename = "folder")] 277 | pub folder_id: String, 278 | pub duration: Option, 279 | pub from: FolderState, 280 | pub to: FolderState, 281 | pub error: Option, 282 | } 283 | 284 | #[derive(Debug, Deserialize)] 285 | pub(crate) enum EventData { 286 | ConfigSaved(ConfigSavedEvent), 287 | DeviceConnected(DeviceConnectedEvent), 288 | DeviceDisconnected(DeviceDisconnectedEvent), 289 | DeviceDiscovered(DeviceDiscoveredEvent), 290 | DevicePaused(DevicePausedEvent), 291 | DeviceRejected(DeviceRejectedEvent), 292 | DeviceResumed(DeviceResumedEvent), 293 | DownloadProgress(HashMap), 294 | FolderCompletion(FolderCompletionEvent), 295 | FolderErrors(FolderErrorsEvent), 296 | FolderRejected(FolderRejectedEvent), 297 | FolderScanProgress(FolderScanProgressEvent), 298 | FolderSummary(Box), 299 | ItemFinished(ItemFinishedEvent), 300 | ItemStarted(ItemStartedEvent), 301 | ListenAddressesChanged(ListenAddressesChangedEvent), 302 | LocalChangeDetected(LocalChangeDetectedEvent), 303 | LocalIndexUpdated(LocalIndexUpdatedEvent), 304 | LoginAttempt(LoginAttemptEvent), 305 | RemoteChangeDetected(RemoteChangeDetectedEvent), 306 | RemoteDownloadProgress(RemoteDownloadProgressEvent), 307 | RemoteIndexUpdated(RemoteIndexUpdatedEvent), 308 | Starting(StartingEvent), 309 | StartupComplete, 310 | StateChanged(StateChangedEvent), 311 | } 312 | 313 | #[derive(Debug, Deserialize)] 314 | pub(super) struct RawEvent { 315 | pub id: u64, 316 | #[serde(rename = "globalID")] 317 | pub global_id: u64, 318 | #[serde(rename = "type")] 319 | pub event_type: EventType, 320 | pub time: String, 321 | pub data: Box, 322 | } 323 | 324 | #[derive(Debug, Copy, Clone, Eq, PartialEq, Deserialize, Serialize)] 325 | pub(crate) enum EventType { 326 | ConfigSaved, 327 | DeviceConnected, 328 | DeviceDisconnected, 329 | DeviceDiscovered, 330 | DevicePaused, 331 | DeviceRejected, 332 | DeviceResumed, 333 | DownloadProgress, 334 | FolderCompletion, 335 | FolderErrors, 336 | FolderRejected, 337 | FolderScanProgress, 338 | FolderSummary, 339 | ItemFinished, 340 | ItemStarted, 341 | ListenAddressesChanged, 342 | LocalChangeDetected, 343 | LocalIndexUpdated, 344 | LoginAttempt, 345 | RemoteChangeDetected, 346 | RemoteDownloadProgress, 347 | RemoteIndexUpdated, 348 | Starting, 349 | StartupComplete, 350 | StateChanged, 351 | } 352 | 353 | #[derive(Debug, Deserialize)] 354 | #[serde(try_from = "RawEvent")] 355 | pub(crate) struct Event { 356 | pub id: u64, 357 | pub global_id: u64, 358 | pub time: String, 359 | pub data: EventData, 360 | } 361 | 362 | impl TryFrom for Event { 363 | type Error = serde_json::Error; 364 | 365 | fn try_from(raw_event: RawEvent) -> Result { 366 | use EventData::*; 367 | let RawEvent { 368 | id, 369 | global_id, 370 | event_type, 371 | time, 372 | data, 373 | } = raw_event; 374 | let data = data.get(); 375 | Ok(Event { 376 | id, 377 | global_id, 378 | time, 379 | data: match event_type { 380 | EventType::ConfigSaved => ConfigSaved(serde_json::from_str(data)?), 381 | EventType::DeviceConnected => DeviceConnected(serde_json::from_str(data)?), 382 | EventType::DeviceDisconnected => DeviceDisconnected(serde_json::from_str(data)?), 383 | EventType::DeviceDiscovered => DeviceDiscovered(serde_json::from_str(data)?), 384 | EventType::DevicePaused => DevicePaused(serde_json::from_str(data)?), 385 | EventType::DeviceRejected => DeviceRejected(serde_json::from_str(data)?), 386 | EventType::DeviceResumed => DeviceResumed(serde_json::from_str(data)?), 387 | EventType::DownloadProgress => DownloadProgress(serde_json::from_str(data)?), 388 | EventType::FolderCompletion => FolderCompletion(serde_json::from_str(data)?), 389 | EventType::FolderErrors => FolderErrors(serde_json::from_str(data)?), 390 | EventType::FolderRejected => FolderRejected(serde_json::from_str(data)?), 391 | EventType::FolderScanProgress => FolderScanProgress(serde_json::from_str(data)?), 392 | EventType::FolderSummary => FolderSummary(serde_json::from_str(data)?), 393 | EventType::ItemFinished => ItemFinished(serde_json::from_str(data)?), 394 | EventType::ItemStarted => ItemStarted(serde_json::from_str(data)?), 395 | EventType::ListenAddressesChanged => { 396 | ListenAddressesChanged(serde_json::from_str(data)?) 397 | } 398 | EventType::LocalChangeDetected => LocalChangeDetected(serde_json::from_str(data)?), 399 | EventType::LocalIndexUpdated => LocalIndexUpdated(serde_json::from_str(data)?), 400 | EventType::LoginAttempt => LoginAttempt(serde_json::from_str(data)?), 401 | EventType::RemoteChangeDetected => { 402 | RemoteChangeDetected(serde_json::from_str(data)?) 403 | } 404 | EventType::RemoteDownloadProgress => { 405 | RemoteDownloadProgress(serde_json::from_str(data)?) 406 | } 407 | EventType::RemoteIndexUpdated => RemoteIndexUpdated(serde_json::from_str(data)?), 408 | EventType::Starting => Starting(serde_json::from_str(data)?), 409 | EventType::StartupComplete => StartupComplete, 410 | EventType::StateChanged => StateChanged(serde_json::from_str(data)?), 411 | }, 412 | }) 413 | } 414 | } 415 | 416 | // 417 | // /rest/system/config response 418 | // 419 | 420 | #[derive(serde::Deserialize)] 421 | pub(crate) struct SystemConfig { 422 | pub folders: Vec, 423 | } 424 | 425 | #[derive(serde::Deserialize)] 426 | pub(crate) struct SystemConfigFolder { 427 | pub path: String, 428 | pub id: String, 429 | } 430 | 431 | // 432 | // /rest/system/status response 433 | // 434 | 435 | #[derive(serde::Deserialize)] 436 | pub(crate) struct SystemStatus { 437 | #[serde(rename = "myID")] 438 | pub my_id: String, 439 | } 440 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "aho-corasick" 22 | version = "1.1.3" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 25 | dependencies = [ 26 | "memchr", 27 | ] 28 | 29 | [[package]] 30 | name = "anyhow" 31 | version = "1.0.95" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" 34 | dependencies = [ 35 | "backtrace", 36 | ] 37 | 38 | [[package]] 39 | name = "backtrace" 40 | version = "0.3.74" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 43 | dependencies = [ 44 | "addr2line", 45 | "cfg-if", 46 | "libc", 47 | "miniz_oxide", 48 | "object", 49 | "rustc-demangle", 50 | "windows-targets 0.52.6", 51 | ] 52 | 53 | [[package]] 54 | name = "base64" 55 | version = "0.22.1" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 58 | 59 | [[package]] 60 | name = "bstr" 61 | version = "1.11.3" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" 64 | dependencies = [ 65 | "memchr", 66 | "serde", 67 | ] 68 | 69 | [[package]] 70 | name = "cfg-if" 71 | version = "1.0.0" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 74 | 75 | [[package]] 76 | name = "colored" 77 | version = "2.2.0" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" 80 | dependencies = [ 81 | "lazy_static", 82 | "windows-sys 0.59.0", 83 | ] 84 | 85 | [[package]] 86 | name = "displaydoc" 87 | version = "0.2.5" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 90 | dependencies = [ 91 | "proc-macro2", 92 | "quote", 93 | "syn", 94 | ] 95 | 96 | [[package]] 97 | name = "equivalent" 98 | version = "1.0.1" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 101 | 102 | [[package]] 103 | name = "form_urlencoded" 104 | version = "1.2.1" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 107 | dependencies = [ 108 | "percent-encoding", 109 | ] 110 | 111 | [[package]] 112 | name = "gimli" 113 | version = "0.31.1" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 116 | 117 | [[package]] 118 | name = "globset" 119 | version = "0.4.15" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "15f1ce686646e7f1e19bf7d5533fe443a45dbfb990e00629110797578b42fb19" 122 | dependencies = [ 123 | "aho-corasick", 124 | "bstr", 125 | "regex-automata", 126 | "regex-syntax", 127 | ] 128 | 129 | [[package]] 130 | name = "hashbrown" 131 | version = "0.15.2" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 134 | 135 | [[package]] 136 | name = "icu_collections" 137 | version = "1.5.0" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" 140 | dependencies = [ 141 | "displaydoc", 142 | "yoke", 143 | "zerofrom", 144 | "zerovec", 145 | ] 146 | 147 | [[package]] 148 | name = "icu_locid" 149 | version = "1.5.0" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" 152 | dependencies = [ 153 | "displaydoc", 154 | "litemap", 155 | "tinystr", 156 | "writeable", 157 | "zerovec", 158 | ] 159 | 160 | [[package]] 161 | name = "icu_locid_transform" 162 | version = "1.5.0" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" 165 | dependencies = [ 166 | "displaydoc", 167 | "icu_locid", 168 | "icu_locid_transform_data", 169 | "icu_provider", 170 | "tinystr", 171 | "zerovec", 172 | ] 173 | 174 | [[package]] 175 | name = "icu_locid_transform_data" 176 | version = "1.5.0" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" 179 | 180 | [[package]] 181 | name = "icu_normalizer" 182 | version = "1.5.0" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" 185 | dependencies = [ 186 | "displaydoc", 187 | "icu_collections", 188 | "icu_normalizer_data", 189 | "icu_properties", 190 | "icu_provider", 191 | "smallvec", 192 | "utf16_iter", 193 | "utf8_iter", 194 | "write16", 195 | "zerovec", 196 | ] 197 | 198 | [[package]] 199 | name = "icu_normalizer_data" 200 | version = "1.5.0" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" 203 | 204 | [[package]] 205 | name = "icu_properties" 206 | version = "1.5.1" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" 209 | dependencies = [ 210 | "displaydoc", 211 | "icu_collections", 212 | "icu_locid_transform", 213 | "icu_properties_data", 214 | "icu_provider", 215 | "tinystr", 216 | "zerovec", 217 | ] 218 | 219 | [[package]] 220 | name = "icu_properties_data" 221 | version = "1.5.0" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" 224 | 225 | [[package]] 226 | name = "icu_provider" 227 | version = "1.5.0" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" 230 | dependencies = [ 231 | "displaydoc", 232 | "icu_locid", 233 | "icu_provider_macros", 234 | "stable_deref_trait", 235 | "tinystr", 236 | "writeable", 237 | "yoke", 238 | "zerofrom", 239 | "zerovec", 240 | ] 241 | 242 | [[package]] 243 | name = "icu_provider_macros" 244 | version = "1.5.0" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" 247 | dependencies = [ 248 | "proc-macro2", 249 | "quote", 250 | "syn", 251 | ] 252 | 253 | [[package]] 254 | name = "idna" 255 | version = "1.0.3" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 258 | dependencies = [ 259 | "idna_adapter", 260 | "smallvec", 261 | "utf8_iter", 262 | ] 263 | 264 | [[package]] 265 | name = "idna_adapter" 266 | version = "1.2.0" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" 269 | dependencies = [ 270 | "icu_normalizer", 271 | "icu_properties", 272 | ] 273 | 274 | [[package]] 275 | name = "indexmap" 276 | version = "2.7.0" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" 279 | dependencies = [ 280 | "equivalent", 281 | "hashbrown", 282 | ] 283 | 284 | [[package]] 285 | name = "itoa" 286 | version = "1.0.14" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" 289 | 290 | [[package]] 291 | name = "lazy_static" 292 | version = "1.5.0" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 295 | 296 | [[package]] 297 | name = "libc" 298 | version = "0.2.169" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 301 | 302 | [[package]] 303 | name = "litemap" 304 | version = "0.7.4" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" 307 | 308 | [[package]] 309 | name = "log" 310 | version = "0.4.25" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" 313 | 314 | [[package]] 315 | name = "memchr" 316 | version = "2.7.4" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 319 | 320 | [[package]] 321 | name = "miniz_oxide" 322 | version = "0.8.3" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" 325 | dependencies = [ 326 | "adler2", 327 | ] 328 | 329 | [[package]] 330 | name = "object" 331 | version = "0.36.7" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 334 | dependencies = [ 335 | "memchr", 336 | ] 337 | 338 | [[package]] 339 | name = "once_cell" 340 | version = "1.20.2" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 343 | 344 | [[package]] 345 | name = "percent-encoding" 346 | version = "2.3.1" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 349 | 350 | [[package]] 351 | name = "proc-macro2" 352 | version = "1.0.93" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" 355 | dependencies = [ 356 | "unicode-ident", 357 | ] 358 | 359 | [[package]] 360 | name = "quick-xml" 361 | version = "0.37.2" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003" 364 | dependencies = [ 365 | "memchr", 366 | "serde", 367 | ] 368 | 369 | [[package]] 370 | name = "quote" 371 | version = "1.0.38" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 374 | dependencies = [ 375 | "proc-macro2", 376 | ] 377 | 378 | [[package]] 379 | name = "regex-automata" 380 | version = "0.4.9" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 383 | dependencies = [ 384 | "aho-corasick", 385 | "memchr", 386 | "regex-syntax", 387 | ] 388 | 389 | [[package]] 390 | name = "regex-syntax" 391 | version = "0.8.5" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 394 | 395 | [[package]] 396 | name = "rustc-demangle" 397 | version = "0.1.24" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 400 | 401 | [[package]] 402 | name = "ryu" 403 | version = "1.0.18" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 406 | 407 | [[package]] 408 | name = "serde" 409 | version = "1.0.217" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" 412 | dependencies = [ 413 | "serde_derive", 414 | ] 415 | 416 | [[package]] 417 | name = "serde_derive" 418 | version = "1.0.217" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" 421 | dependencies = [ 422 | "proc-macro2", 423 | "quote", 424 | "syn", 425 | ] 426 | 427 | [[package]] 428 | name = "serde_json" 429 | version = "1.0.135" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9" 432 | dependencies = [ 433 | "itoa", 434 | "memchr", 435 | "ryu", 436 | "serde", 437 | ] 438 | 439 | [[package]] 440 | name = "serde_spanned" 441 | version = "0.6.8" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" 444 | dependencies = [ 445 | "serde", 446 | ] 447 | 448 | [[package]] 449 | name = "shlex" 450 | version = "1.3.0" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 453 | 454 | [[package]] 455 | name = "simple-expand-tilde" 456 | version = "0.4.6" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "082c932448e7db87dc9222706818d95e0f4635a0ab293a748a577c2b4f01c197" 459 | dependencies = [ 460 | "simple-home-dir", 461 | ] 462 | 463 | [[package]] 464 | name = "simple-home-dir" 465 | version = "0.4.6" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "cd988c122a92006d24f347b3cbba965d5558e0ce94659fa7f0b6074e48641913" 468 | dependencies = [ 469 | "windows-sys 0.59.0", 470 | ] 471 | 472 | [[package]] 473 | name = "simple_logger" 474 | version = "5.0.0" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "e8c5dfa5e08767553704aa0ffd9d9794d527103c736aba9854773851fd7497eb" 477 | dependencies = [ 478 | "colored", 479 | "log", 480 | "windows-sys 0.48.0", 481 | ] 482 | 483 | [[package]] 484 | name = "smallvec" 485 | version = "1.13.2" 486 | source = "registry+https://github.com/rust-lang/crates.io-index" 487 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 488 | 489 | [[package]] 490 | name = "stable_deref_trait" 491 | version = "1.2.0" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 494 | 495 | [[package]] 496 | name = "stfed" 497 | version = "1.1.0" 498 | dependencies = [ 499 | "anyhow", 500 | "globset", 501 | "log", 502 | "quick-xml", 503 | "serde", 504 | "serde_json", 505 | "shlex", 506 | "simple-expand-tilde", 507 | "simple_logger", 508 | "thiserror", 509 | "toml", 510 | "ureq", 511 | "url", 512 | "xdg", 513 | ] 514 | 515 | [[package]] 516 | name = "syn" 517 | version = "2.0.96" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" 520 | dependencies = [ 521 | "proc-macro2", 522 | "quote", 523 | "unicode-ident", 524 | ] 525 | 526 | [[package]] 527 | name = "synstructure" 528 | version = "0.13.1" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" 531 | dependencies = [ 532 | "proc-macro2", 533 | "quote", 534 | "syn", 535 | ] 536 | 537 | [[package]] 538 | name = "thiserror" 539 | version = "2.0.11" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" 542 | dependencies = [ 543 | "thiserror-impl", 544 | ] 545 | 546 | [[package]] 547 | name = "thiserror-impl" 548 | version = "2.0.11" 549 | source = "registry+https://github.com/rust-lang/crates.io-index" 550 | checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" 551 | dependencies = [ 552 | "proc-macro2", 553 | "quote", 554 | "syn", 555 | ] 556 | 557 | [[package]] 558 | name = "tinystr" 559 | version = "0.7.6" 560 | source = "registry+https://github.com/rust-lang/crates.io-index" 561 | checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" 562 | dependencies = [ 563 | "displaydoc", 564 | "zerovec", 565 | ] 566 | 567 | [[package]] 568 | name = "toml" 569 | version = "0.8.19" 570 | source = "registry+https://github.com/rust-lang/crates.io-index" 571 | checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" 572 | dependencies = [ 573 | "serde", 574 | "serde_spanned", 575 | "toml_datetime", 576 | "toml_edit", 577 | ] 578 | 579 | [[package]] 580 | name = "toml_datetime" 581 | version = "0.6.8" 582 | source = "registry+https://github.com/rust-lang/crates.io-index" 583 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" 584 | dependencies = [ 585 | "serde", 586 | ] 587 | 588 | [[package]] 589 | name = "toml_edit" 590 | version = "0.22.22" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" 593 | dependencies = [ 594 | "indexmap", 595 | "serde", 596 | "serde_spanned", 597 | "toml_datetime", 598 | "winnow", 599 | ] 600 | 601 | [[package]] 602 | name = "unicode-ident" 603 | version = "1.0.14" 604 | source = "registry+https://github.com/rust-lang/crates.io-index" 605 | checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" 606 | 607 | [[package]] 608 | name = "ureq" 609 | version = "2.12.1" 610 | source = "registry+https://github.com/rust-lang/crates.io-index" 611 | checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" 612 | dependencies = [ 613 | "base64", 614 | "log", 615 | "once_cell", 616 | "url", 617 | ] 618 | 619 | [[package]] 620 | name = "url" 621 | version = "2.5.4" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 624 | dependencies = [ 625 | "form_urlencoded", 626 | "idna", 627 | "percent-encoding", 628 | "serde", 629 | ] 630 | 631 | [[package]] 632 | name = "utf16_iter" 633 | version = "1.0.5" 634 | source = "registry+https://github.com/rust-lang/crates.io-index" 635 | checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" 636 | 637 | [[package]] 638 | name = "utf8_iter" 639 | version = "1.0.4" 640 | source = "registry+https://github.com/rust-lang/crates.io-index" 641 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 642 | 643 | [[package]] 644 | name = "windows-sys" 645 | version = "0.48.0" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 648 | dependencies = [ 649 | "windows-targets 0.48.5", 650 | ] 651 | 652 | [[package]] 653 | name = "windows-sys" 654 | version = "0.59.0" 655 | source = "registry+https://github.com/rust-lang/crates.io-index" 656 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 657 | dependencies = [ 658 | "windows-targets 0.52.6", 659 | ] 660 | 661 | [[package]] 662 | name = "windows-targets" 663 | version = "0.48.5" 664 | source = "registry+https://github.com/rust-lang/crates.io-index" 665 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 666 | dependencies = [ 667 | "windows_aarch64_gnullvm 0.48.5", 668 | "windows_aarch64_msvc 0.48.5", 669 | "windows_i686_gnu 0.48.5", 670 | "windows_i686_msvc 0.48.5", 671 | "windows_x86_64_gnu 0.48.5", 672 | "windows_x86_64_gnullvm 0.48.5", 673 | "windows_x86_64_msvc 0.48.5", 674 | ] 675 | 676 | [[package]] 677 | name = "windows-targets" 678 | version = "0.52.6" 679 | source = "registry+https://github.com/rust-lang/crates.io-index" 680 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 681 | dependencies = [ 682 | "windows_aarch64_gnullvm 0.52.6", 683 | "windows_aarch64_msvc 0.52.6", 684 | "windows_i686_gnu 0.52.6", 685 | "windows_i686_gnullvm", 686 | "windows_i686_msvc 0.52.6", 687 | "windows_x86_64_gnu 0.52.6", 688 | "windows_x86_64_gnullvm 0.52.6", 689 | "windows_x86_64_msvc 0.52.6", 690 | ] 691 | 692 | [[package]] 693 | name = "windows_aarch64_gnullvm" 694 | version = "0.48.5" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 697 | 698 | [[package]] 699 | name = "windows_aarch64_gnullvm" 700 | version = "0.52.6" 701 | source = "registry+https://github.com/rust-lang/crates.io-index" 702 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 703 | 704 | [[package]] 705 | name = "windows_aarch64_msvc" 706 | version = "0.48.5" 707 | source = "registry+https://github.com/rust-lang/crates.io-index" 708 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 709 | 710 | [[package]] 711 | name = "windows_aarch64_msvc" 712 | version = "0.52.6" 713 | source = "registry+https://github.com/rust-lang/crates.io-index" 714 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 715 | 716 | [[package]] 717 | name = "windows_i686_gnu" 718 | version = "0.48.5" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 721 | 722 | [[package]] 723 | name = "windows_i686_gnu" 724 | version = "0.52.6" 725 | source = "registry+https://github.com/rust-lang/crates.io-index" 726 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 727 | 728 | [[package]] 729 | name = "windows_i686_gnullvm" 730 | version = "0.52.6" 731 | source = "registry+https://github.com/rust-lang/crates.io-index" 732 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 733 | 734 | [[package]] 735 | name = "windows_i686_msvc" 736 | version = "0.48.5" 737 | source = "registry+https://github.com/rust-lang/crates.io-index" 738 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 739 | 740 | [[package]] 741 | name = "windows_i686_msvc" 742 | version = "0.52.6" 743 | source = "registry+https://github.com/rust-lang/crates.io-index" 744 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 745 | 746 | [[package]] 747 | name = "windows_x86_64_gnu" 748 | version = "0.48.5" 749 | source = "registry+https://github.com/rust-lang/crates.io-index" 750 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 751 | 752 | [[package]] 753 | name = "windows_x86_64_gnu" 754 | version = "0.52.6" 755 | source = "registry+https://github.com/rust-lang/crates.io-index" 756 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 757 | 758 | [[package]] 759 | name = "windows_x86_64_gnullvm" 760 | version = "0.48.5" 761 | source = "registry+https://github.com/rust-lang/crates.io-index" 762 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 763 | 764 | [[package]] 765 | name = "windows_x86_64_gnullvm" 766 | version = "0.52.6" 767 | source = "registry+https://github.com/rust-lang/crates.io-index" 768 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 769 | 770 | [[package]] 771 | name = "windows_x86_64_msvc" 772 | version = "0.48.5" 773 | source = "registry+https://github.com/rust-lang/crates.io-index" 774 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 775 | 776 | [[package]] 777 | name = "windows_x86_64_msvc" 778 | version = "0.52.6" 779 | source = "registry+https://github.com/rust-lang/crates.io-index" 780 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 781 | 782 | [[package]] 783 | name = "winnow" 784 | version = "0.6.24" 785 | source = "registry+https://github.com/rust-lang/crates.io-index" 786 | checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a" 787 | dependencies = [ 788 | "memchr", 789 | ] 790 | 791 | [[package]] 792 | name = "write16" 793 | version = "1.0.0" 794 | source = "registry+https://github.com/rust-lang/crates.io-index" 795 | checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" 796 | 797 | [[package]] 798 | name = "writeable" 799 | version = "0.5.5" 800 | source = "registry+https://github.com/rust-lang/crates.io-index" 801 | checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" 802 | 803 | [[package]] 804 | name = "xdg" 805 | version = "2.5.2" 806 | source = "registry+https://github.com/rust-lang/crates.io-index" 807 | checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" 808 | 809 | [[package]] 810 | name = "yoke" 811 | version = "0.7.5" 812 | source = "registry+https://github.com/rust-lang/crates.io-index" 813 | checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" 814 | dependencies = [ 815 | "serde", 816 | "stable_deref_trait", 817 | "yoke-derive", 818 | "zerofrom", 819 | ] 820 | 821 | [[package]] 822 | name = "yoke-derive" 823 | version = "0.7.5" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" 826 | dependencies = [ 827 | "proc-macro2", 828 | "quote", 829 | "syn", 830 | "synstructure", 831 | ] 832 | 833 | [[package]] 834 | name = "zerofrom" 835 | version = "0.1.5" 836 | source = "registry+https://github.com/rust-lang/crates.io-index" 837 | checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" 838 | dependencies = [ 839 | "zerofrom-derive", 840 | ] 841 | 842 | [[package]] 843 | name = "zerofrom-derive" 844 | version = "0.1.5" 845 | source = "registry+https://github.com/rust-lang/crates.io-index" 846 | checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" 847 | dependencies = [ 848 | "proc-macro2", 849 | "quote", 850 | "syn", 851 | "synstructure", 852 | ] 853 | 854 | [[package]] 855 | name = "zerovec" 856 | version = "0.10.4" 857 | source = "registry+https://github.com/rust-lang/crates.io-index" 858 | checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" 859 | dependencies = [ 860 | "yoke", 861 | "zerofrom", 862 | "zerovec-derive", 863 | ] 864 | 865 | [[package]] 866 | name = "zerovec-derive" 867 | version = "0.10.3" 868 | source = "registry+https://github.com/rust-lang/crates.io-index" 869 | checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" 870 | dependencies = [ 871 | "proc-macro2", 872 | "quote", 873 | "syn", 874 | ] 875 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------