├── .cirrus.yml ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── ci.yml ├── .gitignore ├── .typos.toml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Cargo.toml ├── README.md ├── UPGRADING_V4_TO_V5.md ├── clippy.toml ├── deny.toml ├── examples ├── Cargo.toml ├── README.md ├── async_monitor.rs ├── debouncer_full.rs ├── debouncer_mini.rs ├── debouncer_mini_custom.rs ├── hot_reload_tide │ ├── .gitignore │ ├── Cargo.toml │ ├── README.md │ ├── config.json │ ├── src │ │ ├── lib.rs │ │ ├── main.rs │ │ └── messages.rs │ └── tests │ │ ├── messages_test.rs │ │ └── messages_test_config.json ├── monitor_debounced.rs ├── monitor_raw.rs ├── poll_sysfs.rs ├── pollwatcher_manual.rs ├── pollwatcher_scan.rs └── watcher_kind.rs ├── file-id ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── bin │ └── file_id.rs └── src │ └── lib.rs ├── notify-debouncer-full ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── src │ ├── cache.rs │ ├── lib.rs │ ├── testing.rs │ └── time.rs └── test_cases │ ├── add_create_dir_event_twice.hjson │ ├── add_create_event.hjson │ ├── add_create_event_after_remove_event.hjson │ ├── add_errors.hjson │ ├── add_modify_content_event_after_create_event.hjson │ ├── add_remove_event.hjson │ ├── add_remove_event_after_create_and_modify_event.hjson │ ├── add_remove_event_after_create_event.hjson │ ├── add_remove_event_after_modify_event.hjson │ ├── add_remove_parent_event_after_remove_child_event.hjson │ ├── add_rename_both_event.hjson │ ├── add_rename_from_and_to_event.hjson │ ├── add_rename_from_and_to_event_after_create.hjson │ ├── add_rename_from_and_to_event_after_modify_content.hjson │ ├── add_rename_from_and_to_event_after_rename.hjson │ ├── add_rename_from_and_to_event_override_created.hjson │ ├── add_rename_from_and_to_event_override_modified.hjson │ ├── add_rename_from_and_to_event_override_removed.hjson │ ├── add_rename_from_and_to_event_with_different_file_ids.hjson │ ├── add_rename_from_and_to_event_with_different_tracker.hjson │ ├── add_rename_from_and_to_event_with_file_ids.hjson │ ├── add_rename_from_event.hjson │ ├── add_rename_from_event_after_create_and_modify_event.hjson │ ├── add_rename_from_event_after_create_event.hjson │ ├── add_rename_from_event_after_modify_event.hjson │ ├── add_rename_from_event_after_rename_from_event.hjson │ ├── add_rename_to_dir_event.hjson │ ├── add_rename_to_event.hjson │ ├── emit_close_events_only_once.hjson │ ├── emit_continuous_modify_content_events.hjson │ ├── emit_events_in_chronological_order.hjson │ ├── emit_events_with_a_prepended_rename_event.hjson │ ├── emit_modify_event_after_close_event.hjson │ ├── emit_needs_rescan_event.hjson │ ├── read_file_id_without_create_event.hjson │ ├── sort_events_chronologically.hjson │ └── sort_events_with_reordering.hjson ├── notify-debouncer-mini ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md └── src │ └── lib.rs ├── notify-types ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT └── src │ ├── debouncer_full.rs │ ├── debouncer_mini.rs │ ├── event.rs │ ├── lib.rs │ └── snapshots │ ├── notify_types__event__tests__access-any.snap │ ├── notify_types__event__tests__access-close-any.snap │ ├── notify_types__event__tests__access-close-execute.snap │ ├── notify_types__event__tests__access-close-other.snap │ ├── notify_types__event__tests__access-close-read.snap │ ├── notify_types__event__tests__access-close-write.snap │ ├── notify_types__event__tests__access-open-any.snap │ ├── notify_types__event__tests__access-open-execute.snap │ ├── notify_types__event__tests__access-open-other.snap │ ├── notify_types__event__tests__access-open-read.snap │ ├── notify_types__event__tests__access-open-write.snap │ ├── notify_types__event__tests__access-other.snap │ ├── notify_types__event__tests__access-read.snap │ ├── notify_types__event__tests__any.snap │ ├── notify_types__event__tests__create-any.snap │ ├── notify_types__event__tests__create-file.snap │ ├── notify_types__event__tests__create-folder.snap │ ├── notify_types__event__tests__create-other.snap │ ├── notify_types__event__tests__modify-any.snap │ ├── notify_types__event__tests__modify-data-any.snap │ ├── notify_types__event__tests__modify-data-content.snap │ ├── notify_types__event__tests__modify-data-other.snap │ ├── notify_types__event__tests__modify-data-size.snap │ ├── notify_types__event__tests__modify-metadata-accesstime.snap │ ├── notify_types__event__tests__modify-metadata-any.snap │ ├── notify_types__event__tests__modify-metadata-extended.snap │ ├── notify_types__event__tests__modify-metadata-other.snap │ ├── notify_types__event__tests__modify-metadata-ownership.snap │ ├── notify_types__event__tests__modify-metadata-permissions.snap │ ├── notify_types__event__tests__modify-metadata-writetime.snap │ ├── notify_types__event__tests__modify-name-any.snap │ ├── notify_types__event__tests__modify-name-both.snap │ ├── notify_types__event__tests__modify-name-from.snap │ ├── notify_types__event__tests__modify-name-other.snap │ ├── notify_types__event__tests__modify-name-to.snap │ ├── notify_types__event__tests__modify-other.snap │ ├── notify_types__event__tests__other.snap │ ├── notify_types__event__tests__remove-any.snap │ ├── notify_types__event__tests__remove-file.snap │ ├── notify_types__event__tests__remove-folder.snap │ ├── notify_types__event__tests__remove-other.snap │ └── notify_types__event__tests__serialize_event_with_attrs.snap ├── notify ├── .gitignore ├── Cargo.toml ├── LICENSE-CC0 └── src │ ├── config.rs │ ├── error.rs │ ├── fsevent.rs │ ├── inotify.rs │ ├── kqueue.rs │ ├── lib.rs │ ├── null.rs │ ├── poll.rs │ └── windows.rs ├── release_checklist.md ├── rust-toolchain.toml └── tests ├── and-retry ├── poll-watcher-hashing.rs ├── race-with-remove-dir.rs └── serialise-events.rs /.cirrus.yml: -------------------------------------------------------------------------------- 1 | freebsd_instance: 2 | image_family: freebsd-14-2 3 | 4 | env: 5 | CARGO_TERM_COLOR: always 6 | RUST_BACKTRACE: 1 7 | 8 | task: 9 | name: FreeBSD 10 | matrix: 11 | - name: FreeBSD 14.0 - Rust stable 12 | env: 13 | RUST_VERSION: stable 14 | - name: FreeBSD 14.0 - Rust nightly 15 | env: 16 | RUST_VERSION: nightly 17 | - name: FreeBSD 14.0 - Rust 1.77.2 (MSRV) 18 | env: 19 | RUST_VERSION: 1.77.2 20 | 21 | setup_script: 22 | - rm -f rust-toolchain.toml 23 | - curl https://sh.rustup.rs -sSf | sh -s -- -y --profile=minimal --default-toolchain ${RUST_VERSION} 24 | - . $HOME/.cargo/env 25 | - cargo --version 26 | - rustc --version 27 | 28 | cargo_cache: 29 | folder: $HOME/.cargo/registry 30 | fingerprint_script: cat Cargo.lock || echo "No Cargo.lock" 31 | 32 | build_script: 33 | - . $HOME/.cargo/env 34 | - cargo build --verbose 35 | 36 | build_examples_script: 37 | - . $HOME/.cargo/env 38 | - cargo build --examples --verbose 39 | 40 | test_script: 41 | - . $HOME/.cargo/env 42 | - cargo test --verbose 43 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.toml] 12 | indent_size = 2 13 | 14 | [*.yml] 15 | indent_size = 2 16 | 17 | [*.md] 18 | indent_size = 3 19 | 20 | [*.sh] 21 | indent_size = 2 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | #### System details 4 | 5 | 6 | 7 | - OS/Platform name and version: 8 | - Rust version (if building from source): `rustc --version`: 9 | - Notify version (or commit hash if building from git): 10 | 11 | 12 | 13 | - If you're coming from a project that makes use of Notify, what it is, and a link to the downstream issue if there is one: 14 | - Filesystem type and options: 15 | - On Linux: Kernel version: 16 | - On Windows: version and if you're running under Windows, Cygwin (unsupported), Linux Subsystem: 17 | - If you're running as a privileged user (root, System): 18 | - If you're running in a container, details on the runtime and overlay: 19 | - If you're running in a VM, details on the hypervisor: 20 | 21 | 22 | 23 | 24 | #### What you did (as detailed as you can) 25 | 26 | 27 | 28 | #### What you expected 29 | 30 | 31 | 32 | #### What happened 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | workflow_dispatch: 9 | 10 | env: 11 | CARGO_TERM_COLOR: always 12 | RUST_BACKTRACE: 1 13 | 14 | jobs: 15 | # Code quality checks 16 | quality: 17 | name: Quality Checks 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - name: Install Rust 23 | uses: dtolnay/rust-toolchain@stable 24 | with: 25 | components: rustfmt, clippy 26 | 27 | - name: Cache dependencies 28 | uses: Swatinem/rust-cache@v2 29 | 30 | - name: Check formatting 31 | run: cargo +stable fmt --all -- --check 32 | 33 | - name: Clippy 34 | run: cargo +stable clippy --all-targets --all-features -- -D warnings 35 | 36 | - name: Install typos-cli 37 | uses: taiki-e/install-action@v2 38 | with: 39 | tool: typos-cli 40 | 41 | - name: Check for typos 42 | run: typos 43 | 44 | - name: Install cargo-deny 45 | uses: taiki-e/install-action@v2 46 | with: 47 | tool: cargo-deny 48 | 49 | - name: Run cargo-deny 50 | run: cargo deny check 51 | 52 | - name: Install cargo-machete 53 | uses: taiki-e/install-action@v2 54 | with: 55 | tool: cargo-machete 56 | 57 | - name: Run cargo-machete 58 | run: cargo machete 59 | 60 | # Test on multiple platforms with different Rust versions 61 | test: 62 | name: Test ${{ matrix.os }} - ${{ matrix.rust }} ${{ matrix.features != '' && format('({0})', matrix.features) || '' }} 63 | runs-on: ${{ matrix.os }} 64 | needs: quality 65 | strategy: 66 | fail-fast: false 67 | matrix: 68 | os: [ubuntu-latest, windows-latest] 69 | rust: [stable, nightly, 1.77.2] 70 | features: [""] 71 | include: 72 | # MacOS with fsevent 73 | - os: macos-latest 74 | rust: stable 75 | features: "--no-default-features --features macos_fsevent" 76 | - os: macos-latest 77 | rust: nightly 78 | features: "--no-default-features --features macos_fsevent" 79 | - os: macos-latest 80 | rust: 1.77.2 81 | features: "--no-default-features --features macos_fsevent" 82 | # MacOS with kqueue 83 | - os: macos-latest 84 | rust: stable 85 | features: "--no-default-features --features macos_kqueue" 86 | - os: macos-latest 87 | rust: nightly 88 | features: "--no-default-features --features macos_kqueue" 89 | - os: macos-latest 90 | rust: 1.77.2 91 | features: "--no-default-features --features macos_kqueue" 92 | 93 | steps: 94 | - uses: actions/checkout@v4 95 | 96 | - name: Remove rust-toolchain.toml (Unix) 97 | if: runner.os != 'Windows' 98 | run: rm -f rust-toolchain.toml 99 | 100 | - name: Remove rust-toolchain.toml (Windows) 101 | if: runner.os == 'Windows' 102 | run: | 103 | if (Test-Path rust-toolchain.toml) { 104 | Remove-Item -Path rust-toolchain.toml 105 | } 106 | 107 | - name: Install Rust ${{ matrix.rust }} 108 | uses: dtolnay/rust-toolchain@master 109 | with: 110 | toolchain: ${{ matrix.rust }} 111 | 112 | - name: Cache dependencies 113 | uses: Swatinem/rust-cache@v2 114 | 115 | - name: Build 116 | run: cargo build --verbose ${{ matrix.features }} 117 | 118 | - name: Build examples 119 | run: cargo build --examples --verbose ${{ matrix.features }} 120 | 121 | - name: Run tests 122 | run: cargo test --verbose ${{ matrix.features }} 123 | 124 | # Android cross-compilation 125 | android: 126 | name: Android 127 | runs-on: ubuntu-latest 128 | needs: quality 129 | steps: 130 | - uses: actions/checkout@v4 131 | 132 | - name: Remove rust-toolchain.toml 133 | run: rm -f rust-toolchain.toml 134 | 135 | - name: Install Rust 136 | uses: dtolnay/rust-toolchain@stable 137 | with: 138 | targets: armv7-linux-androideabi, aarch64-linux-android 139 | 140 | - name: Cache dependencies 141 | uses: Swatinem/rust-cache@v2 142 | 143 | - name: Install cargo-ndk 144 | uses: taiki-e/install-action@v2 145 | with: 146 | tool: cargo-ndk 147 | 148 | - name: Build for Android (arm64) 149 | run: cargo ndk --target aarch64-linux-android build --verbose 150 | 151 | - name: Build for Android (arm) 152 | run: cargo ndk --target armv7-linux-androideabi build --verbose 153 | 154 | # # WebAssembly System Interface (WASI) 155 | # wasi: 156 | # name: WASI 157 | # runs-on: ubuntu-latest 158 | # needs: quality 159 | # steps: 160 | # - uses: actions/checkout@v4 161 | 162 | # - name: Remove rust-toolchain.toml 163 | # run: rm -f rust-toolchain.toml 164 | 165 | # - name: Install Rust 166 | # uses: dtolnay/rust-toolchain@nightly 167 | # with: 168 | # targets: wasm32-wasip2 169 | 170 | # - name: Cache dependencies 171 | # uses: Swatinem/rust-cache@v2 172 | 173 | # - name: Build for WASI 174 | # run: cargo build --target wasm32-wasip2 --verbose 175 | 176 | # - name: Build examples for WASI 177 | # run: cargo build --examples --target wasm32-wasip2 --verbose 178 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | .*.sw* 4 | tests/last-fails 5 | tests/last-run.log 6 | .cargo 7 | -------------------------------------------------------------------------------- /.typos.toml: -------------------------------------------------------------------------------- 1 | # See the configuration reference at 2 | # https://github.com/crate-ci/typos/blob/master/docs/reference.md 3 | 4 | # Corrections take the form of a key/value pair. The key is the incorrect word 5 | # and the value is the correct word. If the key and value are the same, the 6 | # word is treated as always correct. If the value is an empty string, the word 7 | # is treated as always incorrect. 8 | 9 | # Match Identifier - Case Sensitive 10 | [default.extend-identifiers] 11 | ecd686ba = "ecd686ba" # In the CHANGELOG.md 12 | waitres = "waitres" 13 | 14 | # Match Inside a Word - Case Insensitive 15 | [default.extend-words] 16 | 17 | [files] 18 | # Include .github, .cargo, etc. 19 | ignore-hidden = false 20 | # /.git isn't in .gitignore, because git never tracks it. 21 | # Typos doesn't know that, though. 22 | extend-exclude = ["/.git"] 23 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project lead at felix@passcod.name. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "notify", 5 | "notify-types", 6 | "notify-debouncer-mini", 7 | "notify-debouncer-full", 8 | "file-id", 9 | "examples", 10 | ] 11 | exclude = [ 12 | "examples/hot_reload_tide", # excluded until https://github.com/rustsec/rustsec/issues/501 is resolved 13 | ] 14 | 15 | [workspace.package] 16 | rust-version = "1.77" 17 | homepage = "https://github.com/notify-rs/notify" 18 | repository = "https://github.com/notify-rs/notify.git" 19 | edition = "2021" 20 | 21 | [workspace.dependencies] 22 | bitflags = "2.7.0" 23 | crossbeam-channel = "0.5.0" 24 | flume = "0.11.1" 25 | deser-hjson = "2.2.4" 26 | env_logger = "0.11.2" 27 | file-id = { version = "0.2.2", path = "file-id" } 28 | filetime = "0.2.22" 29 | fsevent-sys = "4.0.0" 30 | futures = "0.3.30" 31 | inotify = { version = "0.11.0", default-features = false } 32 | insta = "1.34.0" 33 | kqueue = "1.1.1" 34 | libc = "0.2.4" 35 | log = "0.4.17" 36 | mio = { version = "1.0", features = ["os-ext"] } 37 | web-time = "1.1.0" 38 | nix = "0.29.0" 39 | notify = { version = "8.0.0", path = "notify" } 40 | notify-debouncer-full = { version = "0.5.0", path = "notify-debouncer-full" } 41 | notify-debouncer-mini = { version = "0.6.0", path = "notify-debouncer-mini" } 42 | notify-types = { version = "2.0.0", path = "notify-types" } 43 | pretty_assertions = "1.3.0" 44 | rand = "0.8.5" 45 | rstest = "0.24.0" 46 | serde = { version = "1.0.89", features = ["derive"] } 47 | serde_json = "1.0.39" 48 | tempfile = "3.10.0" 49 | trash = "5.2.2" 50 | walkdir = "2.4.0" 51 | windows-sys = "0.59.0" 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Notify 2 | 3 | [![» Crate](https://flat.badgen.net/crates/v/notify)][crate] 4 | [![» Docs](https://flat.badgen.net/badge/api/docs.rs/df3600)][notify-docs] 5 | [![» CI](https://flat.badgen.net/github/checks/notify-rs/notify/main)][build] 6 | [![» Downloads](https://flat.badgen.net/crates/d/notify)][crate] 7 | [![» Conduct](https://flat.badgen.net/badge/contributor/covenant/5e0d73)][coc] 8 | [![» Public Domain](https://flat.badgen.net/badge/license/CC0-1.0/purple)][cc0] 9 | 10 | _Cross-platform filesystem notification library for Rust._ 11 | 12 | 13 | - [Notify Documentation][notify-docs] 14 | - [Notify Types Documentation][notify-types-docs] 15 | - [Mini Debouncer Documentation][debouncer-mini-docs] 16 | - [Full Debouncer Documentation][debouncer-full-docs] 17 | - [File ID][file-id-docs] 18 | - [Examples][examples] 19 | - [Changelog][changelog] 20 | - [Upgrading notify from v4](UPGRADING_V4_TO_V5.md) 21 | - Minimum supported Rust version: **1.77** 22 | 23 | As used by: [alacritty], [cargo watch], [cobalt], [deno], [docket], [mdBook], 24 | [rust-analyzer], [watchexec], [watchfiles], [xi-editor], 25 | and others. 26 | 27 | (Looking for desktop notifications instead? Have a look at [notify-rust] or 28 | [alert-after]!) 29 | 30 | ## Platforms 31 | 32 | - Linux / Android: inotify 33 | - macOS: FSEvents or kqueue, see features 34 | - Windows: ReadDirectoryChangesW 35 | - iOS / FreeBSD / NetBSD / OpenBSD / DragonflyBSD: kqueue 36 | - All platforms: polling 37 | 38 | ## License 39 | 40 | notify is licensed under the [CC Zero 1.0][cc0]. 41 | notify-types is licensed under the [MIT] or [Apache-2.0][apache] license. 42 | notify-debouncer-mini is licensed under the [MIT] or [Apache-2.0][apache] license. 43 | notify-debouncer-full is licensed under the [MIT] or [Apache-2.0][apache] license. 44 | file-id is licensed under the [MIT] or [Apache-2.0][apache] license. 45 | 46 | ## Origins 47 | 48 | Inspired by Go's [fsnotify] and Node.js's [Chokidar], born out of need for 49 | [cargo watch], and general frustration at the non-existence of C/Rust 50 | cross-platform notify libraries. 51 | 52 | Originally created by [Félix Saparelli] and awesome [contributors]. 53 | 54 | [Chokidar]: https://github.com/paulmillr/chokidar 55 | [FileSystemEventSecurity]: https://developer.apple.com/library/mac/documentation/Darwin/Conceptual/FSEvents_ProgGuide/FileSystemEventSecurity/FileSystemEventSecurity.html 56 | [debouncer-full-docs]: https://docs.rs/notify-debouncer-full/latest/notify_debouncer_full/ 57 | [debouncer-mini-docs]: https://docs.rs/notify-debouncer-mini/latest/notify_debouncer_mini/ 58 | [Félix Saparelli]: https://passcod.name 59 | [alacritty]: https://github.com/jwilm/alacritty 60 | [alert-after]: https://github.com/frewsxcv/alert-after 61 | [build]: https://github.com/notify-rs/notify/actions 62 | [cargo watch]: https://github.com/passcod/cargo-watch 63 | [cc0]: ./notify/LICENSE-CC0 64 | [MIT]: ./file-id/LICENSE-MIT 65 | [apache]: ./file-id/LICENSE-APACHE 66 | [changelog]: ./CHANGELOG.md 67 | [cobalt]: https://github.com/cobalt-org/cobalt.rs 68 | [coc]: http://contributor-covenant.org/version/1/4/ 69 | [contributors]: https://github.com/notify-rs/notify/graphs/contributors 70 | [crate]: https://crates.io/crates/notify 71 | [deno]: https://github.com/denoland/deno 72 | [docket]: https://iwillspeak.github.io/docket/ 73 | [notify-docs]: https://docs.rs/notify/latest/notify/ 74 | [notify-types-docs]: https://docs.rs/notify-types/latest/notify-types/ 75 | [file-id-docs]: https://docs.rs/file-id/latest/file_id/ 76 | [fsnotify]: https://github.com/fsnotify/fsnotify 77 | [handlebars-iron]: https://github.com/sunng87/handlebars-iron 78 | [hotwatch]: https://github.com/francesca64/hotwatch 79 | [mdBook]: https://github.com/rust-lang-nursery/mdBook 80 | [notify-rust]: https://github.com/hoodie/notify-rust 81 | [rust-analyzer]: https://github.com/rust-analyzer/rust-analyzer 82 | [serde]: https://serde.rs/ 83 | [watchexec]: https://github.com/mattgreen/watchexec 84 | [wiki]: https://github.com/notify-rs/notify/wiki 85 | [xi-editor]: https://xi-editor.io/ 86 | [watchfiles]: https://watchfiles.helpmanual.io/ 87 | [examples]: examples/ 88 | -------------------------------------------------------------------------------- /UPGRADING_V4_TO_V5.md: -------------------------------------------------------------------------------- 1 | # Upgrading from notify v4 to v5 2 | 3 | This guide documents changes between v4 and v5 for upgrading existing code. 4 | 5 | ## (Precise) Events & Debouncing 6 | 7 | Notify v5 only contains precise events. Debouncing is done by a separate crate [notify-debouncer-mini]. If you relied on `RawEvent`, this got replaced by `Event`. 8 | 9 | The old `DebouncedEvent` is completely removed. [notify-debouncer-mini] only reports an `Any` like event (named `DebouncedEvent` too), as relying on specific kinds (Write/Create/Remove) is very platform specific and [can't](https://github.com/notify-rs/notify/issues/261) [be](https://github.com/notify-rs/notify/issues/187) [guaranteed](https://github.com/notify-rs/notify/issues/272) to work, relying on a lot of assumptions. In most cases you should check anyway what exactly the state of files is, or probably re-run your application code, not relying on which event happened. 10 | 11 | If you've used the debounced API, which was the default in v4 without `raw_`, please see [here](https://github.com/notify-rs/notify/blob/main/examples/debounced.rs) for an example using the new crate. 12 | 13 | For an example of precise events you can look [here](https://github.com/notify-rs/notify/blob/main/examples/monitor_raw.rs). 14 | 15 | Watchers now accept the `EventHandler` trait for event handling, allowing for callbacks and foreign channels. 16 | 17 | ## Watcher Config & Creation 18 | 19 | All watchers only expose the `Watcher` trait, which takes an `EventHandler` and a `Config`, the latter being used to possibly initialize things that can only be specified before running the watcher. One Example would be the `compare_contents` from `PollWatcher`. 20 | 21 | ## Crate Features 22 | 23 | Notify v5 by default uses `crossbeam-channel` internally. You can disable this as documented in the crate, this may be required for tokio users. 24 | 25 | For macOS the kqueue backend can now be used alternatively by using the `macos_kqueue` feature. 26 | 27 | ## Platforms 28 | 29 | Platform support in v5 now includes BSD and kqueue on macos in addition to fsevent. 30 | 31 | [notify-debouncer-mini]: https://crates.io/crates/notify-debouncer-mini -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | doc-valid-idents = ["GitHub", "FSEvents", "ReadDirectoryChangesW", "FileSystemEventSecurity", "iOS", "macOS"] 2 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | [licenses] 2 | allow = [ 3 | "MIT", 4 | "Apache-2.0", 5 | "ISC", 6 | "CC0-1.0", 7 | ] 8 | -------------------------------------------------------------------------------- /examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "examples" 3 | version = "0.0.0" 4 | publish = false 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | 8 | [dev-dependencies] 9 | notify = { workspace = true } 10 | notify-debouncer-mini = { workspace = true } 11 | notify-debouncer-full = { workspace = true } 12 | futures = { workspace = true } 13 | tempfile = { workspace = true } 14 | log = { workspace = true } 15 | env_logger = { workspace = true } 16 | 17 | [[example]] 18 | name = "async_monitor" 19 | path = "async_monitor.rs" 20 | 21 | [[example]] 22 | name = "monitor_raw" 23 | path = "monitor_raw.rs" 24 | 25 | [[example]] 26 | name = "monitor_debounced" 27 | path = "monitor_debounced.rs" 28 | 29 | [[example]] 30 | name = "debouncer_mini" 31 | path = "debouncer_mini.rs" 32 | 33 | [[example]] 34 | name = "debouncer_mini_custom" 35 | path = "debouncer_mini_custom.rs" 36 | 37 | [[example]] 38 | name = "debouncer_full" 39 | path = "debouncer_full.rs" 40 | 41 | [[example]] 42 | name = "poll_sysfs" 43 | path = "poll_sysfs.rs" 44 | 45 | [[example]] 46 | name = "watcher_kind" 47 | path = "watcher_kind.rs" 48 | 49 | [[example]] 50 | name = "pollwatcher_scan" 51 | path = "pollwatcher_scan.rs" 52 | 53 | [[example]] 54 | name = "pollwatcher_manual" 55 | path = "pollwatcher_manual.rs" 56 | 57 | # specifically in its own sub folder 58 | # to prevent cargo audit from complaining 59 | #[[example]] 60 | #name = "hot_reload_tide" 61 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | Examples for notify and the debouncers. 2 | 3 | ### Notify 4 | 5 | - **monitor_raw** basic example for using notify 6 | - **async_monitor** example for using `futures::channel` to receive events in async code 7 | - **poll_sysfs** example for observing linux `/sys` events using PollWatcher and the hashing mode 8 | - **watcher_kind** example for detecting the kind of watcher used and running specific configurations 9 | - **hot_reload_tide** large example for async notify using the crates tide and async-std 10 | - **pollwatcher_scan** example using `PollWatcher::with_initial_scan` to listen for files found during initial scanning 11 | - **pollwatcher_manual** example using `PollWatcher::poll` without automatic polling for manual triggered polling 12 | 13 | ### Notify Debouncer Full (debouncer) 14 | 15 | - **monitor_debounced** basic usage example for the debouncer 16 | - **debouncer_full** advanced usage accessing the internal file ID cache 17 | 18 | ### Debouncer Mini (mini debouncer) 19 | 20 | - **debouncer_mini** basic usage example for the mini debouncer 21 | - **debouncer_mini_custom** using the mini debouncer with a specific backend (PollWatcher) 22 | -------------------------------------------------------------------------------- /examples/async_monitor.rs: -------------------------------------------------------------------------------- 1 | use futures::{ 2 | channel::mpsc::{channel, Receiver}, 3 | SinkExt, StreamExt, 4 | }; 5 | use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher}; 6 | use std::path::Path; 7 | 8 | /// Async, futures channel based event watching 9 | fn main() { 10 | let path = std::env::args() 11 | .nth(1) 12 | .expect("Argument 1 needs to be a path"); 13 | println!("watching {}", path); 14 | 15 | futures::executor::block_on(async { 16 | if let Err(e) = async_watch(path).await { 17 | println!("error: {:?}", e) 18 | } 19 | }); 20 | } 21 | 22 | fn async_watcher() -> notify::Result<(RecommendedWatcher, Receiver>)> { 23 | let (mut tx, rx) = channel(1); 24 | 25 | // Automatically select the best implementation for your platform. 26 | // You can also access each implementation directly e.g. INotifyWatcher. 27 | let watcher = RecommendedWatcher::new( 28 | move |res| { 29 | futures::executor::block_on(async { 30 | tx.send(res).await.unwrap(); 31 | }) 32 | }, 33 | Config::default(), 34 | )?; 35 | 36 | Ok((watcher, rx)) 37 | } 38 | 39 | async fn async_watch>(path: P) -> notify::Result<()> { 40 | let (mut watcher, mut rx) = async_watcher()?; 41 | 42 | // Add a path to be watched. All files and directories at that path and 43 | // below will be monitored for changes. 44 | watcher.watch(path.as_ref(), RecursiveMode::Recursive)?; 45 | 46 | while let Some(res) = rx.next().await { 47 | match res { 48 | Ok(event) => println!("changed: {:?}", event), 49 | Err(e) => println!("watch error: {:?}", e), 50 | } 51 | } 52 | 53 | Ok(()) 54 | } 55 | -------------------------------------------------------------------------------- /examples/debouncer_full.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, thread, time::Duration}; 2 | 3 | use notify::RecursiveMode; 4 | use notify_debouncer_full::new_debouncer; 5 | use tempfile::tempdir; 6 | 7 | /// Advanced example of the notify-debouncer-full, accessing the internal file ID cache 8 | fn main() -> Result<(), Box> { 9 | let dir = tempdir()?; 10 | let dir_path = dir.path().to_path_buf(); 11 | 12 | // emit some events by changing a file 13 | thread::spawn(move || { 14 | let mut n = 1; 15 | let mut file_path = dir_path.join(format!("file-{n:03}.txt")); 16 | loop { 17 | for _ in 0..5 { 18 | fs::write(&file_path, b"Lorem ipsum").unwrap(); 19 | thread::sleep(Duration::from_millis(500)); 20 | } 21 | n += 1; 22 | let target_path = dir_path.join(format!("file-{n:03}.txt")); 23 | fs::rename(&file_path, &target_path).unwrap(); 24 | file_path = target_path; 25 | } 26 | }); 27 | 28 | // setup debouncer 29 | let (tx, rx) = std::sync::mpsc::channel(); 30 | 31 | // no specific tickrate, max debounce time 2 seconds 32 | let mut debouncer = new_debouncer(Duration::from_secs(2), None, tx)?; 33 | 34 | debouncer.watch(dir.path(), RecursiveMode::Recursive)?; 35 | 36 | // print all events and errors 37 | for result in rx { 38 | match result { 39 | Ok(events) => events.iter().for_each(|event| println!("{event:?}")), 40 | Err(errors) => errors.iter().for_each(|error| println!("{error:?}")), 41 | } 42 | println!(); 43 | } 44 | 45 | Ok(()) 46 | } 47 | -------------------------------------------------------------------------------- /examples/debouncer_mini.rs: -------------------------------------------------------------------------------- 1 | use std::{path::Path, time::Duration}; 2 | 3 | use notify::RecursiveMode; 4 | use notify_debouncer_mini::new_debouncer; 5 | 6 | /// Example for debouncer mini 7 | fn main() { 8 | env_logger::Builder::from_env( 9 | env_logger::Env::default().default_filter_or("debouncer_mini=trace"), 10 | ) 11 | .init(); 12 | // emit some events by changing a file 13 | std::thread::spawn(|| { 14 | let path = Path::new("test.txt"); 15 | let _ = std::fs::remove_file(path); 16 | // log::info!("running 250ms events"); 17 | for _ in 0..20 { 18 | log::trace!("writing.."); 19 | std::fs::write(path, b"Lorem ipsum").unwrap(); 20 | std::thread::sleep(Duration::from_millis(250)); 21 | } 22 | // log::debug!("waiting 20s"); 23 | std::thread::sleep(Duration::from_millis(20000)); 24 | // log::info!("running 3s events"); 25 | for _ in 0..20 { 26 | // log::debug!("writing.."); 27 | std::fs::write(path, b"Lorem ipsum").unwrap(); 28 | std::thread::sleep(Duration::from_millis(3000)); 29 | } 30 | }); 31 | 32 | // setup debouncer 33 | let (tx, rx) = std::sync::mpsc::channel(); 34 | 35 | // No specific tickrate, max debounce time 1 seconds 36 | let mut debouncer = new_debouncer(Duration::from_secs(1), tx).unwrap(); 37 | 38 | debouncer 39 | .watcher() 40 | .watch(Path::new("."), RecursiveMode::Recursive) 41 | .unwrap(); 42 | 43 | // print all events, non returning 44 | for result in rx { 45 | match result { 46 | Ok(events) => events 47 | .iter() 48 | .for_each(|event| log::info!("Event {event:?}")), 49 | Err(error) => log::info!("Error {error:?}"), 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /examples/debouncer_mini_custom.rs: -------------------------------------------------------------------------------- 1 | use std::{path::Path, time::Duration}; 2 | 3 | use notify::{self, RecursiveMode}; 4 | use notify_debouncer_mini::{new_debouncer_opt, Config}; 5 | 6 | /// Debouncer with custom backend and waiting for exit 7 | fn main() { 8 | // emit some events by changing a file 9 | std::thread::spawn(|| { 10 | let path = Path::new("test.txt"); 11 | let _ = std::fs::remove_file(path); 12 | loop { 13 | std::fs::write(path, b"Lorem ipsum").unwrap(); 14 | std::thread::sleep(Duration::from_millis(250)); 15 | } 16 | }); 17 | 18 | // setup debouncer 19 | let (tx, rx) = std::sync::mpsc::channel(); 20 | // notify backend configuration 21 | let backend_config = notify::Config::default().with_poll_interval(Duration::from_secs(1)); 22 | // debouncer configuration 23 | let debouncer_config = Config::default() 24 | .with_timeout(Duration::from_millis(1000)) 25 | .with_notify_config(backend_config); 26 | // select backend via fish operator, here PollWatcher backend 27 | let mut debouncer = new_debouncer_opt::<_, notify::PollWatcher>(debouncer_config, tx).unwrap(); 28 | 29 | debouncer 30 | .watcher() 31 | .watch(Path::new("."), RecursiveMode::Recursive) 32 | .unwrap(); 33 | // print all events, non returning 34 | for result in rx { 35 | match result { 36 | Ok(event) => println!("Event {event:?}"), 37 | Err(error) => println!("Error {error:?}"), 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /examples/hot_reload_tide/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /examples/hot_reload_tide/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hot_reload_tide" 3 | version = "0.1.0" 4 | authors = ["user "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | tide = "0.16.0" 11 | async-std = { version = "1.6.0", features = ["attributes"] } 12 | serde_json = "1.0" 13 | serde = "1.0.115" 14 | notify = { path = "../../notify", features = ["serde"] } 15 | 16 | # required to prevent mixing with workspace 17 | # hack to prevent cargo audit from catching this 18 | [workspace] -------------------------------------------------------------------------------- /examples/hot_reload_tide/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notify-rs/notify/65ea4b7845c3af1e547e4b3a044bba83c86bf400/examples/hot_reload_tide/README.md -------------------------------------------------------------------------------- /examples/hot_reload_tide/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "audio_folder_path": "sounds/", 3 | "messages": { 4 | "sound.mp3": { 5 | "display_name": "Sound 1", 6 | "volume": 62.0 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/hot_reload_tide/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod messages; 2 | -------------------------------------------------------------------------------- /examples/hot_reload_tide/src/main.rs: -------------------------------------------------------------------------------- 1 | // Imagine this is a web app that remembers information about audio messages. 2 | // It has a config.json file that acts as a database, 3 | // you can edit the configuration and the app will pick up changes without the need to restart it. 4 | // This concept is known as hot-reloading. 5 | use hot_reload_tide::messages::{load_config, Config}; 6 | use notify::{Error, Event, RecommendedWatcher, RecursiveMode, Watcher}; 7 | use std::path::Path; 8 | use std::sync::{Arc, Mutex}; 9 | use tide::{Body, Response}; 10 | 11 | const CONFIG_PATH: &str = "config.json"; 12 | 13 | // Because we're running a web server we need a runtime, 14 | // for more information on async runtimes, please check out [async-std](https://github.com/async-rs/async-std) 15 | #[async_std::main] 16 | async fn main() -> tide::Result<()> { 17 | let config = load_config(CONFIG_PATH).unwrap(); 18 | 19 | // We wrap the data a mutex under an atomic reference counted pointer 20 | // to guarantee that the config won't be read and written to at the same time. 21 | // To learn about how that works, 22 | // please check out the [Fearless Concurrency](https://doc.rust-lang.org/book/ch16-00-concurrency.html) chapter of the Rust book. 23 | let config = Arc::new(Mutex::new(config)); 24 | let cloned_config = Arc::clone(&config); 25 | 26 | // We listen to file changes by giving Notify 27 | // a function that will get called when events happen 28 | let mut watcher = 29 | // To make sure that the config lives as long as the function 30 | // we need to move the ownership of the config inside the function 31 | // To learn more about move please read [Using move Closures with Threads](https://doc.rust-lang.org/book/ch16-01-threads.html?highlight=move#using-move-closures-with-threads) 32 | RecommendedWatcher::new(move |result: Result| { 33 | let event = result.unwrap(); 34 | 35 | if event.kind.is_modify() { 36 | match load_config(CONFIG_PATH) { 37 | Ok(new_config) => *cloned_config.lock().unwrap() = new_config, 38 | Err(error) => println!("Error reloading config: {:?}", error), 39 | } 40 | } 41 | },notify::Config::default())?; 42 | 43 | watcher.watch(Path::new(CONFIG_PATH), RecursiveMode::Recursive)?; 44 | 45 | // We set up a web server using [Tide](https://github.com/http-rs/tide) 46 | let mut app = tide::with_state(config); 47 | 48 | app.at("/messages").get(get_messages); 49 | app.at("/message/:name").get(get_message); 50 | app.listen("127.0.0.1:8080").await?; 51 | 52 | Ok(()) 53 | } 54 | 55 | type Request = tide::Request>>; 56 | 57 | async fn get_messages(req: Request) -> tide::Result { 58 | let mut res = Response::new(200); 59 | let config = &req.state().lock().unwrap(); 60 | let body = Body::from_json(&config.messages)?; 61 | res.set_body(body); 62 | Ok(res) 63 | } 64 | 65 | async fn get_message(req: Request) -> tide::Result { 66 | let mut res = Response::new(200); 67 | 68 | let name: String = req.param("name")?.parse()?; 69 | let config = &req.state().lock().unwrap(); 70 | let value = config.messages.get(&name); 71 | 72 | let body = Body::from_json(&value)?; 73 | res.set_body(body); 74 | Ok(res) 75 | } 76 | -------------------------------------------------------------------------------- /examples/hot_reload_tide/src/messages.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::collections::HashMap; 3 | 4 | #[derive(Serialize, Deserialize, Debug, Clone)] 5 | pub struct Config { 6 | pub audio_folder_path: String, 7 | pub messages: Messages, 8 | } 9 | 10 | /// The key is the audio file name 11 | type Messages = HashMap; 12 | 13 | #[derive(Serialize, Deserialize, Debug, Clone)] 14 | pub struct Message { 15 | pub display_name: String, 16 | pub volume: f32, 17 | } 18 | 19 | pub fn load_config(path: &str) -> Result> { 20 | let file = std::fs::File::open(path)?; 21 | let file_size = file.metadata()?.len(); 22 | 23 | if file_size == 0 { 24 | return Err("The config file is empty.".into()); 25 | } 26 | 27 | let reader = std::io::BufReader::new(file); 28 | 29 | let config: Config = serde_json::from_reader(reader)?; 30 | Ok(config) 31 | } 32 | -------------------------------------------------------------------------------- /examples/hot_reload_tide/tests/messages_test.rs: -------------------------------------------------------------------------------- 1 | use hot_reload_tide::messages::{load_config, Config}; 2 | 3 | #[test] 4 | fn load_config_from_file() { 5 | let Config { 6 | audio_folder_path, 7 | messages, 8 | } = load_config("tests/messages_test_config.json").unwrap(); 9 | 10 | assert_eq!(audio_folder_path, "sounds/"); 11 | 12 | let message = messages.get("sound.mp3").unwrap(); 13 | 14 | assert_eq!(message.display_name, "Sound 1"); 15 | } 16 | -------------------------------------------------------------------------------- /examples/hot_reload_tide/tests/messages_test_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "audio_folder_path": "sounds/", 3 | "messages": { 4 | "sound.mp3": { 5 | "display_name": "Sound 1", 6 | "volume": 60.0 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/monitor_debounced.rs: -------------------------------------------------------------------------------- 1 | use notify::RecursiveMode; 2 | use notify_debouncer_full::new_debouncer; 3 | use std::{path::Path, time::Duration}; 4 | 5 | /// Example for notify-debouncer-full 6 | fn main() { 7 | env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); 8 | 9 | let path = std::env::args() 10 | .nth(1) 11 | .expect("Argument 1 needs to be a path"); 12 | 13 | log::info!("Watching {path}"); 14 | 15 | if let Err(error) = watch(path) { 16 | log::error!("Error: {error:?}"); 17 | } 18 | } 19 | 20 | fn watch>(path: P) -> notify::Result<()> { 21 | let (tx, rx) = std::sync::mpsc::channel(); 22 | 23 | // Create a new debounced file watcher with a timeout of 2 seconds. 24 | // The tickrate will be selected automatically, as well as the underlying watch implementation. 25 | let mut debouncer = new_debouncer(Duration::from_secs(2), None, tx)?; 26 | 27 | // Add a path to be watched. All files and directories at that path and 28 | // below will be monitored for changes. 29 | debouncer.watch(path.as_ref(), RecursiveMode::Recursive)?; 30 | 31 | // print all events and errors 32 | for result in rx { 33 | match result { 34 | Ok(events) => events.iter().for_each(|event| log::info!("{event:?}")), 35 | Err(errors) => errors.iter().for_each(|error| log::error!("{error:?}")), 36 | } 37 | } 38 | 39 | Ok(()) 40 | } 41 | -------------------------------------------------------------------------------- /examples/monitor_raw.rs: -------------------------------------------------------------------------------- 1 | use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher}; 2 | use std::path::Path; 3 | 4 | fn main() { 5 | env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); 6 | 7 | let path = std::env::args() 8 | .nth(1) 9 | .expect("Argument 1 needs to be a path"); 10 | 11 | log::info!("Watching {path}"); 12 | 13 | if let Err(error) = watch(path) { 14 | log::error!("Error: {error:?}"); 15 | } 16 | } 17 | 18 | fn watch>(path: P) -> notify::Result<()> { 19 | let (tx, rx) = std::sync::mpsc::channel(); 20 | 21 | // Automatically select the best implementation for your platform. 22 | // You can also access each implementation directly e.g. INotifyWatcher. 23 | let mut watcher = RecommendedWatcher::new(tx, Config::default())?; 24 | 25 | // Add a path to be watched. All files and directories at that path and 26 | // below will be monitored for changes. 27 | watcher.watch(path.as_ref(), RecursiveMode::Recursive)?; 28 | 29 | for res in rx { 30 | match res { 31 | Ok(event) => log::info!("Change: {event:?}"), 32 | Err(error) => log::error!("Error: {error:?}"), 33 | } 34 | } 35 | 36 | Ok(()) 37 | } 38 | -------------------------------------------------------------------------------- /examples/poll_sysfs.rs: -------------------------------------------------------------------------------- 1 | /// Example for watching kernel internal filesystems like `/sys` and `/proc` 2 | /// These can't be watched by the default backend or unconfigured pollwatcher 3 | /// This example can't be demonstrated under windows, it might be relevant for network shares 4 | #[cfg(not(target_os = "windows"))] 5 | fn not_windows_main() -> notify::Result<()> { 6 | use notify::{Config, PollWatcher, RecursiveMode, Watcher}; 7 | use std::path::Path; 8 | use std::time::Duration; 9 | 10 | let mut paths: Vec<_> = std::env::args() 11 | .skip(1) 12 | .map(|arg| Path::new(&arg).to_path_buf()) 13 | .collect(); 14 | if paths.is_empty() { 15 | let lo_stats = Path::new("/sys/class/net/lo/statistics/tx_bytes").to_path_buf(); 16 | if !lo_stats.exists() { 17 | eprintln!("Must provide path to watch, default system path was not found (probably you're not running on Linux?)"); 18 | std::process::exit(1); 19 | } 20 | println!( 21 | "Trying {:?}, use `ping localhost` to see changes!", 22 | lo_stats 23 | ); 24 | paths.push(lo_stats); 25 | } 26 | 27 | println!("watching {:?}...", paths); 28 | // configure pollwatcher backend 29 | let config = Config::default() 30 | .with_compare_contents(true) // crucial part for pseudo filesystems 31 | .with_poll_interval(Duration::from_secs(2)); 32 | let (tx, rx) = std::sync::mpsc::channel(); 33 | // create pollwatcher backend 34 | let mut watcher = PollWatcher::new(tx, config)?; 35 | for path in paths { 36 | // watch all paths 37 | watcher.watch(&path, RecursiveMode::Recursive)?; 38 | } 39 | // print all events, never returns 40 | for res in rx { 41 | match res { 42 | Ok(event) => println!("changed: {:?}", event), 43 | Err(e) => println!("watch error: {:?}", e), 44 | } 45 | } 46 | 47 | Ok(()) 48 | } 49 | 50 | fn main() -> notify::Result<()> { 51 | #[cfg(not(target_os = "windows"))] 52 | { 53 | not_windows_main() 54 | } 55 | #[cfg(target_os = "windows")] 56 | notify::Result::Ok(()) 57 | } 58 | -------------------------------------------------------------------------------- /examples/pollwatcher_manual.rs: -------------------------------------------------------------------------------- 1 | use notify::{Config, PollWatcher, RecursiveMode, Watcher}; 2 | use std::path::Path; 3 | 4 | // Example for the PollWatcher with manual polling. 5 | // Call with cargo run -p examples --example pollwatcher_manual -- path/to/watch 6 | fn main() { 7 | env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); 8 | 9 | let path = std::env::args() 10 | .nth(1) 11 | .expect("Argument 1 needs to be a path"); 12 | 13 | log::info!("Watching {path}"); 14 | 15 | if let Err(error) = watch(path) { 16 | log::error!("Error: {error:?}"); 17 | } 18 | } 19 | 20 | fn watch>(path: P) -> notify::Result<()> { 21 | let (tx, rx) = std::sync::mpsc::channel(); 22 | // use the PollWatcher and disable automatic polling 23 | let mut watcher = PollWatcher::new(tx, Config::default().with_manual_polling())?; 24 | 25 | // Add a path to be watched. All files and directories at that path and 26 | // below will be monitored for changes. 27 | watcher.watch(path.as_ref(), RecursiveMode::Recursive)?; 28 | 29 | // run event receiver on a different thread, we want this one for user input 30 | std::thread::spawn(move || { 31 | for res in rx { 32 | match res { 33 | Ok(event) => println!("changed: {:?}", event), 34 | Err(e) => println!("watch error: {:?}", e), 35 | } 36 | } 37 | }); 38 | 39 | // wait for any input and poll 40 | loop { 41 | println!("Press enter to poll for changes"); 42 | let mut buffer = String::new(); 43 | std::io::stdin().read_line(&mut buffer)?; 44 | println!("polling.."); 45 | // manually poll for changes, received by the spawned thread 46 | watcher.poll().unwrap(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /examples/pollwatcher_scan.rs: -------------------------------------------------------------------------------- 1 | use notify::{poll::ScanEvent, Config, PollWatcher, RecursiveMode, Watcher}; 2 | use std::path::Path; 3 | 4 | // Example for the pollwatcher scan callback feature. 5 | // Returns the scanned paths 6 | fn main() { 7 | env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); 8 | 9 | let path = std::env::args() 10 | .nth(1) 11 | .expect("Argument 1 needs to be a path"); 12 | 13 | log::info!("Watching {path}"); 14 | 15 | if let Err(error) = watch(path) { 16 | log::error!("Error: {error:?}"); 17 | } 18 | } 19 | 20 | fn watch>(path: P) -> notify::Result<()> { 21 | let (tx, rx) = std::sync::mpsc::channel(); 22 | 23 | // if you want to use the same channel for both events 24 | // and you need to differentiate between scan and file change events, 25 | // then you will have to use something like this 26 | enum Message { 27 | Event(notify::Result), 28 | Scan(ScanEvent), 29 | } 30 | 31 | let tx_c = tx.clone(); 32 | // use the pollwatcher and set a callback for the scanning events 33 | let mut watcher = PollWatcher::with_initial_scan( 34 | move |watch_event| { 35 | tx_c.send(Message::Event(watch_event)).unwrap(); 36 | }, 37 | Config::default(), 38 | move |scan_event| { 39 | tx.send(Message::Scan(scan_event)).unwrap(); 40 | }, 41 | )?; 42 | 43 | // Add a path to be watched. All files and directories at that path and 44 | // below will be monitored for changes. 45 | watcher.watch(path.as_ref(), RecursiveMode::Recursive)?; 46 | 47 | for res in rx { 48 | match res { 49 | Message::Event(e) => println!("Watch event {e:?}"), 50 | Message::Scan(e) => println!("Scan event {e:?}"), 51 | } 52 | } 53 | 54 | Ok(()) 55 | } 56 | -------------------------------------------------------------------------------- /examples/watcher_kind.rs: -------------------------------------------------------------------------------- 1 | use notify::*; 2 | use std::{path::Path, time::Duration}; 3 | 4 | // example of detecting the recommended watcher kind 5 | fn main() { 6 | let (tx, rx) = std::sync::mpsc::channel(); 7 | // This example is a little bit misleading as you can just create one Config and use it for all watchers. 8 | // That way the pollwatcher specific stuff is still configured, if it should be used. 9 | let mut watcher: Box = if RecommendedWatcher::kind() == WatcherKind::PollWatcher { 10 | // custom config for PollWatcher kind 11 | // you 12 | let config = Config::default().with_poll_interval(Duration::from_secs(1)); 13 | Box::new(PollWatcher::new(tx, config).unwrap()) 14 | } else { 15 | // use default config for everything else 16 | Box::new(RecommendedWatcher::new(tx, Config::default()).unwrap()) 17 | }; 18 | 19 | // watch some stuff 20 | watcher 21 | .watch(Path::new("."), RecursiveMode::Recursive) 22 | .unwrap(); 23 | 24 | // just print all events, this blocks forever 25 | for e in rx { 26 | println!("{:?}", e); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /file-id/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "file-id" 3 | version = "0.2.2" 4 | description = "Utility for reading inode numbers (Linux, MacOS) and file IDs (Windows)" 5 | documentation = "https://docs.rs/notify" 6 | readme = "README.md" 7 | license = "MIT OR Apache-2.0" 8 | keywords = ["filesystem", "inode", "file", "index"] 9 | categories = ["filesystem"] 10 | authors = ["Daniel Faust "] 11 | rust-version.workspace = true 12 | edition.workspace = true 13 | homepage.workspace = true 14 | repository.workspace = true 15 | 16 | [[bin]] 17 | name = "file-id" 18 | path = "bin/file_id.rs" 19 | 20 | [dependencies] 21 | serde = { workspace = true, optional = true } 22 | 23 | [target.'cfg(windows)'.dependencies] 24 | windows-sys = { workspace = true, features = ["Win32_Storage_FileSystem", "Win32_Foundation"] } 25 | 26 | [dev-dependencies] 27 | tempfile.workspace = true 28 | -------------------------------------------------------------------------------- /file-id/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2023 Notify Contributors 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /file-id/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Notify Contributors 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /file-id/README.md: -------------------------------------------------------------------------------- 1 | # File Id 2 | 3 | [![» Docs](https://flat.badgen.net/badge/api/docs.rs/df3600)][docs] 4 | 5 | A utility to read file IDs. 6 | 7 | Modern file systems assign a unique ID to each file. On Linux and MacOS it is called an `inode number`, on Windows it is called `file index`. 8 | Together with the `device id`, a file can be identified uniquely on a device at a given time. 9 | 10 | Keep in mind though, that IDs may be re-used at some point. 11 | 12 | ## Example 13 | 14 | ```rust 15 | let file_id = file_id::get_file_id(path).unwrap(); 16 | 17 | println!("{file_id:?}"); 18 | ``` 19 | 20 | ## Features 21 | 22 | - `serde` for serde support, off by default 23 | 24 | [docs]: https://docs.rs/file-id 25 | -------------------------------------------------------------------------------- /file-id/bin/file_id.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use file_id::FileId; 4 | 5 | fn main() { 6 | let path = std::env::args().nth(1).expect("no path given"); 7 | 8 | print_file_id(&path); 9 | } 10 | 11 | #[cfg(target_family = "unix")] 12 | fn print_file_id(path: &str) { 13 | print_result(file_id::get_file_id(path)); 14 | } 15 | 16 | #[cfg(target_family = "windows")] 17 | fn print_file_id(path: &str) { 18 | print_result(file_id::get_low_res_file_id(path)); 19 | print_result(file_id::get_high_res_file_id(path)); 20 | } 21 | 22 | fn print_result(result: io::Result) { 23 | match result { 24 | Ok(file_id) => println!("{file_id:?}"), 25 | Err(error) => println!("Error: {error}"), 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /file-id/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Utility for reading inode numbers (Linux, macOS) and file ids (Windows) that uniquely identify a file on a single computer. 2 | //! 3 | //! Modern file systems assign a unique ID to each file. On Linux and macOS it is called an `inode number`, 4 | //! on Windows it is called a `file id` or `file index`. 5 | //! Together with the `device id` (Linux, macOS) or the `volume serial number` (Windows), 6 | //! a file or directory can be uniquely identified on a single computer at a given time. 7 | //! 8 | //! Keep in mind though, that IDs may be re-used at some point. 9 | //! 10 | //! ## Example 11 | //! 12 | //! ``` 13 | //! let file = tempfile::NamedTempFile::new().unwrap(); 14 | //! 15 | //! let file_id = file_id::get_file_id(file.path()).unwrap(); 16 | //! println!("{file_id:?}"); 17 | //! ``` 18 | //! 19 | //! ## Example (Windows Only) 20 | //! 21 | //! ```ignore 22 | //! let file = tempfile::NamedTempFile::new().unwrap(); 23 | //! 24 | //! let file_id = file_id::get_low_res_file_id(file.path()).unwrap(); 25 | //! println!("{file_id:?}"); 26 | //! 27 | //! let file_id = file_id::get_high_res_file_id(file.path()).unwrap(); 28 | //! println!("{file_id:?}"); 29 | //! ``` 30 | use std::{fs, io, path::Path}; 31 | 32 | #[cfg(feature = "serde")] 33 | use serde::{Deserialize, Serialize}; 34 | 35 | /// Unique identifier of a file 36 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 37 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 38 | pub enum FileId { 39 | /// Inode number, available on Linux and macOS. 40 | #[cfg_attr(feature = "serde", serde(rename = "inode"))] 41 | Inode { 42 | /// Device ID 43 | #[cfg_attr(feature = "serde", serde(rename = "device"))] 44 | device_id: u64, 45 | 46 | /// Inode number 47 | #[cfg_attr(feature = "serde", serde(rename = "inode"))] 48 | inode_number: u64, 49 | }, 50 | 51 | /// Low resolution file ID, available on Windows XP and above. 52 | /// 53 | /// Compared to the high resolution variant, only the lower parts of the IDs are stored. 54 | /// 55 | /// On Windows, the low resolution variant can be requested explicitly with the `get_low_res_file_id` function. 56 | /// 57 | /// Details: . 58 | #[cfg_attr(feature = "serde", serde(rename = "lowres"))] 59 | LowRes { 60 | /// Volume serial number 61 | #[cfg_attr(feature = "serde", serde(rename = "volume"))] 62 | volume_serial_number: u32, 63 | 64 | /// File index 65 | #[cfg_attr(feature = "serde", serde(rename = "index"))] 66 | file_index: u64, 67 | }, 68 | 69 | /// High resolution file ID, available on Windows Vista and above. 70 | /// 71 | /// On Windows, the high resolution variant can be requested explicitly with the `get_high_res_file_id` function. 72 | /// 73 | /// Details: . 74 | #[cfg_attr(feature = "serde", serde(rename = "highres"))] 75 | HighRes { 76 | /// Volume serial number 77 | #[cfg_attr(feature = "serde", serde(rename = "volume"))] 78 | volume_serial_number: u64, 79 | 80 | /// File ID 81 | #[cfg_attr(feature = "serde", serde(rename = "file"))] 82 | file_id: u128, 83 | }, 84 | } 85 | 86 | impl FileId { 87 | pub fn new_inode(device_id: u64, inode_number: u64) -> Self { 88 | FileId::Inode { 89 | device_id, 90 | inode_number, 91 | } 92 | } 93 | 94 | pub fn new_low_res(volume_serial_number: u32, file_index: u64) -> Self { 95 | FileId::LowRes { 96 | volume_serial_number, 97 | file_index, 98 | } 99 | } 100 | 101 | pub fn new_high_res(volume_serial_number: u64, file_id: u128) -> Self { 102 | FileId::HighRes { 103 | volume_serial_number, 104 | file_id, 105 | } 106 | } 107 | } 108 | 109 | impl AsRef for FileId { 110 | fn as_ref(&self) -> &FileId { 111 | self 112 | } 113 | } 114 | 115 | /// Get the `FileId` for the file or directory at `path` 116 | #[cfg(target_family = "unix")] 117 | pub fn get_file_id(path: impl AsRef) -> io::Result { 118 | use std::os::unix::fs::MetadataExt; 119 | 120 | let metadata = fs::metadata(path.as_ref())?; 121 | 122 | Ok(FileId::new_inode(metadata.dev(), metadata.ino())) 123 | } 124 | 125 | /// Get the `FileId` for the file or directory at `path` 126 | #[cfg(target_family = "windows")] 127 | pub fn get_file_id(path: impl AsRef) -> io::Result { 128 | let file = open_file(path)?; 129 | 130 | unsafe { get_file_info_ex(&file).or_else(|_| get_file_info(&file)) } 131 | } 132 | 133 | /// Get the `FileId` with the low resolution variant for the file or directory at `path` 134 | #[cfg(target_family = "windows")] 135 | pub fn get_low_res_file_id(path: impl AsRef) -> io::Result { 136 | let file = open_file(path)?; 137 | 138 | unsafe { get_file_info(&file) } 139 | } 140 | 141 | /// Get the `FileId` with the high resolution variant for the file or directory at `path` 142 | #[cfg(target_family = "windows")] 143 | pub fn get_high_res_file_id(path: impl AsRef) -> io::Result { 144 | let file = open_file(path)?; 145 | 146 | unsafe { get_file_info_ex(&file) } 147 | } 148 | 149 | #[cfg(target_family = "windows")] 150 | unsafe fn get_file_info_ex(file: &fs::File) -> Result { 151 | use std::{mem, os::windows::prelude::*}; 152 | use windows_sys::Win32::{ 153 | Foundation::HANDLE, 154 | Storage::FileSystem::{FileIdInfo, GetFileInformationByHandleEx, FILE_ID_INFO}, 155 | }; 156 | 157 | let mut info: FILE_ID_INFO = mem::zeroed(); 158 | let ret = GetFileInformationByHandleEx( 159 | file.as_raw_handle() as HANDLE, 160 | FileIdInfo, 161 | &mut info as *mut FILE_ID_INFO as _, 162 | mem::size_of::() as u32, 163 | ); 164 | 165 | if ret == 0 { 166 | return Err(io::Error::last_os_error()); 167 | }; 168 | 169 | Ok(FileId::new_high_res( 170 | info.VolumeSerialNumber, 171 | u128::from_le_bytes(info.FileId.Identifier), 172 | )) 173 | } 174 | 175 | #[cfg(target_family = "windows")] 176 | unsafe fn get_file_info(file: &fs::File) -> Result { 177 | use std::{mem, os::windows::prelude::*}; 178 | use windows_sys::Win32::{ 179 | Foundation::HANDLE, 180 | Storage::FileSystem::{GetFileInformationByHandle, BY_HANDLE_FILE_INFORMATION}, 181 | }; 182 | 183 | let mut info: BY_HANDLE_FILE_INFORMATION = mem::zeroed(); 184 | let ret = GetFileInformationByHandle(file.as_raw_handle() as HANDLE, &mut info); 185 | if ret == 0 { 186 | return Err(io::Error::last_os_error()); 187 | }; 188 | 189 | Ok(FileId::new_low_res( 190 | info.dwVolumeSerialNumber, 191 | ((info.nFileIndexHigh as u64) << 32) | (info.nFileIndexLow as u64), 192 | )) 193 | } 194 | 195 | #[cfg(target_family = "windows")] 196 | fn open_file>(path: P) -> io::Result { 197 | use std::{fs::OpenOptions, os::windows::fs::OpenOptionsExt}; 198 | use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_BACKUP_SEMANTICS; 199 | 200 | OpenOptions::new() 201 | .access_mode(0) 202 | .custom_flags(FILE_FLAG_BACKUP_SEMANTICS) 203 | .open(path) 204 | } 205 | -------------------------------------------------------------------------------- /notify-debouncer-full/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "notify-debouncer-full" 3 | version = "0.5.0" 4 | description = "notify event debouncer optimized for ease of use" 5 | documentation = "https://docs.rs/notify-debouncer-full" 6 | authors = ["Daniel Faust "] 7 | keywords = ["events", "filesystem", "notify", "watch"] 8 | license = "MIT OR Apache-2.0" 9 | readme = "README.md" 10 | rust-version.workspace = true 11 | edition.workspace = true 12 | homepage.workspace = true 13 | repository.workspace = true 14 | 15 | [features] 16 | default = ["macos_fsevent"] 17 | serde = ["notify-types/serde"] 18 | web-time = ["notify-types/web-time"] 19 | crossbeam-channel = ["dep:crossbeam-channel", "notify/crossbeam-channel"] 20 | flume = ["dep:flume", "notify/flume"] 21 | macos_fsevent = ["notify/macos_fsevent"] 22 | macos_kqueue = ["notify/macos_kqueue"] 23 | serialization-compat-6 = ["notify/serialization-compat-6"] 24 | 25 | [dependencies] 26 | notify.workspace = true 27 | notify-types.workspace = true 28 | crossbeam-channel = { workspace = true, optional = true } 29 | flume = { workspace = true, optional = true } 30 | file-id.workspace = true 31 | walkdir.workspace = true 32 | log.workspace = true 33 | 34 | [dev-dependencies] 35 | pretty_assertions.workspace = true 36 | rstest.workspace = true 37 | serde.workspace = true 38 | deser-hjson.workspace = true 39 | rand.workspace = true 40 | tempfile.workspace = true 41 | -------------------------------------------------------------------------------- /notify-debouncer-full/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2023 Notify Contributors 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /notify-debouncer-full/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Notify Contributors 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /notify-debouncer-full/README.md: -------------------------------------------------------------------------------- 1 | # Notify Debouncer Full 2 | 3 | [![» Docs](https://flat.badgen.net/badge/api/docs.rs/df3600)][docs] 4 | 5 | A debouncer for [notify] that is optimized for ease of use. 6 | 7 | * Only emits a single `Rename` event if the rename `From` and `To` events can be matched 8 | * Merges multiple `Rename` events 9 | * Takes `Rename` events into account and updates paths for events that occurred before the rename event, but which haven't been emitted, yet 10 | * Optionally keeps track of the file system IDs all files and stitches rename events together (FSevents, Windows) 11 | * Emits only one `Remove` event when deleting a directory (inotify) 12 | * Doesn't emit duplicate create events 13 | * Doesn't emit `Modify` events after a `Create` event 14 | 15 | ## Features 16 | 17 | - `crossbeam-channel` passed down to notify, off by default 18 | 19 | - `flume` passed down to notify, off by default 20 | 21 | - `serialization-compat-6` passed down to notify, off by default 22 | 23 | [docs]: https://docs.rs/notify-debouncer-full 24 | [notify]: https://crates.io/crates/notify 25 | -------------------------------------------------------------------------------- /notify-debouncer-full/src/cache.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | path::{Path, PathBuf}, 4 | }; 5 | 6 | use file_id::{get_file_id, FileId}; 7 | use notify::RecursiveMode; 8 | use walkdir::WalkDir; 9 | 10 | /// The interface of a file ID cache. 11 | /// 12 | /// This trait can be implemented for an existing cache, if it already holds `FileId`s. 13 | pub trait FileIdCache { 14 | /// Get a `FileId` from the cache for a given `path`. 15 | /// 16 | /// If the path is not cached, `None` should be returned and there should not be any attempt to read the file ID from disk. 17 | fn cached_file_id(&self, path: &Path) -> Option>; 18 | 19 | /// Add a new path to the cache or update its value. 20 | /// 21 | /// This will be called if a new file or directory is created or if an existing file is overridden. 22 | fn add_path(&mut self, path: &Path, recursive_mode: RecursiveMode); 23 | 24 | /// Remove a path from the cache. 25 | /// 26 | /// This will be called if a file or directory is deleted. 27 | fn remove_path(&mut self, path: &Path); 28 | 29 | /// Re-scan all `root_paths`. 30 | /// 31 | /// This will be called if the notification back-end has dropped events. 32 | /// The root paths are passed as argument, so the implementer doesn't have to store them. 33 | /// 34 | /// The default implementation calls `add_path` for each root path. 35 | fn rescan(&mut self, root_paths: &[(PathBuf, RecursiveMode)]) { 36 | for (path, recursive_mode) in root_paths { 37 | self.add_path(path, *recursive_mode); 38 | } 39 | } 40 | } 41 | 42 | /// A cache to hold the file system IDs of all watched files. 43 | /// 44 | /// The file ID cache uses unique file IDs provided by the file system and is used to stich together 45 | /// rename events in case the notification back-end doesn't emit rename cookies. 46 | #[derive(Debug, Clone, Default)] 47 | pub struct FileIdMap { 48 | paths: HashMap, 49 | } 50 | 51 | impl FileIdMap { 52 | /// Construct an empty cache. 53 | pub fn new() -> Self { 54 | Default::default() 55 | } 56 | 57 | fn dir_scan_depth(is_recursive: bool) -> usize { 58 | if is_recursive { 59 | usize::MAX 60 | } else { 61 | 1 62 | } 63 | } 64 | } 65 | 66 | impl FileIdCache for FileIdMap { 67 | fn cached_file_id(&self, path: &Path) -> Option> { 68 | self.paths.get(path) 69 | } 70 | 71 | fn add_path(&mut self, path: &Path, recursive_mode: RecursiveMode) { 72 | let is_recursive = recursive_mode == RecursiveMode::Recursive; 73 | 74 | for (path, file_id) in WalkDir::new(path) 75 | .follow_links(true) 76 | .max_depth(Self::dir_scan_depth(is_recursive)) 77 | .into_iter() 78 | .filter_map(|entry| { 79 | let path = entry.ok()?.into_path(); 80 | let file_id = get_file_id(&path).ok()?; 81 | Some((path, file_id)) 82 | }) 83 | { 84 | self.paths.insert(path, file_id); 85 | } 86 | } 87 | 88 | fn remove_path(&mut self, path: &Path) { 89 | self.paths.retain(|p, _| !p.starts_with(path)); 90 | } 91 | } 92 | 93 | /// An implementation of the `FileIdCache` trait that doesn't hold any data. 94 | /// 95 | /// This pseudo cache can be used to disable the file tracking using file system IDs. 96 | #[derive(Debug, Clone, Default)] 97 | pub struct NoCache; 98 | 99 | impl NoCache { 100 | /// Construct an empty cache. 101 | pub fn new() -> Self { 102 | Default::default() 103 | } 104 | } 105 | 106 | impl FileIdCache for NoCache { 107 | fn cached_file_id(&self, _path: &Path) -> Option> { 108 | Option::<&FileId>::None 109 | } 110 | 111 | fn add_path(&mut self, _path: &Path, _recursive_mode: RecursiveMode) {} 112 | 113 | fn remove_path(&mut self, _path: &Path) {} 114 | } 115 | 116 | /// The recommended file ID cache implementation for the current platform 117 | #[cfg(any(target_os = "linux", target_os = "android"))] 118 | pub type RecommendedCache = NoCache; 119 | /// The recommended file ID cache implementation for the current platform 120 | #[cfg(not(any(target_os = "linux", target_os = "android")))] 121 | pub type RecommendedCache = FileIdMap; 122 | -------------------------------------------------------------------------------- /notify-debouncer-full/src/testing.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::{HashMap, VecDeque}, 3 | path::{Path, PathBuf}, 4 | time::{Duration, Instant}, 5 | }; 6 | 7 | use file_id::FileId; 8 | use notify::{ 9 | event::{ 10 | AccessKind, AccessMode, CreateKind, DataChange, Flag, MetadataKind, ModifyKind, RemoveKind, 11 | RenameMode, 12 | }, 13 | Error, ErrorKind, Event, EventKind, RecursiveMode, 14 | }; 15 | 16 | use crate::{DebounceDataInner, DebouncedEvent, FileIdCache, Queue}; 17 | 18 | pub(crate) use schema::TestCase; 19 | 20 | mod schema { 21 | use std::collections::HashMap; 22 | 23 | use serde::Deserialize; 24 | 25 | #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] 26 | pub(crate) struct Error { 27 | /// The error kind is parsed by `into_notify_error` 28 | pub kind: String, 29 | 30 | /// The error paths 31 | #[serde(default)] 32 | pub paths: Vec, 33 | } 34 | 35 | #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] 36 | pub(crate) struct Event { 37 | /// The timestamp the event occurred 38 | #[serde(default)] 39 | pub time: u64, 40 | 41 | /// The event kind is parsed by `into_notify_event` 42 | pub kind: String, 43 | 44 | /// The event paths 45 | #[serde(default)] 46 | pub paths: Vec, 47 | 48 | /// The event flags 49 | #[serde(default)] 50 | pub flags: Vec, 51 | 52 | /// The event tracker 53 | pub tracker: Option, 54 | 55 | /// The event info 56 | pub info: Option, 57 | 58 | /// The file id for the file associated with the event 59 | /// 60 | /// Only used for the rename event. 61 | pub file_id: Option, 62 | } 63 | 64 | #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] 65 | pub(crate) struct Queue { 66 | pub events: Vec, 67 | } 68 | 69 | #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] 70 | pub(crate) struct State { 71 | /// Timeout for the debouncer 72 | /// 73 | /// Only used for the initial state. 74 | pub timeout: Option, 75 | 76 | /// The event queues for each file 77 | #[serde(default)] 78 | pub queues: HashMap, 79 | 80 | /// Cached file ids 81 | #[serde(default)] 82 | pub cache: HashMap, 83 | 84 | /// A map of file ids, used instead of accessing the file system 85 | #[serde(default)] 86 | pub file_system: HashMap, 87 | 88 | /// Current rename event 89 | pub rename_event: Option, 90 | 91 | /// Current rescan event 92 | pub rescan_event: Option, 93 | 94 | /// Debounced events 95 | /// 96 | /// Only used for the expected state. 97 | #[serde(default)] 98 | pub events: HashMap>, 99 | 100 | /// Errors 101 | #[serde(default)] 102 | pub errors: Vec, 103 | } 104 | 105 | #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] 106 | pub(crate) struct TestCase { 107 | /// Initial state 108 | pub state: State, 109 | 110 | /// Events that are added during the test 111 | #[serde(default)] 112 | pub events: Vec, 113 | 114 | /// Errors that are added during the test 115 | #[serde(default)] 116 | pub errors: Vec, 117 | 118 | /// Expected state after the test 119 | pub expected: State, 120 | } 121 | } 122 | 123 | impl schema::Error { 124 | pub fn into_notify_error(self) -> Error { 125 | let kind = match &*self.kind { 126 | "path-not-found" => ErrorKind::PathNotFound, 127 | "watch-not-found" => ErrorKind::WatchNotFound, 128 | "max-files-watch" => ErrorKind::MaxFilesWatch, 129 | _ => panic!("unknown error type `{}`", self.kind), 130 | }; 131 | let mut error = Error::new(kind); 132 | 133 | for p in self.paths { 134 | error = error.add_path(PathBuf::from(p)); 135 | } 136 | 137 | error 138 | } 139 | } 140 | 141 | impl schema::Event { 142 | #[rustfmt::skip] 143 | pub fn into_debounced_event(self, time: Instant, path: Option<&str>) -> DebouncedEvent { 144 | let kind = match &*self.kind { 145 | "any" => EventKind::Any, 146 | "other" => EventKind::Other, 147 | "access-any" => EventKind::Access(AccessKind::Any), 148 | "access-read" => EventKind::Access(AccessKind::Read), 149 | "access-open-any" => EventKind::Access(AccessKind::Open(AccessMode::Any)), 150 | "access-open-execute" => EventKind::Access(AccessKind::Open(AccessMode::Execute)), 151 | "access-open-read" => EventKind::Access(AccessKind::Open(AccessMode::Read)), 152 | "access-open-write" => EventKind::Access(AccessKind::Open(AccessMode::Write)), 153 | "access-open-other" => EventKind::Access(AccessKind::Open(AccessMode::Other)), 154 | "access-close-any" => EventKind::Access(AccessKind::Close(AccessMode::Any)), 155 | "access-close-execute" => EventKind::Access(AccessKind::Close(AccessMode::Execute)), 156 | "access-close-read" => EventKind::Access(AccessKind::Close(AccessMode::Read)), 157 | "access-close-write" => EventKind::Access(AccessKind::Close(AccessMode::Write)), 158 | "access-close-other" => EventKind::Access(AccessKind::Close(AccessMode::Other)), 159 | "access-other" => EventKind::Access(AccessKind::Other), 160 | "create-any" => EventKind::Create(CreateKind::Any), 161 | "create-file" => EventKind::Create(CreateKind::File), 162 | "create-folder" => EventKind::Create(CreateKind::Folder), 163 | "create-other" => EventKind::Create(CreateKind::Other), 164 | "modify-any" => EventKind::Modify(ModifyKind::Any), 165 | "modify-other" => EventKind::Modify(ModifyKind::Other), 166 | "modify-data-any" => EventKind::Modify(ModifyKind::Data(DataChange::Any)), 167 | "modify-data-size" => EventKind::Modify(ModifyKind::Data(DataChange::Size)), 168 | "modify-data-content" => EventKind::Modify(ModifyKind::Data(DataChange::Content)), 169 | "modify-data-other" => EventKind::Modify(ModifyKind::Data(DataChange::Other)), 170 | "modify-metadata-any" => EventKind::Modify(ModifyKind::Metadata(MetadataKind::Any)), 171 | "modify-metadata-access-time" => EventKind::Modify(ModifyKind::Metadata(MetadataKind::AccessTime)), 172 | "modify-metadata-write-time" => EventKind::Modify(ModifyKind::Metadata(MetadataKind::WriteTime)), 173 | "modify-metadata-permissions" => EventKind::Modify(ModifyKind::Metadata(MetadataKind::Permissions)), 174 | "modify-metadata-ownership" => EventKind::Modify(ModifyKind::Metadata(MetadataKind::Ownership)), 175 | "modify-metadata-extended" => EventKind::Modify(ModifyKind::Metadata(MetadataKind::Extended)), 176 | "modify-metadata-other" => EventKind::Modify(ModifyKind::Metadata(MetadataKind::Other)), 177 | "rename-any" => EventKind::Modify(ModifyKind::Name(RenameMode::Any)), 178 | "rename-from" => EventKind::Modify(ModifyKind::Name(RenameMode::From)), 179 | "rename-to" => EventKind::Modify(ModifyKind::Name(RenameMode::To)), 180 | "rename-both" => EventKind::Modify(ModifyKind::Name(RenameMode::Both)), 181 | "rename-other" => EventKind::Modify(ModifyKind::Name(RenameMode::Other)), 182 | "remove-any" => EventKind::Remove(RemoveKind::Any), 183 | "remove-file" => EventKind::Remove(RemoveKind::File), 184 | "remove-folder" => EventKind::Remove(RemoveKind::Folder), 185 | "remove-other" => EventKind::Remove(RemoveKind::Other), 186 | _ => panic!("unknown event type `{}`", self.kind), 187 | }; 188 | let mut event = Event::new(kind); 189 | 190 | for p in self.paths { 191 | event = event.add_path(if p == "*" { 192 | PathBuf::from(path.expect("cannot replace `*`")) 193 | } else { 194 | PathBuf::from(p) 195 | }); 196 | 197 | if let Some(tracker) = self.tracker { 198 | event = event.set_tracker(tracker); 199 | } 200 | 201 | if let Some(info) = &self.info { 202 | event = event.set_info(info.as_str()); 203 | } 204 | } 205 | 206 | for f in self.flags { 207 | let flag = match &*f { 208 | "rescan" => Flag::Rescan, 209 | _ => panic!("unknown event flag `{f}`"), 210 | }; 211 | 212 | event = event.set_flag(flag); 213 | } 214 | 215 | DebouncedEvent { event, time: time + Duration::from_millis(self.time) } 216 | } 217 | } 218 | 219 | impl schema::State { 220 | pub(crate) fn into_debounce_data_inner(self, time: Instant) -> DebounceDataInner { 221 | let queues = self 222 | .queues 223 | .into_iter() 224 | .map(|(path, queue)| { 225 | let queue = Queue { 226 | events: queue 227 | .events 228 | .into_iter() 229 | .map(|event| event.into_debounced_event(time, Some(&path))) 230 | .collect::>(), 231 | }; 232 | (path.into(), queue) 233 | }) 234 | .collect(); 235 | 236 | let cache = self 237 | .cache 238 | .into_iter() 239 | .map(|(path, id)| { 240 | let path = PathBuf::from(path); 241 | let id = FileId::new_inode(id, id); 242 | (path, id) 243 | }) 244 | .collect::>(); 245 | 246 | let file_system = self 247 | .file_system 248 | .into_iter() 249 | .map(|(path, id)| { 250 | let path = PathBuf::from(path); 251 | let id = FileId::new_inode(id, id); 252 | (path, id) 253 | }) 254 | .collect::>(); 255 | 256 | let cache = TestCache::new(cache, file_system); 257 | 258 | let rename_event = self.rename_event.map(|e| { 259 | let file_id = e.file_id.map(|id| FileId::new_inode(id, id)); 260 | let event = e.into_debounced_event(time, None); 261 | (event, file_id) 262 | }); 263 | 264 | let rescan_event = self 265 | .rescan_event 266 | .map(|e| e.into_debounced_event(time, None)); 267 | 268 | DebounceDataInner { 269 | queues, 270 | roots: Vec::new(), 271 | cache, 272 | rename_event, 273 | rescan_event, 274 | errors: Vec::new(), 275 | timeout: Duration::from_millis(self.timeout.unwrap_or(50)), 276 | } 277 | } 278 | } 279 | 280 | #[derive(Debug, Clone)] 281 | pub struct TestCache { 282 | pub paths: HashMap, 283 | pub file_system: HashMap, 284 | } 285 | 286 | impl TestCache { 287 | pub fn new(paths: HashMap, file_system: HashMap) -> Self { 288 | Self { paths, file_system } 289 | } 290 | } 291 | 292 | impl FileIdCache for TestCache { 293 | fn cached_file_id(&self, path: &Path) -> Option> { 294 | self.paths.get(path) 295 | } 296 | 297 | fn add_path(&mut self, path: &Path, recursive_mode: RecursiveMode) { 298 | for (file_path, file_id) in &self.file_system { 299 | if file_path == path 300 | || (file_path.starts_with(path) && recursive_mode == RecursiveMode::Recursive) 301 | { 302 | self.paths.insert(file_path.clone(), *file_id); 303 | } 304 | } 305 | } 306 | 307 | fn remove_path(&mut self, path: &Path) { 308 | self.paths.remove(path); 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /notify-debouncer-full/src/time.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(test))] 2 | pub use build::*; 3 | 4 | #[cfg(not(test))] 5 | mod build { 6 | use std::time::Instant; 7 | 8 | pub fn now() -> Instant { 9 | Instant::now() 10 | } 11 | } 12 | 13 | #[cfg(test)] 14 | pub use test::*; 15 | 16 | #[cfg(test)] 17 | mod test { 18 | use std::{ 19 | sync::Mutex, 20 | time::{Duration, Instant}, 21 | }; 22 | 23 | thread_local! { 24 | static NOW: Mutex> = const { Mutex::new(None) }; 25 | } 26 | 27 | pub fn now() -> Instant { 28 | let time = NOW.with(|now| *now.lock().unwrap()); 29 | time.unwrap_or_else(Instant::now) 30 | } 31 | 32 | pub struct MockTime; 33 | 34 | impl MockTime { 35 | pub fn set_time(time: Instant) { 36 | NOW.with(|now| *now.lock().unwrap() = Some(time)); 37 | } 38 | 39 | pub fn advance(delta: Duration) { 40 | NOW.with(|now| { 41 | if let Some(n) = &mut *now.lock().unwrap() { 42 | *n += delta; 43 | } 44 | }); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /notify-debouncer-full/test_cases/add_create_dir_event_twice.hjson: -------------------------------------------------------------------------------- 1 | // https://github.com/spacedriveapp/spacedrive/blob/90a350946914be7f91ba692887ca03db659d530a/core/src/location/manager/watcher/macos.rs 2 | // 3 | // This is a MacOS specific event that happens when a folder is created trough Finder. 4 | // It creates a folder but 2 events are triggered in FSEvents. 5 | { 6 | state: {} 7 | events: [ 8 | { kind: "create-folder", paths: ["/watch/dir"] } 9 | { kind: "create-folder", paths: ["/watch/dir"] } 10 | ] 11 | expected: { 12 | queues: { 13 | /watch/dir: { 14 | events: [ 15 | { kind: "create-folder", paths: ["*"] } 16 | ] 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /notify-debouncer-full/test_cases/add_create_event.hjson: -------------------------------------------------------------------------------- 1 | { 2 | state: { 3 | file_system: { 4 | /watch/file: 1 5 | } 6 | } 7 | events: [ 8 | { kind: "create-any", paths: ["/watch/file"] } 9 | ] 10 | expected: { 11 | queues: { 12 | /watch/file: { 13 | events: [ 14 | { kind: "create-any", paths: ["*"] } 15 | ] 16 | } 17 | } 18 | cache: { 19 | /watch/file: 1 20 | } 21 | events: { 22 | none: [] 23 | short: [] 24 | long: [ 25 | { kind: "create-any", paths: ["/watch/file"] } 26 | ] 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /notify-debouncer-full/test_cases/add_create_event_after_remove_event.hjson: -------------------------------------------------------------------------------- 1 | { 2 | state: { 3 | queues: { 4 | /watch/file: { 5 | events: [ 6 | { kind: "remove-any", paths: ["*"] } 7 | ] 8 | } 9 | } 10 | } 11 | events: [ 12 | { kind: "create-any", paths: ["/watch/file"] } 13 | ] 14 | expected: { 15 | queues: { 16 | /watch/file: { 17 | events: [ 18 | { kind: "remove-any", paths: ["*"] } 19 | { kind: "create-any", paths: ["*"] } 20 | ] 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /notify-debouncer-full/test_cases/add_errors.hjson: -------------------------------------------------------------------------------- 1 | { 2 | state: {} 3 | errors: [ 4 | { kind: "max-files-watch" } 5 | { kind: "path-not-found", paths: ["/watch/file"] } 6 | ] 7 | expected: { 8 | errors: [ 9 | { kind: "max-files-watch" } 10 | { kind: "path-not-found", paths: ["/watch/file"] } 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /notify-debouncer-full/test_cases/add_modify_content_event_after_create_event.hjson: -------------------------------------------------------------------------------- 1 | // https://github.com/spacedriveapp/spacedrive/blob/90a350946914be7f91ba692887ca03db659d530a/core/src/location/manager/watcher/macos.rs 2 | 3 | // MacOS emits a Create File and then an Update Content event when a file is created. 4 | { 5 | state: { 6 | queues: { 7 | /watch/file: { 8 | events: [ 9 | { kind: "create-file", paths: ["*"] } 10 | ] 11 | } 12 | } 13 | } 14 | events: [ 15 | { kind: "modify-data-content", paths: ["/watch/file"] } 16 | ] 17 | expected: { 18 | queues: { 19 | /watch/file: { 20 | events: [ 21 | { kind: "create-file", paths: ["*"] } 22 | ] 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /notify-debouncer-full/test_cases/add_remove_event.hjson: -------------------------------------------------------------------------------- 1 | { 2 | state: {} 3 | events: [ 4 | { kind: "remove-any", paths: ["/watch/file"] } 5 | ] 6 | expected: { 7 | queues: { 8 | /watch/file: { 9 | events: [ 10 | { kind: "remove-any", paths: ["*"] } 11 | ] 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /notify-debouncer-full/test_cases/add_remove_event_after_create_and_modify_event.hjson: -------------------------------------------------------------------------------- 1 | { 2 | state: { 3 | queues: { 4 | /watch/file: { 5 | events: [ 6 | { kind: "create-any", paths: ["*"] } 7 | { kind: "modify-data-any", paths: ["*"] } 8 | ] 9 | } 10 | } 11 | } 12 | events: [ 13 | { kind: "remove-any", paths: ["/watch/file"] } 14 | ] 15 | expected: {} 16 | } 17 | -------------------------------------------------------------------------------- /notify-debouncer-full/test_cases/add_remove_event_after_create_event.hjson: -------------------------------------------------------------------------------- 1 | { 2 | state: { 3 | queues: { 4 | /watch/file: { 5 | events: [ 6 | { kind: "create-any", paths: ["*"] } 7 | ] 8 | } 9 | } 10 | } 11 | events: [ 12 | { kind: "remove-any", paths: ["/watch/file"] } 13 | ] 14 | expected: {} 15 | } 16 | -------------------------------------------------------------------------------- /notify-debouncer-full/test_cases/add_remove_event_after_modify_event.hjson: -------------------------------------------------------------------------------- 1 | { 2 | state: { 3 | queues: { 4 | /watch/file: { 5 | events: [ 6 | { kind: "modify-data-any", paths: ["*"] } 7 | ] 8 | } 9 | } 10 | } 11 | events: [ 12 | { kind: "remove-any", paths: ["/watch/file"] } 13 | ] 14 | expected: { 15 | queues: { 16 | /watch/file: { 17 | events: [ 18 | { kind: "remove-any", paths: ["*"] } 19 | ] 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /notify-debouncer-full/test_cases/add_remove_parent_event_after_remove_child_event.hjson: -------------------------------------------------------------------------------- 1 | { 2 | state: { 3 | cache: { 4 | /watch: 1 5 | /watch/parent: 2 6 | /watch/parent/child: 3 7 | } 8 | } 9 | events: [ 10 | { kind: "remove-any", paths: ["/watch/parent/child"] } 11 | { kind: "remove-any", paths: ["/watch/parent"] } 12 | ] 13 | expected: { 14 | queues: { 15 | /watch/parent: { 16 | events: [ 17 | { kind: "remove-any", paths: ["*"] } 18 | ] 19 | } 20 | } 21 | cache: { 22 | /watch: 1 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /notify-debouncer-full/test_cases/add_rename_both_event.hjson: -------------------------------------------------------------------------------- 1 | { 2 | state: {} 3 | events: [ 4 | { kind: "rename-both", paths: ["/watch/source", "/watch/target"] } 5 | ] 6 | expected: {} 7 | } 8 | -------------------------------------------------------------------------------- /notify-debouncer-full/test_cases/add_rename_from_and_to_event.hjson: -------------------------------------------------------------------------------- 1 | { 2 | state: {} 3 | events: [ 4 | { kind: "rename-from", paths: ["/watch/source"], tracker: 1 } 5 | { kind: "rename-to", paths: ["/watch/target"], tracker: 1 } 6 | ] 7 | expected: { 8 | queues: { 9 | /watch/target: { 10 | events: [ 11 | { kind: "rename-both", paths: ["/watch/source", "/watch/target"], tracker: 1 } 12 | ] 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /notify-debouncer-full/test_cases/add_rename_from_and_to_event_after_create.hjson: -------------------------------------------------------------------------------- 1 | { 2 | state: { 3 | queues: { 4 | /watch/source: { 5 | events: [ 6 | { kind: "create-any", paths: ["*"] } 7 | ] 8 | } 9 | } 10 | } 11 | events: [ 12 | { kind: "rename-from", paths: ["/watch/source"], tracker: 1 } 13 | { kind: "rename-to", paths: ["/watch/target"], tracker: 1 } 14 | ] 15 | expected: { 16 | queues: { 17 | /watch/target: { 18 | events: [ 19 | { kind: "create-any", paths: ["*"] } 20 | ] 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /notify-debouncer-full/test_cases/add_rename_from_and_to_event_after_modify_content.hjson: -------------------------------------------------------------------------------- 1 | { 2 | state: { 3 | queues: { 4 | /watch/source: { 5 | events: [ 6 | { kind: "modify-data-content", paths: ["*"] } 7 | ] 8 | } 9 | } 10 | } 11 | events: [ 12 | { kind: "rename-from", paths: ["/watch/source"], tracker: 1 } 13 | { kind: "rename-to", paths: ["/watch/target"], tracker: 1 } 14 | ] 15 | expected: { 16 | queues: { 17 | /watch/target: { 18 | events: [ 19 | { kind: "rename-both", paths: ["/watch/source", "/watch/target"], tracker: 1 } 20 | { kind: "modify-data-content", paths: ["*"] } 21 | ] 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /notify-debouncer-full/test_cases/add_rename_from_and_to_event_after_rename.hjson: -------------------------------------------------------------------------------- 1 | { 2 | state: { 3 | queues: { 4 | /watch/temp: { 5 | events: [ 6 | { kind: "rename-both", paths: ["/watch/source", "/watch/temp"], tracker: 1, time: 1 } 7 | ] 8 | } 9 | } 10 | } 11 | events: [ 12 | { kind: "rename-from", paths: ["/watch/temp"], tracker: 2, time: 2 } 13 | { kind: "rename-to", paths: ["/watch/target"], tracker: 2, time: 3 } 14 | ] 15 | expected: { 16 | queues: { 17 | /watch/target: { 18 | events: [ 19 | { kind: "rename-both", paths: ["/watch/source", "/watch/target"], tracker: 2, time: 1 } 20 | ] 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /notify-debouncer-full/test_cases/add_rename_from_and_to_event_override_created.hjson: -------------------------------------------------------------------------------- 1 | { 2 | state: { 3 | queues: { 4 | /watch/target: { 5 | events: [ 6 | { kind: "create-any", paths: ["*"] } 7 | ] 8 | } 9 | } 10 | } 11 | events: [ 12 | { kind: "rename-from", paths: ["/watch/source"], tracker: 1 } 13 | { kind: "rename-to", paths: ["/watch/target"], tracker: 1 } 14 | ] 15 | expected: { 16 | queues: { 17 | /watch/target: { 18 | events: [ 19 | { kind: "rename-both", paths: ["/watch/source", "/watch/target"], tracker: 1 } 20 | ] 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /notify-debouncer-full/test_cases/add_rename_from_and_to_event_override_modified.hjson: -------------------------------------------------------------------------------- 1 | { 2 | state: { 3 | queues: { 4 | /watch/target: { 5 | events: [ 6 | { kind: "modify-data-any", paths: ["*"] } 7 | ] 8 | } 9 | } 10 | cache: { 11 | /watch/target: 1 12 | /watch/source: 2 13 | } 14 | file_system: { 15 | /watch/target: 2 16 | } 17 | } 18 | events: [ 19 | { kind: "rename-from", paths: ["/watch/source"], tracker: 1 } 20 | { kind: "rename-to", paths: ["/watch/target"], tracker: 1 } 21 | ] 22 | expected: { 23 | queues: { 24 | /watch/target: { 25 | events: [ 26 | { kind: "remove-any", paths: ["*"], info: "override" } 27 | { kind: "rename-both", paths: ["/watch/source", "/watch/target"], tracker: 1 } 28 | ] 29 | } 30 | } 31 | cache: { 32 | /watch/target: 2 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /notify-debouncer-full/test_cases/add_rename_from_and_to_event_override_removed.hjson: -------------------------------------------------------------------------------- 1 | { 2 | state: { 3 | queues: { 4 | /watch/target: { 5 | events: [ 6 | { kind: "remove-any", paths: ["*"] } 7 | ] 8 | } 9 | } 10 | } 11 | events: [ 12 | { kind: "rename-from", paths: ["/watch/source"], tracker: 1 } 13 | { kind: "rename-to", paths: ["/watch/target"], tracker: 1 } 14 | ] 15 | expected: { 16 | queues: { 17 | /watch/target: { 18 | events: [ 19 | { kind: "remove-any", paths: ["*"] } 20 | { kind: "rename-both", paths: ["/watch/source", "/watch/target"], tracker: 1 } 21 | ] 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /notify-debouncer-full/test_cases/add_rename_from_and_to_event_with_different_file_ids.hjson: -------------------------------------------------------------------------------- 1 | { 2 | state: { 3 | cache: { 4 | /watch/source: 1 5 | } 6 | file_system: { 7 | /watch/target: 2 8 | } 9 | } 10 | events: [ 11 | { kind: "rename-from", paths: ["/watch/source"] } 12 | { kind: "rename-to", paths: ["/watch/target"] } 13 | ] 14 | expected: { 15 | queues: { 16 | /watch/source: { 17 | events: [ 18 | { kind: "rename-from", paths: ["*"] } 19 | ] 20 | } 21 | /watch/target: { 22 | events: [ 23 | { kind: "rename-to", paths: ["*"] } 24 | ] 25 | } 26 | } 27 | cache: { 28 | /watch/target: 2 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /notify-debouncer-full/test_cases/add_rename_from_and_to_event_with_different_tracker.hjson: -------------------------------------------------------------------------------- 1 | { 2 | state: {} 3 | events: [ 4 | { kind: "rename-from", paths: ["/watch/source"], tracker: 1 } 5 | { kind: "rename-to", paths: ["/watch/target"], tracker: 2 } 6 | ] 7 | expected: { 8 | queues: { 9 | /watch/source: { 10 | events: [ 11 | { kind: "rename-from", paths: ["*"], tracker: 1 } 12 | ] 13 | } 14 | /watch/target: { 15 | events: [ 16 | { kind: "rename-to", paths: ["*"], tracker: 2 } 17 | ] 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /notify-debouncer-full/test_cases/add_rename_from_and_to_event_with_file_ids.hjson: -------------------------------------------------------------------------------- 1 | { 2 | state: { 3 | cache: { 4 | /watch/source: 1 5 | } 6 | file_system: { 7 | /watch/target: 1 8 | } 9 | } 10 | events: [ 11 | { kind: "rename-from", paths: ["/watch/source"] } 12 | { kind: "rename-to", paths: ["/watch/target"] } 13 | ] 14 | expected: { 15 | queues: { 16 | /watch/target: { 17 | events: [ 18 | { kind: "rename-both", paths: ["/watch/source", "/watch/target"] } 19 | ] 20 | } 21 | } 22 | cache: { 23 | /watch/target: 1 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /notify-debouncer-full/test_cases/add_rename_from_event.hjson: -------------------------------------------------------------------------------- 1 | { 2 | state: {} 3 | events: [ 4 | { kind: "rename-from", paths: ["/watch/source"] } 5 | ] 6 | expected: { 7 | queues: { 8 | /watch/source: { 9 | events: [ 10 | { kind: "rename-from", paths: ["*"] } 11 | ] 12 | } 13 | } 14 | rename_event: { kind: "rename-from", paths: ["/watch/source"] } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /notify-debouncer-full/test_cases/add_rename_from_event_after_create_and_modify_event.hjson: -------------------------------------------------------------------------------- 1 | { 2 | state: { 3 | queues: { 4 | /watch/file: { 5 | events: [ 6 | { kind: "create-any", paths: ["*"] } 7 | { kind: "modify-data-any", paths: ["*"] } 8 | ] 9 | } 10 | } 11 | } 12 | events: [ 13 | { kind: "rename-from", paths: ["/watch/file"] } 14 | ] 15 | expected: { 16 | queues: { 17 | /watch/file: { 18 | events: [ 19 | { kind: "create-any", paths: ["*"] } 20 | { kind: "modify-data-any", paths: ["*"] } 21 | { kind: "rename-from", paths: ["*"] } 22 | ] 23 | } 24 | } 25 | rename_event: { kind: "rename-from", paths: ["/watch/file"] } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /notify-debouncer-full/test_cases/add_rename_from_event_after_create_event.hjson: -------------------------------------------------------------------------------- 1 | { 2 | state: { 3 | queues: { 4 | /watch/file: { 5 | events: [ 6 | { kind: "create-any", paths: ["*"] } 7 | ] 8 | } 9 | } 10 | } 11 | events: [ 12 | { kind: "rename-from", paths: ["/watch/file"] } 13 | ] 14 | expected: { 15 | queues: { 16 | /watch/file: { 17 | events: [ 18 | { kind: "create-any", paths: ["*"] } 19 | { kind: "rename-from", paths: ["*"] } 20 | ] 21 | } 22 | } 23 | rename_event: { kind: "rename-from", paths: ["/watch/file"] } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /notify-debouncer-full/test_cases/add_rename_from_event_after_modify_event.hjson: -------------------------------------------------------------------------------- 1 | { 2 | state: { 3 | queues: { 4 | /watch/file: { 5 | events: [ 6 | { kind: "modify-data-any", paths: ["*"] } 7 | ] 8 | } 9 | } 10 | } 11 | events: [ 12 | { kind: "rename-from", paths: ["/watch/file"] } 13 | ] 14 | expected: { 15 | queues: { 16 | /watch/file: { 17 | events: [ 18 | { kind: "modify-data-any", paths: ["*"] } 19 | { kind: "rename-from", paths: ["*"] } 20 | ] 21 | } 22 | } 23 | rename_event: { kind: "rename-from", paths: ["/watch/file"] } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /notify-debouncer-full/test_cases/add_rename_from_event_after_rename_from_event.hjson: -------------------------------------------------------------------------------- 1 | { 2 | state: { 3 | queues: { 4 | /watch/file-a: { 5 | events: [ 6 | { kind: "rename-from", paths: ["*"] } 7 | ] 8 | } 9 | } 10 | rename_event: { kind: "rename-from", paths: ["/watch/file-a"] } 11 | } 12 | events: [ 13 | { kind: "rename-from", paths: ["/watch/file-b"] } 14 | ] 15 | expected: { 16 | queues: { 17 | /watch/file-a: { 18 | events: [ 19 | { kind: "rename-from", paths: ["*"] } 20 | ] 21 | } 22 | /watch/file-b: { 23 | events: [ 24 | { kind: "rename-from", paths: ["*"] } 25 | ] 26 | } 27 | } 28 | rename_event: { kind: "rename-from", paths: ["/watch/file-b"] } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /notify-debouncer-full/test_cases/add_rename_to_dir_event.hjson: -------------------------------------------------------------------------------- 1 | { 2 | state: { 3 | file_system: { 4 | /watch/parent: 1 5 | /watch/parent/child: 2 6 | } 7 | } 8 | events: [ 9 | { kind: "rename-to", paths: ["/watch/parent"] } 10 | ] 11 | expected: { 12 | queues: { 13 | /watch/parent: { 14 | events: [ 15 | { kind: "rename-to", paths: ["*"] } 16 | ] 17 | } 18 | } 19 | cache: { 20 | /watch/parent: 1 21 | /watch/parent/child: 2 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /notify-debouncer-full/test_cases/add_rename_to_event.hjson: -------------------------------------------------------------------------------- 1 | { 2 | state: {} 3 | events: [ 4 | { kind: "rename-to", paths: ["/watch/target"] } 5 | ] 6 | expected: { 7 | queues: { 8 | /watch/target: { 9 | events: [ 10 | { kind: "rename-to", paths: ["*"] } 11 | ] 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /notify-debouncer-full/test_cases/emit_close_events_only_once.hjson: -------------------------------------------------------------------------------- 1 | { 2 | state: {} 3 | events: [ 4 | { kind: "modify-data-any", paths: ["/watch/file"], time: 1 } 5 | { kind: "access-close-write", paths: ["/watch/file"], time: 2 } 6 | { kind: "modify-data-any", paths: ["/watch/file"], time: 3 } 7 | { kind: "access-close-write", paths: ["/watch/file"], time: 4 } 8 | ] 9 | expected: { 10 | queues: { 11 | /watch/file: { 12 | events: [ 13 | { kind: "modify-data-any", paths: ["*"], time: 1 } 14 | { kind: "access-close-write", paths: ["*"], time: 2 } 15 | { kind: "modify-data-any", paths: ["*"], time: 3 } 16 | { kind: "access-close-write", paths: ["*"], time: 4 } 17 | ] 18 | } 19 | } 20 | events: { 21 | short: [] 22 | long: [ 23 | { kind: "modify-data-any", paths: ["/watch/file"], time: 3 } 24 | { kind: "access-close-write", paths: ["/watch/file"], time: 4 } 25 | ] 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /notify-debouncer-full/test_cases/emit_continuous_modify_content_events.hjson: -------------------------------------------------------------------------------- 1 | { 2 | state: { 3 | timeout: 5 4 | } 5 | events: [ 6 | { kind: "modify-data-content", paths: ["/watch/file"], time: 1 } 7 | { kind: "modify-data-content", paths: ["/watch/file"], time: 2 } 8 | { kind: "modify-data-content", paths: ["/watch/file"], time: 3 } 9 | ] 10 | expected: { 11 | queues: { 12 | /watch/file: { 13 | events: [ 14 | { kind: "modify-data-content", paths: ["*"], time: 1 } 15 | { kind: "modify-data-content", paths: ["*"], time: 2 } 16 | { kind: "modify-data-content", paths: ["*"], time: 3 } 17 | ] 18 | } 19 | } 20 | events: { 21 | 1: [] 22 | 2: [] 23 | 3: [] 24 | 4: [] 25 | 5: [] 26 | 6: [ 27 | { kind: "modify-data-content", paths: ["/watch/file"], time: 1 } 28 | ] 29 | 7: [ 30 | { kind: "modify-data-content", paths: ["/watch/file"], time: 2 } 31 | ] 32 | 8: [ 33 | { kind: "modify-data-content", paths: ["/watch/file"], time: 3 } 34 | ] 35 | 9: [ 36 | { kind: "modify-data-content", paths: ["/watch/file"], time: 3 } 37 | ] 38 | 10: [ 39 | { kind: "modify-data-content", paths: ["/watch/file"], time: 3 } 40 | ] 41 | 100: [ 42 | { kind: "modify-data-content", paths: ["/watch/file"], time: 3 } 43 | ] 44 | 1000: [ 45 | { kind: "modify-data-content", paths: ["/watch/file"], time: 3 } 46 | ] 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /notify-debouncer-full/test_cases/emit_events_in_chronological_order.hjson: -------------------------------------------------------------------------------- 1 | { 2 | state: { 3 | timeout: 5 4 | } 5 | events: [ 6 | { kind: "modify-data-content", paths: ["/watch/file-a"], time: 1 } 7 | { kind: "modify-data-content", paths: ["/watch/file-b"], time: 3 } 8 | { kind: "modify-data-content", paths: ["/watch/file-c"], time: 4 } 9 | { kind: "modify-metadata-write-time", paths: ["/watch/file-b"], time: 7 } 10 | { kind: "modify-metadata-write-time", paths: ["/watch/file-c"], time: 8 } 11 | { kind: "modify-metadata-write-time", paths: ["/watch/file-a"], time: 9 } 12 | ] 13 | expected: { 14 | queues: { 15 | /watch/file-a: { 16 | events: [ 17 | { kind: "modify-data-content", paths: ["*"], time: 1 } 18 | { kind: "modify-metadata-write-time", paths: ["*"], time: 9 } 19 | ] 20 | } 21 | /watch/file-b: { 22 | events: [ 23 | { kind: "modify-data-content", paths: ["*"], time: 3 } 24 | { kind: "modify-metadata-write-time", paths: ["*"], time: 7 } 25 | ] 26 | } 27 | /watch/file-c: { 28 | events: [ 29 | { kind: "modify-data-content", paths: ["*"], time: 4 } 30 | { kind: "modify-metadata-write-time", paths: ["*"], time: 8 } 31 | ] 32 | } 33 | } 34 | events: { 35 | long: [ 36 | { kind: "modify-data-content", paths: ["/watch/file-a"], time: 1 } 37 | { kind: "modify-data-content", paths: ["/watch/file-b"], time: 3 } 38 | { kind: "modify-data-content", paths: ["/watch/file-c"], time: 4 } 39 | { kind: "modify-metadata-write-time", paths: ["/watch/file-b"], time: 7 } 40 | { kind: "modify-metadata-write-time", paths: ["/watch/file-c"], time: 8 } 41 | { kind: "modify-metadata-write-time", paths: ["/watch/file-a"], time: 9 } 42 | ] 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /notify-debouncer-full/test_cases/emit_events_with_a_prepended_rename_event.hjson: -------------------------------------------------------------------------------- 1 | { 2 | state: { 3 | timeout: 5 4 | } 5 | events: [ 6 | { kind: "modify-data-content", paths: ["/watch/source"], time: 1 } 7 | { kind: "modify-data-content", paths: ["/watch/source"], time: 4 } 8 | { kind: "rename-from", paths: ["/watch/source"], tracker: 1, time: 7 } 9 | { kind: "rename-to", paths: ["/watch/target"], tracker: 1, time: 8 } 10 | { kind: "modify-metadata-write-time", paths: ["/watch/target"], time: 9 } 11 | ] 12 | expected: { 13 | queues: { 14 | /watch/target: { 15 | events: [ 16 | { kind: "rename-both", paths: ["/watch/source", "/watch/target"], tracker: 1, time: 7 } 17 | { kind: "modify-data-content", paths: ["*"], time: 1 } 18 | { kind: "modify-data-content", paths: ["*"], time: 4 } 19 | { kind: "modify-metadata-write-time", paths: ["*"], time: 9 } 20 | ] 21 | } 22 | } 23 | events: { 24 | 11: [] 25 | 12: [ 26 | { kind: "rename-both", paths: ["/watch/source", "/watch/target"], tracker: 1, time: 7 } 27 | { kind: "modify-data-content", paths: ["/watch/target"], time: 4 } 28 | ] 29 | 14: [ 30 | { kind: "rename-both", paths: ["/watch/source", "/watch/target"], tracker: 1, time: 7 } 31 | { kind: "modify-data-content", paths: ["/watch/target"], time: 4 } 32 | { kind: "modify-metadata-write-time", paths: ["/watch/target"], time: 9 } 33 | ] 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /notify-debouncer-full/test_cases/emit_modify_event_after_close_event.hjson: -------------------------------------------------------------------------------- 1 | { 2 | state: {} 3 | events: [ 4 | { kind: "modify-data-any", paths: ["/watch/file"], time: 1 } 5 | { kind: "access-close-write", paths: ["/watch/file"], time: 2 } 6 | { kind: "modify-data-any", paths: ["/watch/file"], time: 3 } 7 | { kind: "access-close-write", paths: ["/watch/file"], time: 4 } 8 | { kind: "modify-data-any", paths: ["/watch/file"], time: 5 } 9 | ] 10 | expected: { 11 | queues: { 12 | /watch/file: { 13 | events: [ 14 | { kind: "modify-data-any", paths: ["*"], time: 1 } 15 | { kind: "access-close-write", paths: ["*"], time: 2 } 16 | { kind: "modify-data-any", paths: ["*"], time: 3 } 17 | { kind: "access-close-write", paths: ["*"], time: 4 } 18 | { kind: "modify-data-any", paths: ["*"], time: 5 } 19 | ] 20 | } 21 | } 22 | events: { 23 | short: [] 24 | long: [ 25 | { kind: "access-close-write", paths: ["/watch/file"], time: 4 } 26 | { kind: "modify-data-any", paths: ["/watch/file"], time: 5 } 27 | ] 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /notify-debouncer-full/test_cases/emit_needs_rescan_event.hjson: -------------------------------------------------------------------------------- 1 | { 2 | state: { 3 | queues: { 4 | /watch/file-a: { 5 | events: [ 6 | { kind: "create-any", paths: ["*"], time: 1 } 7 | ] 8 | } 9 | /watch/file-b: { 10 | events: [ 11 | { kind: "create-any", paths: ["*"], time: 2 } 12 | ] 13 | } 14 | } 15 | cache: { 16 | /watch/file-a: 1 17 | /watch/file-b: 2 18 | } 19 | file_system: { 20 | /watch/file-a: 1 21 | /watch/file-b: 2 22 | /watch/file-c: 3 23 | } 24 | } 25 | events: [ 26 | { kind: "other", flags: ["rescan"], time: 3 } 27 | ] 28 | expected: { 29 | queues: { 30 | /watch/file-a: { 31 | events: [ 32 | { kind: "create-any", paths: ["*"], time: 1 } 33 | ] 34 | } 35 | /watch/file-b: { 36 | events: [ 37 | { kind: "create-any", paths: ["*"], time: 2 } 38 | ] 39 | } 40 | } 41 | rescan_event: { kind: "other", flags: ["rescan"], time: 3 } 42 | cache: { 43 | /watch/file-a: 1 44 | /watch/file-b: 2 45 | /watch/file-c: 3 46 | } 47 | events: { 48 | short: [] 49 | long: [ 50 | { kind: "create-any", paths: ["/watch/file-a"], time: 1 } 51 | { kind: "create-any", paths: ["/watch/file-b"], time: 2 } 52 | { kind: "other", flags: ["rescan"], time: 3 } 53 | ] 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /notify-debouncer-full/test_cases/read_file_id_without_create_event.hjson: -------------------------------------------------------------------------------- 1 | { 2 | state: { 3 | file_system: { 4 | /watch/file: 1 5 | } 6 | } 7 | events: [ 8 | { kind: "modify-data-any", paths: ["/watch/file"] } 9 | ] 10 | expected: { 11 | queues: { 12 | /watch/file: { 13 | events: [ 14 | { kind: "modify-data-any", paths: ["*"] } 15 | ] 16 | } 17 | } 18 | cache: { 19 | /watch/file: 1 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /notify-debouncer-full/test_cases/sort_events_chronologically.hjson: -------------------------------------------------------------------------------- 1 | { 2 | state: { 3 | queues: { 4 | /watch/file-1: { 5 | events: [ 6 | { kind: "create-any", paths: ["*"], time: 2 } 7 | { kind: "modify-any", paths: ["*"], time: 3 } 8 | ] 9 | } 10 | /watch/file-2: { 11 | events: [ 12 | { kind: "create-any", paths: ["*"], time: 1 } 13 | { kind: "modify-any", paths: ["*"], time: 4 } 14 | ] 15 | } 16 | } 17 | } 18 | expected: { 19 | queues: { 20 | /watch/file-1: { 21 | events: [ 22 | { kind: "create-any", paths: ["*"], time: 2 } 23 | { kind: "modify-any", paths: ["*"], time: 3 } 24 | ] 25 | } 26 | /watch/file-2: { 27 | events: [ 28 | { kind: "create-any", paths: ["*"], time: 1 } 29 | { kind: "modify-any", paths: ["*"], time: 4 } 30 | ] 31 | } 32 | } 33 | events: { 34 | long: [ 35 | { kind: "create-any", paths: ["/watch/file-2"], time: 1 } 36 | { kind: "create-any", paths: ["/watch/file-1"], time: 2 } 37 | { kind: "modify-any", paths: ["/watch/file-1"], time: 3 } 38 | { kind: "modify-any", paths: ["/watch/file-2"], time: 4 } 39 | ] 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /notify-debouncer-full/test_cases/sort_events_with_reordering.hjson: -------------------------------------------------------------------------------- 1 | { 2 | state: { 3 | queues: { 4 | /watch/file-1: { 5 | events: [ 6 | { kind: "create-any", paths: ["*"], time: 2 } 7 | { kind: "modify-any", paths: ["*"], time: 3 } 8 | ] 9 | } 10 | /watch/file-2: { 11 | events: [ 12 | { kind: "rename-to", paths: ["*"], time: 4 } 13 | { kind: "modify-any", paths: ["*"], time: 1 } 14 | ] 15 | } 16 | } 17 | } 18 | expected: { 19 | queues: { 20 | /watch/file-1: { 21 | events: [ 22 | { kind: "create-any", paths: ["*"], time: 2 } 23 | { kind: "modify-any", paths: ["*"], time: 3 } 24 | ] 25 | } 26 | /watch/file-2: { 27 | events: [ 28 | { kind: "rename-to", paths: ["*"], time: 4 } 29 | { kind: "modify-any", paths: ["*"], time: 1 } 30 | ] 31 | } 32 | } 33 | events: { 34 | long: [ 35 | { kind: "create-any", paths: ["/watch/file-1"], time: 2 } 36 | { kind: "modify-any", paths: ["/watch/file-1"], time: 3 } 37 | { kind: "rename-to", paths: ["/watch/file-2"], time: 4 } 38 | { kind: "modify-any", paths: ["/watch/file-2"], time: 1 } 39 | ] 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /notify-debouncer-mini/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "notify-debouncer-mini" 3 | version = "0.6.0" 4 | description = "notify mini debouncer for events" 5 | documentation = "https://docs.rs/notify-debouncer-mini" 6 | authors = ["Aron Heinecke "] 7 | keywords = ["events", "filesystem", "notify", "watch"] 8 | license = "MIT OR Apache-2.0" 9 | readme = "README.md" 10 | rust-version.workspace = true 11 | edition.workspace = true 12 | homepage.workspace = true 13 | repository.workspace = true 14 | 15 | [features] 16 | default = ["macos_fsevent"] 17 | serde = ["notify-types/serde"] 18 | crossbeam-channel = ["dep:crossbeam-channel", "notify/crossbeam-channel"] 19 | flume = ["dep:flume", "notify/flume"] 20 | macos_fsevent = ["notify/macos_fsevent"] 21 | macos_kqueue = ["notify/macos_kqueue"] 22 | serialization-compat-6 = ["notify/serialization-compat-6"] 23 | 24 | [dependencies] 25 | notify.workspace = true 26 | notify-types.workspace = true 27 | crossbeam-channel = { workspace = true, optional = true } 28 | flume = { workspace = true, optional = true } 29 | log.workspace = true 30 | tempfile.workspace = true 31 | -------------------------------------------------------------------------------- /notify-debouncer-mini/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2023 Notify Contributors 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /notify-debouncer-mini/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Notify Contributors 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /notify-debouncer-mini/README.md: -------------------------------------------------------------------------------- 1 | # Notify debouncer 2 | 3 | [![» Docs](https://flat.badgen.net/badge/api/docs.rs/df3600)][docs] 4 | 5 | Tiny debouncer for [notify]. Filters incoming events and emits only one event per timeframe per file. 6 | 7 | ## Features 8 | 9 | - `crossbeam-channel` passed down to notify, off by default 10 | 11 | - `flume` passed down to notify, off by default 12 | 13 | - `serde` for serde support of event types, off by default 14 | 15 | - `serialization-compat-6` passed down to notify, off by default 16 | 17 | [docs]: https://docs.rs/notify-debouncer-mini 18 | [notify]: https://crates.io/crates/notify 19 | -------------------------------------------------------------------------------- /notify-types/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "notify-types" 3 | version = "2.0.0" 4 | description = "Types used by the notify crate" 5 | documentation = "https://docs.rs/notify-types" 6 | readme = "../README.md" 7 | license = "MIT OR Apache-2.0" 8 | keywords = ["events", "filesystem", "notify", "watch"] 9 | categories = ["filesystem"] 10 | authors = ["Daniel Faust "] 11 | rust-version.workspace = true 12 | edition.workspace = true 13 | homepage.workspace = true 14 | repository.workspace = true 15 | 16 | [features] 17 | serialization-compat-6 = [] 18 | 19 | [dependencies] 20 | serde = { workspace = true, optional = true } 21 | web-time = { workspace = true, optional = true } 22 | 23 | [dev-dependencies] 24 | serde_json.workspace = true 25 | insta.workspace = true 26 | rstest.workspace = true 27 | -------------------------------------------------------------------------------- /notify-types/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2023 Notify Contributors 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /notify-types/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Notify Contributors 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /notify-types/src/debouncer_full.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Deref, DerefMut}; 2 | 3 | #[cfg(feature = "web-time")] 4 | use web_time::Instant; 5 | 6 | #[cfg(not(feature = "web-time"))] 7 | use std::time::Instant; 8 | 9 | use crate::event::Event; 10 | 11 | /// A debounced event is emitted after a short delay. 12 | #[derive(Debug, Clone, PartialEq, Eq)] 13 | pub struct DebouncedEvent { 14 | /// The original event. 15 | pub event: Event, 16 | 17 | /// The time at which the event occurred. 18 | pub time: Instant, 19 | } 20 | 21 | impl DebouncedEvent { 22 | pub fn new(event: Event, time: Instant) -> Self { 23 | Self { event, time } 24 | } 25 | } 26 | 27 | impl Deref for DebouncedEvent { 28 | type Target = Event; 29 | 30 | fn deref(&self) -> &Self::Target { 31 | &self.event 32 | } 33 | } 34 | 35 | impl DerefMut for DebouncedEvent { 36 | fn deref_mut(&mut self) -> &mut Self::Target { 37 | &mut self.event 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /notify-types/src/debouncer_mini.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | #[cfg(feature = "serde")] 4 | use serde::{Deserialize, Serialize}; 5 | 6 | /// A debounced event kind. 7 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 8 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 9 | #[non_exhaustive] 10 | pub enum DebouncedEventKind { 11 | /// No precise events 12 | Any, 13 | /// Event but debounce timed out (for example continuous writes) 14 | AnyContinuous, 15 | } 16 | 17 | /// A debounced event. 18 | /// 19 | /// Does not emit any specific event type on purpose, only distinguishes between an any event and a continuous any event. 20 | #[derive(Clone, Debug, Eq, Hash, PartialEq)] 21 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 22 | pub struct DebouncedEvent { 23 | /// Event path 24 | pub path: PathBuf, 25 | /// Event kind 26 | pub kind: DebouncedEventKind, 27 | } 28 | 29 | impl DebouncedEvent { 30 | #[inline(always)] 31 | pub fn new(path: PathBuf, kind: DebouncedEventKind) -> Self { 32 | Self { path, kind } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /notify-types/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod debouncer_full; 2 | pub mod debouncer_mini; 3 | pub mod event; 4 | 5 | #[cfg(test)] 6 | mod tests { 7 | use super::*; 8 | 9 | #[test] 10 | fn test_debug_impl() { 11 | macro_rules! assert_debug_impl { 12 | ($t:ty) => {{ 13 | #[allow(dead_code)] 14 | trait NeedsDebug: std::fmt::Debug {} 15 | impl NeedsDebug for $t {} 16 | }}; 17 | } 18 | 19 | assert_debug_impl!(event::AccessKind); 20 | assert_debug_impl!(event::AccessMode); 21 | assert_debug_impl!(event::CreateKind); 22 | assert_debug_impl!(event::DataChange); 23 | assert_debug_impl!(event::EventAttributes); 24 | assert_debug_impl!(event::Flag); 25 | assert_debug_impl!(event::MetadataKind); 26 | assert_debug_impl!(event::ModifyKind); 27 | assert_debug_impl!(event::RemoveKind); 28 | assert_debug_impl!(event::RenameMode); 29 | assert_debug_impl!(event::Event); 30 | assert_debug_impl!(event::EventKind); 31 | assert_debug_impl!(debouncer_mini::DebouncedEvent); 32 | assert_debug_impl!(debouncer_mini::DebouncedEventKind); 33 | assert_debug_impl!(debouncer_full::DebouncedEvent); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /notify-types/src/snapshots/notify_types__event__tests__access-any.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: notify/src/event.rs 3 | expression: json 4 | --- 5 | {"type":"access","kind":"any","paths":[],"attrs":{}} 6 | -------------------------------------------------------------------------------- /notify-types/src/snapshots/notify_types__event__tests__access-close-any.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: notify/src/event.rs 3 | expression: json 4 | --- 5 | {"type":"access","kind":"close","mode":"any","paths":[],"attrs":{}} 6 | -------------------------------------------------------------------------------- /notify-types/src/snapshots/notify_types__event__tests__access-close-execute.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: notify/src/event.rs 3 | expression: json 4 | --- 5 | {"type":"access","kind":"close","mode":"execute","paths":[],"attrs":{}} 6 | -------------------------------------------------------------------------------- /notify-types/src/snapshots/notify_types__event__tests__access-close-other.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: notify/src/event.rs 3 | expression: json 4 | --- 5 | {"type":"access","kind":"close","mode":"other","paths":[],"attrs":{}} 6 | -------------------------------------------------------------------------------- /notify-types/src/snapshots/notify_types__event__tests__access-close-read.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: notify/src/event.rs 3 | expression: json 4 | --- 5 | {"type":"access","kind":"close","mode":"read","paths":[],"attrs":{}} 6 | -------------------------------------------------------------------------------- /notify-types/src/snapshots/notify_types__event__tests__access-close-write.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: notify/src/event.rs 3 | expression: json 4 | --- 5 | {"type":"access","kind":"close","mode":"write","paths":[],"attrs":{}} 6 | -------------------------------------------------------------------------------- /notify-types/src/snapshots/notify_types__event__tests__access-open-any.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: notify/src/event.rs 3 | expression: json 4 | --- 5 | {"type":"access","kind":"open","mode":"any","paths":[],"attrs":{}} 6 | -------------------------------------------------------------------------------- /notify-types/src/snapshots/notify_types__event__tests__access-open-execute.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: notify/src/event.rs 3 | expression: json 4 | --- 5 | {"type":"access","kind":"open","mode":"execute","paths":[],"attrs":{}} 6 | -------------------------------------------------------------------------------- /notify-types/src/snapshots/notify_types__event__tests__access-open-other.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: notify/src/event.rs 3 | expression: json 4 | --- 5 | {"type":"access","kind":"open","mode":"other","paths":[],"attrs":{}} 6 | -------------------------------------------------------------------------------- /notify-types/src/snapshots/notify_types__event__tests__access-open-read.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: notify/src/event.rs 3 | expression: json 4 | --- 5 | {"type":"access","kind":"open","mode":"read","paths":[],"attrs":{}} 6 | -------------------------------------------------------------------------------- /notify-types/src/snapshots/notify_types__event__tests__access-open-write.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: notify/src/event.rs 3 | expression: json 4 | --- 5 | {"type":"access","kind":"open","mode":"write","paths":[],"attrs":{}} 6 | -------------------------------------------------------------------------------- /notify-types/src/snapshots/notify_types__event__tests__access-other.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: notify/src/event.rs 3 | expression: json 4 | --- 5 | {"type":"access","kind":"other","paths":[],"attrs":{}} 6 | -------------------------------------------------------------------------------- /notify-types/src/snapshots/notify_types__event__tests__access-read.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: notify/src/event.rs 3 | expression: json 4 | --- 5 | {"type":"access","kind":"read","paths":[],"attrs":{}} 6 | -------------------------------------------------------------------------------- /notify-types/src/snapshots/notify_types__event__tests__any.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: notify/src/event.rs 3 | expression: json 4 | --- 5 | {"type":"any","paths":[],"attrs":{}} 6 | -------------------------------------------------------------------------------- /notify-types/src/snapshots/notify_types__event__tests__create-any.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: notify/src/event.rs 3 | expression: json 4 | --- 5 | {"type":"create","kind":"any","paths":[],"attrs":{}} 6 | -------------------------------------------------------------------------------- /notify-types/src/snapshots/notify_types__event__tests__create-file.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: notify/src/event.rs 3 | expression: json 4 | --- 5 | {"type":"create","kind":"file","paths":[],"attrs":{}} 6 | -------------------------------------------------------------------------------- /notify-types/src/snapshots/notify_types__event__tests__create-folder.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: notify/src/event.rs 3 | expression: json 4 | --- 5 | {"type":"create","kind":"folder","paths":[],"attrs":{}} 6 | -------------------------------------------------------------------------------- /notify-types/src/snapshots/notify_types__event__tests__create-other.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: notify/src/event.rs 3 | expression: json 4 | --- 5 | {"type":"create","kind":"other","paths":[],"attrs":{}} 6 | -------------------------------------------------------------------------------- /notify-types/src/snapshots/notify_types__event__tests__modify-any.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: notify/src/event.rs 3 | expression: json 4 | --- 5 | {"type":"modify","kind":"any","paths":[],"attrs":{}} 6 | -------------------------------------------------------------------------------- /notify-types/src/snapshots/notify_types__event__tests__modify-data-any.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: notify/src/event.rs 3 | expression: json 4 | --- 5 | {"type":"modify","kind":"data","mode":"any","paths":[],"attrs":{}} 6 | -------------------------------------------------------------------------------- /notify-types/src/snapshots/notify_types__event__tests__modify-data-content.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: notify/src/event.rs 3 | expression: json 4 | --- 5 | {"type":"modify","kind":"data","mode":"content","paths":[],"attrs":{}} 6 | -------------------------------------------------------------------------------- /notify-types/src/snapshots/notify_types__event__tests__modify-data-other.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: notify/src/event.rs 3 | expression: json 4 | --- 5 | {"type":"modify","kind":"data","mode":"other","paths":[],"attrs":{}} 6 | -------------------------------------------------------------------------------- /notify-types/src/snapshots/notify_types__event__tests__modify-data-size.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: notify/src/event.rs 3 | expression: json 4 | --- 5 | {"type":"modify","kind":"data","mode":"size","paths":[],"attrs":{}} 6 | -------------------------------------------------------------------------------- /notify-types/src/snapshots/notify_types__event__tests__modify-metadata-accesstime.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: notify/src/event.rs 3 | expression: json 4 | --- 5 | {"type":"modify","kind":"metadata","mode":"access-time","paths":[],"attrs":{}} 6 | -------------------------------------------------------------------------------- /notify-types/src/snapshots/notify_types__event__tests__modify-metadata-any.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: notify/src/event.rs 3 | expression: json 4 | --- 5 | {"type":"modify","kind":"metadata","mode":"any","paths":[],"attrs":{}} 6 | -------------------------------------------------------------------------------- /notify-types/src/snapshots/notify_types__event__tests__modify-metadata-extended.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: notify/src/event.rs 3 | expression: json 4 | --- 5 | {"type":"modify","kind":"metadata","mode":"extended","paths":[],"attrs":{}} 6 | -------------------------------------------------------------------------------- /notify-types/src/snapshots/notify_types__event__tests__modify-metadata-other.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: notify/src/event.rs 3 | expression: json 4 | --- 5 | {"type":"modify","kind":"metadata","mode":"other","paths":[],"attrs":{}} 6 | -------------------------------------------------------------------------------- /notify-types/src/snapshots/notify_types__event__tests__modify-metadata-ownership.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: notify/src/event.rs 3 | expression: json 4 | --- 5 | {"type":"modify","kind":"metadata","mode":"ownership","paths":[],"attrs":{}} 6 | -------------------------------------------------------------------------------- /notify-types/src/snapshots/notify_types__event__tests__modify-metadata-permissions.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: notify/src/event.rs 3 | expression: json 4 | --- 5 | {"type":"modify","kind":"metadata","mode":"permissions","paths":[],"attrs":{}} 6 | -------------------------------------------------------------------------------- /notify-types/src/snapshots/notify_types__event__tests__modify-metadata-writetime.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: notify/src/event.rs 3 | expression: json 4 | --- 5 | {"type":"modify","kind":"metadata","mode":"write-time","paths":[],"attrs":{}} 6 | -------------------------------------------------------------------------------- /notify-types/src/snapshots/notify_types__event__tests__modify-name-any.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: notify/src/event.rs 3 | expression: json 4 | --- 5 | {"type":"modify","kind":"rename","mode":"any","paths":[],"attrs":{}} 6 | -------------------------------------------------------------------------------- /notify-types/src/snapshots/notify_types__event__tests__modify-name-both.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: notify/src/event.rs 3 | expression: json 4 | --- 5 | {"type":"modify","kind":"rename","mode":"both","paths":[],"attrs":{}} 6 | -------------------------------------------------------------------------------- /notify-types/src/snapshots/notify_types__event__tests__modify-name-from.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: notify/src/event.rs 3 | expression: json 4 | --- 5 | {"type":"modify","kind":"rename","mode":"from","paths":[],"attrs":{}} 6 | -------------------------------------------------------------------------------- /notify-types/src/snapshots/notify_types__event__tests__modify-name-other.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: notify/src/event.rs 3 | expression: json 4 | --- 5 | {"type":"modify","kind":"rename","mode":"other","paths":[],"attrs":{}} 6 | -------------------------------------------------------------------------------- /notify-types/src/snapshots/notify_types__event__tests__modify-name-to.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: notify/src/event.rs 3 | expression: json 4 | --- 5 | {"type":"modify","kind":"rename","mode":"to","paths":[],"attrs":{}} 6 | -------------------------------------------------------------------------------- /notify-types/src/snapshots/notify_types__event__tests__modify-other.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: notify/src/event.rs 3 | expression: json 4 | --- 5 | {"type":"modify","kind":"other","paths":[],"attrs":{}} 6 | -------------------------------------------------------------------------------- /notify-types/src/snapshots/notify_types__event__tests__other.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: notify/src/event.rs 3 | expression: json 4 | --- 5 | {"type":"other","paths":[],"attrs":{}} 6 | -------------------------------------------------------------------------------- /notify-types/src/snapshots/notify_types__event__tests__remove-any.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: notify/src/event.rs 3 | expression: json 4 | --- 5 | {"type":"remove","kind":"any","paths":[],"attrs":{}} 6 | -------------------------------------------------------------------------------- /notify-types/src/snapshots/notify_types__event__tests__remove-file.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: notify/src/event.rs 3 | expression: json 4 | --- 5 | {"type":"remove","kind":"file","paths":[],"attrs":{}} 6 | -------------------------------------------------------------------------------- /notify-types/src/snapshots/notify_types__event__tests__remove-folder.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: notify/src/event.rs 3 | expression: json 4 | --- 5 | {"type":"remove","kind":"folder","paths":[],"attrs":{}} 6 | -------------------------------------------------------------------------------- /notify-types/src/snapshots/notify_types__event__tests__remove-other.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: notify/src/event.rs 3 | expression: json 4 | --- 5 | {"type":"remove","kind":"other","paths":[],"attrs":{}} 6 | -------------------------------------------------------------------------------- /notify-types/src/snapshots/notify_types__event__tests__serialize_event_with_attrs.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: notify/src/event.rs 3 | assertion_line: 694 4 | expression: json 5 | --- 6 | {"type":"any","paths":[],"attrs":{"tracker":123,"flag":"rescan","info":"test event"}} 7 | -------------------------------------------------------------------------------- /notify/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | .*.sw* 4 | tests/last-fails 5 | tests/last-run.log 6 | .cargo 7 | -------------------------------------------------------------------------------- /notify/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "notify" 3 | version = "8.0.0" 4 | description = "Cross-platform filesystem notification library" 5 | documentation = "https://docs.rs/notify" 6 | readme = "../README.md" 7 | license = "CC0-1.0" 8 | keywords = ["events", "filesystem", "notify", "watch"] 9 | categories = ["filesystem"] 10 | authors = [ 11 | "Félix Saparelli ", 12 | "Daniel Faust ", 13 | "Aron Heinecke " 14 | ] 15 | rust-version.workspace = true 16 | edition.workspace = true 17 | homepage.workspace = true 18 | repository.workspace = true 19 | 20 | [features] 21 | default = ["macos_fsevent"] 22 | serde = ["notify-types/serde"] 23 | macos_kqueue = ["kqueue", "mio"] 24 | macos_fsevent = ["fsevent-sys"] 25 | serialization-compat-6 = ["notify-types/serialization-compat-6"] 26 | 27 | [dependencies] 28 | notify-types.workspace = true 29 | crossbeam-channel = { workspace = true, optional = true } 30 | flume = { workspace = true, optional = true } 31 | filetime.workspace = true 32 | libc.workspace = true 33 | log.workspace = true 34 | walkdir.workspace = true 35 | 36 | [target.'cfg(any(target_os="linux", target_os="android"))'.dependencies] 37 | inotify = { workspace = true, default-features = false } 38 | mio.workspace = true 39 | 40 | [target.'cfg(target_os="macos")'.dependencies] 41 | bitflags.workspace = true 42 | fsevent-sys = { workspace = true, optional = true } 43 | kqueue = { workspace = true, optional = true } 44 | mio = { workspace = true, optional = true } 45 | 46 | [target.'cfg(windows)'.dependencies] 47 | windows-sys = { workspace = true, features = ["Win32_System_Threading", "Win32_Foundation", "Win32_Storage_FileSystem", "Win32_Security", "Win32_System_WindowsProgramming", "Win32_System_IO"] } 48 | 49 | [target.'cfg(any(target_os="freebsd", target_os="openbsd", target_os = "netbsd", target_os = "dragonflybsd", target_os = "ios"))'.dependencies] 50 | kqueue.workspace = true 51 | mio.workspace = true 52 | 53 | [dev-dependencies] 54 | serde_json.workspace = true 55 | tempfile.workspace = true 56 | nix.workspace = true 57 | insta.workspace = true 58 | 59 | [target.'cfg(target_os = "windows")'.dev-dependencies] 60 | trash.workspace = true 61 | -------------------------------------------------------------------------------- /notify/LICENSE-CC0: -------------------------------------------------------------------------------- 1 | Creative Commons CC0 1.0 Universal 2 | 3 | <> CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER. <> 4 | 5 | Statement of Purpose 6 | 7 | The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). 8 | 9 | Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others. 10 | 11 | For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. 12 | 13 | 1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: 14 | 15 | i. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; 16 | 17 | ii. moral rights retained by the original author(s) and/or performer(s); 18 | 19 | iii. publicity and privacy rights pertaining to a person's image or likeness depicted in a Work; 20 | 21 | iv. rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below; 22 | 23 | v. rights protecting the extraction, dissemination, use and reuse of data in a Work; 24 | 25 | vi. database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and 26 | 27 | vii. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. 28 | 29 | 2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. 30 | 31 | 3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. 32 | 33 | 4. Limitations and Disclaimers. 34 | 35 | a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. 36 | 37 | b. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. 38 | 39 | c. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. 40 | 41 | d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. -------------------------------------------------------------------------------- /notify/src/config.rs: -------------------------------------------------------------------------------- 1 | //! Configuration types 2 | 3 | use std::time::Duration; 4 | 5 | /// Indicates whether only the provided directory or its sub-directories as well should be watched 6 | #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] 7 | pub enum RecursiveMode { 8 | /// Watch all sub-directories as well, including directories created after installing the watch 9 | Recursive, 10 | 11 | /// Watch only the provided directory 12 | NonRecursive, 13 | } 14 | 15 | impl RecursiveMode { 16 | pub(crate) fn is_recursive(&self) -> bool { 17 | match *self { 18 | RecursiveMode::Recursive => true, 19 | RecursiveMode::NonRecursive => false, 20 | } 21 | } 22 | } 23 | 24 | /// Watcher Backend configuration 25 | /// 26 | /// This contains multiple settings that may relate to only one specific backend, 27 | /// such as to correctly configure each backend regardless of what is selected during runtime. 28 | /// 29 | /// ```rust 30 | /// # use std::time::Duration; 31 | /// # use notify::Config; 32 | /// let config = Config::default() 33 | /// .with_poll_interval(Duration::from_secs(2)) 34 | /// .with_compare_contents(true); 35 | /// ``` 36 | /// 37 | /// Some options can be changed during runtime, others have to be set when creating the watcher backend. 38 | #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)] 39 | pub struct Config { 40 | /// See [Config::with_poll_interval] 41 | poll_interval: Option, 42 | 43 | /// See [Config::with_compare_contents] 44 | compare_contents: bool, 45 | 46 | follow_symlinks: bool, 47 | } 48 | 49 | impl Config { 50 | /// For the [`PollWatcher`](crate::PollWatcher) backend. 51 | /// 52 | /// Interval between each re-scan attempt. This can be extremely expensive for large 53 | /// file trees so it is recommended to measure and tune accordingly. 54 | /// 55 | /// The default poll frequency is 30 seconds. 56 | /// 57 | /// This will enable automatic polling, overwriting [`with_manual_polling()`](Config::with_manual_polling). 58 | pub fn with_poll_interval(mut self, dur: Duration) -> Self { 59 | // TODO: v7.0 break signature to option 60 | self.poll_interval = Some(dur); 61 | self 62 | } 63 | 64 | /// Returns current setting 65 | pub fn poll_interval(&self) -> Option { 66 | // Changed Signature to Option 67 | self.poll_interval 68 | } 69 | 70 | /// For the [`PollWatcher`](crate::PollWatcher) backend. 71 | /// 72 | /// Disable automatic polling. Requires calling [`crate::PollWatcher::poll()`] manually. 73 | /// 74 | /// This will disable automatic polling, overwriting [`with_poll_interval()`](Config::with_poll_interval). 75 | pub fn with_manual_polling(mut self) -> Self { 76 | self.poll_interval = None; 77 | self 78 | } 79 | 80 | /// For the [`PollWatcher`](crate::PollWatcher) backend. 81 | /// 82 | /// Optional feature that will evaluate the contents of changed files to determine if 83 | /// they have indeed changed using a fast hashing algorithm. This is especially important 84 | /// for pseudo filesystems like those on Linux under /sys and /proc which are not obligated 85 | /// to respect any other filesystem norms such as modification timestamps, file sizes, etc. 86 | /// By enabling this feature, performance will be significantly impacted as all files will 87 | /// need to be read and hashed at each `poll_interval`. 88 | /// 89 | /// This can't be changed during runtime. Off by default. 90 | pub fn with_compare_contents(mut self, compare_contents: bool) -> Self { 91 | self.compare_contents = compare_contents; 92 | self 93 | } 94 | 95 | /// Returns current setting 96 | pub fn compare_contents(&self) -> bool { 97 | self.compare_contents 98 | } 99 | 100 | /// For the [INotifyWatcher](crate::INotifyWatcher), [KqueueWatcher](crate::KqueueWatcher), 101 | /// and [PollWatcher](crate::PollWatcher). 102 | /// 103 | /// Determine if symbolic links should be followed when recursively watching a directory. 104 | /// 105 | /// This can't be changed during runtime. On by default. 106 | pub fn with_follow_symlinks(mut self, follow_symlinks: bool) -> Self { 107 | self.follow_symlinks = follow_symlinks; 108 | self 109 | } 110 | 111 | /// Returns current setting 112 | pub fn follow_symlinks(&self) -> bool { 113 | self.follow_symlinks 114 | } 115 | } 116 | 117 | impl Default for Config { 118 | fn default() -> Self { 119 | Self { 120 | poll_interval: Some(Duration::from_secs(30)), 121 | compare_contents: false, 122 | follow_symlinks: true, 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /notify/src/error.rs: -------------------------------------------------------------------------------- 1 | //! Error types 2 | 3 | use crate::Config; 4 | use std::error::Error as StdError; 5 | use std::path::PathBuf; 6 | use std::result::Result as StdResult; 7 | use std::{self, fmt, io}; 8 | 9 | /// Type alias to use this library's `Error` type in a Result 10 | pub type Result = StdResult; 11 | 12 | /// Error kinds 13 | #[derive(Debug)] 14 | pub enum ErrorKind { 15 | /// Generic error 16 | /// 17 | /// May be used in cases where a platform specific error is mapped to this type, or for opaque 18 | /// internal errors. 19 | Generic(String), 20 | 21 | /// I/O errors. 22 | Io(io::Error), 23 | 24 | /// A path does not exist. 25 | PathNotFound, 26 | 27 | /// Attempted to remove a watch that does not exist. 28 | WatchNotFound, 29 | 30 | /// An invalid value was passed as runtime configuration. 31 | InvalidConfig(Config), 32 | 33 | /// Can't watch (more) files, limit on the total number of inotify watches reached 34 | MaxFilesWatch, 35 | } 36 | 37 | /// Notify error type. 38 | /// 39 | /// Errors are emitted either at creation time of a `Watcher`, or during the event stream. They 40 | /// range from kernel errors to filesystem errors to argument errors. 41 | /// 42 | /// Errors can be general, or they can be about specific paths or subtrees. In that later case, the 43 | /// error's `paths` field will be populated. 44 | #[derive(Debug)] 45 | pub struct Error { 46 | /// Kind of the error. 47 | pub kind: ErrorKind, 48 | 49 | /// Relevant paths to the error, if any. 50 | pub paths: Vec, 51 | } 52 | 53 | impl Error { 54 | /// Adds a path to the error. 55 | pub fn add_path(mut self, path: PathBuf) -> Self { 56 | self.paths.push(path); 57 | self 58 | } 59 | 60 | /// Replaces the paths for the error. 61 | pub fn set_paths(mut self, paths: Vec) -> Self { 62 | self.paths = paths; 63 | self 64 | } 65 | 66 | /// Creates a new Error with empty paths given its kind. 67 | pub fn new(kind: ErrorKind) -> Self { 68 | Self { 69 | kind, 70 | paths: Vec::new(), 71 | } 72 | } 73 | 74 | /// Creates a new generic Error from a message. 75 | pub fn generic(msg: &str) -> Self { 76 | Self::new(ErrorKind::Generic(msg.into())) 77 | } 78 | 79 | /// Creates a new i/o Error from a stdlib `io::Error`. 80 | pub fn io(err: io::Error) -> Self { 81 | Self::new(ErrorKind::Io(err)) 82 | } 83 | 84 | /// Similar to [`Error::io`], but specifically handles [`io::ErrorKind::NotFound`]. 85 | pub fn io_watch(err: io::Error) -> Self { 86 | if err.kind() == io::ErrorKind::NotFound { 87 | Self::path_not_found() 88 | } else { 89 | Self::io(err) 90 | } 91 | } 92 | 93 | /// Creates a new "path not found" error. 94 | pub fn path_not_found() -> Self { 95 | Self::new(ErrorKind::PathNotFound) 96 | } 97 | 98 | /// Creates a new "watch not found" error. 99 | pub fn watch_not_found() -> Self { 100 | Self::new(ErrorKind::WatchNotFound) 101 | } 102 | 103 | /// Creates a new "invalid config" error from the given `Config`. 104 | pub fn invalid_config(config: &Config) -> Self { 105 | Self::new(ErrorKind::InvalidConfig(*config)) 106 | } 107 | } 108 | 109 | impl fmt::Display for Error { 110 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 111 | let error = match self.kind { 112 | ErrorKind::PathNotFound => "No path was found.".into(), 113 | ErrorKind::WatchNotFound => "No watch was found.".into(), 114 | ErrorKind::InvalidConfig(ref config) => format!("Invalid configuration: {:?}", config), 115 | ErrorKind::Generic(ref err) => err.clone(), 116 | ErrorKind::Io(ref err) => err.to_string(), 117 | ErrorKind::MaxFilesWatch => "OS file watch limit reached.".into(), 118 | }; 119 | 120 | if self.paths.is_empty() { 121 | write!(f, "{}", error) 122 | } else { 123 | write!(f, "{} about {:?}", error, self.paths) 124 | } 125 | } 126 | } 127 | 128 | impl StdError for Error { 129 | fn cause(&self) -> Option<&dyn StdError> { 130 | match self.kind { 131 | ErrorKind::Io(ref cause) => Some(cause), 132 | _ => None, 133 | } 134 | } 135 | } 136 | 137 | impl From for Error { 138 | fn from(err: io::Error) -> Self { 139 | Error::io(err) 140 | } 141 | } 142 | 143 | impl From> for Error { 144 | fn from(err: std::sync::mpsc::SendError) -> Self { 145 | Error::generic(&format!("internal channel disconnect: {:?}", err)) 146 | } 147 | } 148 | 149 | impl From for Error { 150 | fn from(err: std::sync::mpsc::RecvError) -> Self { 151 | Error::generic(&format!("internal channel disconnect: {:?}", err)) 152 | } 153 | } 154 | 155 | impl From> for Error { 156 | fn from(err: std::sync::PoisonError) -> Self { 157 | Error::generic(&format!("internal mutex poisoned: {:?}", err)) 158 | } 159 | } 160 | 161 | #[test] 162 | fn display_formatted_errors() { 163 | let expected = "Some error"; 164 | 165 | assert_eq!(expected, format!("{}", Error::generic(expected))); 166 | 167 | assert_eq!( 168 | expected, 169 | format!("{}", Error::io(io::Error::other(expected))) 170 | ); 171 | } 172 | -------------------------------------------------------------------------------- /notify/src/null.rs: -------------------------------------------------------------------------------- 1 | //! Stub Watcher implementation 2 | 3 | #![allow(unused_variables)] 4 | 5 | use crate::Config; 6 | 7 | use super::{RecursiveMode, Result, Watcher}; 8 | use std::path::Path; 9 | 10 | /// Stub `Watcher` implementation 11 | /// 12 | /// Events are never delivered from this watcher. 13 | #[derive(Debug)] 14 | pub struct NullWatcher; 15 | 16 | impl Watcher for NullWatcher { 17 | fn watch(&mut self, path: &Path, recursive_mode: RecursiveMode) -> Result<()> { 18 | Ok(()) 19 | } 20 | 21 | fn unwatch(&mut self, path: &Path) -> Result<()> { 22 | Ok(()) 23 | } 24 | 25 | fn new(event_handler: F, config: Config) -> Result 26 | where 27 | Self: Sized, 28 | { 29 | Ok(NullWatcher) 30 | } 31 | 32 | fn configure(&mut self, config: Config) -> Result { 33 | Ok(false) 34 | } 35 | 36 | fn kind() -> crate::WatcherKind { 37 | crate::WatcherKind::NullWatcher 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /release_checklist.md: -------------------------------------------------------------------------------- 1 | # Release checklist of files to update 2 | 3 | Specifically the notify version. 4 | 5 | - update CHANGELOG.md 6 | - update README.md 7 | - update Cargo.toml and src/lib.rs for: 8 | - file-id 9 | - notify 10 | - notify-debouncer-full 11 | - notify-debouncer-mini 12 | - notify-types 13 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.77.2" 3 | -------------------------------------------------------------------------------- /tests/and-retry: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | options="$*" 4 | attempt=1 5 | lastexit=0 6 | 7 | rm tests/last-fails || true 8 | 9 | while [[ $attempt < 5 ]]; do 10 | echo 11 | echo "~~> Test attempt $attempt" 12 | 13 | if [[ -f tests/last-fails ]]; then 14 | echo "~~? Testing only: $(cat tests/last-fails | xargs)" 15 | echo 16 | 17 | rm tests/last-run.log 18 | lastexit=0 19 | 20 | while read t; do 21 | cargo test --color=always $options $t -- --exact | tee -a tests/last-run.log 22 | status=${PIPESTATUS[0]} 23 | if ! [[ $status = 0 ]]; then 24 | lastexit=$status 25 | fi 26 | done < tests/last-fails 27 | else 28 | echo 29 | cargo test --color=always $options | tee tests/last-run.log 30 | lastexit=${PIPESTATUS[0]} 31 | fi 32 | 33 | if [[ $lastexit = 0 ]]; then 34 | break 35 | fi 36 | 37 | grep -E "^test \w+ \.\.\. FAILED$" tests/last-run.log \ 38 | | cut -d\ -f2 > tests/last-fails 39 | 40 | n=$(cat tests/last-fails | wc -l) 41 | 42 | if [[ $n = 0 ]]; then 43 | n=compile 44 | fi 45 | 46 | echo 47 | echo "~~! $n failures this attempt" 48 | echo 49 | 50 | if [[ $n = "compile" ]]; then 51 | break 52 | fi 53 | 54 | ((attempt+=1)) 55 | done 56 | 57 | exit $lastexit 58 | -------------------------------------------------------------------------------- /tests/poll-watcher-hashing.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(target_os = "windows"))] 2 | use nix::sys::stat::futimens; 3 | use std::fs::{File, OpenOptions}; 4 | use std::io::Write; 5 | use std::os::unix::io::AsRawFd; 6 | use std::path::{Path, PathBuf}; 7 | use std::sync::mpsc::Receiver; 8 | use std::time::{Duration, SystemTime}; 9 | use std::{fs, sync, thread}; 10 | 11 | use nix::sys::time::TimeSpec; 12 | use tempfile::TempDir; 13 | 14 | use notify::event::{CreateKind, DataChange, MetadataKind, ModifyKind}; 15 | use notify::poll::PollWatcherConfig; 16 | use notify::{Event, EventKind, PollWatcher, RecursiveMode, Watcher}; 17 | 18 | #[test] 19 | fn test_poll_watcher_distinguish_modify_kind() { 20 | let mut harness = TestHarness::setup(); 21 | harness.watch_tempdir(); 22 | 23 | let testfile = harness.create_file("testfile"); 24 | harness.expect_recv(&testfile, EventKind::Create(CreateKind::Any)); 25 | harness.advance_clock(); 26 | 27 | harness.write_file(&testfile, "data1"); 28 | harness.expect_recv( 29 | &testfile, 30 | EventKind::Modify(ModifyKind::Metadata(MetadataKind::WriteTime)), 31 | ); 32 | harness.advance_clock(); 33 | 34 | harness.write_file_keep_time(&testfile, "data2"); 35 | harness.expect_recv( 36 | &testfile, 37 | EventKind::Modify(ModifyKind::Data(DataChange::Any)), 38 | ); 39 | harness.advance_clock(); 40 | 41 | harness.write_file(&testfile, "data2"); 42 | harness.expect_recv( 43 | &testfile, 44 | EventKind::Modify(ModifyKind::Metadata(MetadataKind::WriteTime)), 45 | ); 46 | } 47 | 48 | struct TestHarness { 49 | testdir: TempDir, 50 | watcher: PollWatcher, 51 | rx: Receiver>, 52 | } 53 | 54 | impl TestHarness { 55 | pub fn setup() -> Self { 56 | let tempdir = tempfile::tempdir().unwrap(); 57 | 58 | let config = PollWatcherConfig { 59 | compare_contents: true, 60 | poll_interval: Duration::from_millis(10), 61 | }; 62 | let (tx, rx) = sync::mpsc::channel(); 63 | let watcher = PollWatcher::with_config( 64 | move |event: notify::Result| { 65 | tx.send(event).unwrap(); 66 | }, 67 | config, 68 | ) 69 | .unwrap(); 70 | 71 | Self { 72 | testdir: tempdir, 73 | watcher, 74 | rx, 75 | } 76 | } 77 | 78 | pub fn watch_tempdir(&mut self) { 79 | self.watcher 80 | .watch(self.testdir.path(), RecursiveMode::Recursive) 81 | .unwrap(); 82 | } 83 | 84 | pub fn create_file(&self, name: &str) -> PathBuf { 85 | let path = self.testdir.path().join(name); 86 | fs::File::create(&path).unwrap(); 87 | path 88 | } 89 | 90 | pub fn write_file>(&self, path: P, contents: &str) { 91 | self.write_file_common(path.as_ref(), contents); 92 | } 93 | 94 | pub fn write_file_keep_time>(&self, path: P, contents: &str) { 95 | let metadata = fs::metadata(path.as_ref()).unwrap(); 96 | let file = self.write_file_common(path.as_ref(), contents); 97 | let atime = Self::to_timespec(metadata.accessed().unwrap()); 98 | let mtime = Self::to_timespec(metadata.modified().unwrap()); 99 | futimens(file.as_raw_fd(), &atime, &mtime).unwrap(); 100 | } 101 | 102 | fn write_file_common(&self, path: &Path, contents: &str) -> File { 103 | let mut file = OpenOptions::new().write(true).open(path).unwrap(); 104 | file.write_all(contents.as_bytes()).unwrap(); 105 | file 106 | } 107 | 108 | fn to_timespec(t: SystemTime) -> TimeSpec { 109 | TimeSpec::from_duration(t.duration_since(SystemTime::UNIX_EPOCH).unwrap()) 110 | } 111 | 112 | pub fn advance_clock(&self) { 113 | // Unfortunately this entire crate is pretty dependent on real syscall behaviour so let's 114 | // test "for real" and require a sleep long enough to trigger mtime actually increasing. 115 | thread::sleep(Duration::from_secs(1)); 116 | } 117 | 118 | fn expect_recv>(&self, expected_path: P, expected_kind: EventKind) { 119 | let actual = self 120 | .rx 121 | .recv_timeout(Duration::from_secs(15)) 122 | .unwrap() 123 | .expect("Watch I/O error not expected under test"); 124 | assert_eq!(actual.paths, vec![expected_path.as_ref().to_path_buf()]); 125 | assert_eq!(expected_kind, actual.kind); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /tests/race-with-remove-dir.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, thread, time::Duration}; 2 | 3 | use notify::{RecursiveMode, Watcher}; 4 | 5 | /// Test for . 6 | /// Note: This test will fail if your temp directory is not writable. 7 | #[test] 8 | fn test_race_with_remove_dir() { 9 | let tmpdir = tempfile::tempdir().unwrap(); 10 | 11 | { 12 | let tmpdir = tmpdir.path().to_path_buf(); 13 | let _ = thread::Builder::new() 14 | .name("notify-rs test-race-with-remove-dir".to_string()) 15 | .spawn(move || { 16 | let mut watcher = notify::recommended_watcher(move |result| { 17 | eprintln!("received event: {:?}", result); 18 | }) 19 | .unwrap(); 20 | 21 | watcher.watch(&tmpdir, RecursiveMode::NonRecursive).unwrap(); 22 | }); 23 | } 24 | 25 | let subdir = tmpdir.path().join("146d921d.tmp"); 26 | fs::create_dir_all(&subdir).unwrap(); 27 | fs::remove_dir_all(&tmpdir).unwrap(); 28 | thread::sleep(Duration::from_secs(1)); 29 | } 30 | -------------------------------------------------------------------------------- /tests/serialise-events.rs: -------------------------------------------------------------------------------- 1 | // This file is dual-licensed under the Artistic License 2.0 as per the 2 | // LICENSE.ARTISTIC file, and the Creative Commons Zero 1.0 license. 3 | 4 | use notify::event::*; 5 | #[cfg(feature = "serde")] 6 | use serde_json::json; 7 | 8 | #[test] 9 | fn events_are_debuggable() { 10 | assert_eq!(format!("{:?}", EventKind::Any), String::from("Any")); 11 | 12 | assert_eq!( 13 | format!( 14 | "{:?}", 15 | EventKind::Access(AccessKind::Open(AccessMode::Execute)) 16 | ), 17 | String::from("Access(Open(Execute))") 18 | ); 19 | 20 | let mut attrs = EventAttributes::new(); 21 | attrs.set_info("unmount"); 22 | attrs.set_flag(Flag::Rescan); 23 | 24 | assert_eq!( 25 | format!( 26 | "{:?}", 27 | Event { 28 | kind: EventKind::Remove(RemoveKind::Other), 29 | paths: vec!["/example".into()], 30 | attrs 31 | } 32 | ), 33 | String::from( 34 | "Event { kind: Remove(Other), paths: [\"/example\"], attr:tracker: None, attr:flag: Some(Rescan), attr:info: Some(\"unmount\"), attr:source: None }" 35 | ) 36 | ); 37 | } 38 | 39 | #[cfg(feature = "serde")] 40 | #[test] 41 | fn events_are_serializable() { 42 | assert_eq!(json!(EventKind::Any), json!("any")); 43 | 44 | assert_eq!(json!(EventKind::Other), json!("other")); 45 | 46 | assert_eq!( 47 | json!(Event { 48 | kind: EventKind::Access(AccessKind::Open(AccessMode::Execute)), 49 | paths: Vec::new(), 50 | attrs: EventAttributes::new(), 51 | }), 52 | json!({ 53 | "type": { "access": { "kind": "open", "mode": "execute" } }, 54 | "paths": [], 55 | "attrs": {}, 56 | }) 57 | ); 58 | 59 | let mut attrs = EventAttributes::new(); 60 | attrs.set_info("unmount".into()); 61 | 62 | assert_eq!( 63 | json!(Event { 64 | kind: EventKind::Remove(RemoveKind::Other), 65 | paths: vec!["/example".into()], 66 | attrs: attrs.clone(), 67 | }), 68 | json!({ 69 | "type": { "remove": { "kind": "other" } }, 70 | "paths": ["/example"], 71 | "attrs": { "info": "unmount" } 72 | }), 73 | "{:#?} != {:#?}", 74 | json!(Event { 75 | kind: EventKind::Remove(RemoveKind::Other), 76 | paths: vec!["/example".into()], 77 | attrs: attrs.clone(), 78 | }), 79 | json!({ 80 | "type": { "remove": { "kind": "other" } }, 81 | "paths": ["/example"], 82 | "attrs": { "info": "unmount" } 83 | }), 84 | ); 85 | } 86 | 87 | #[cfg(feature = "serde")] 88 | #[test] 89 | fn events_are_deserializable() { 90 | assert_eq!( 91 | serde_json::from_str::(r#""any""#).unwrap(), 92 | EventKind::Any 93 | ); 94 | 95 | assert_eq!( 96 | serde_json::from_str::(r#""other""#).unwrap(), 97 | EventKind::Other 98 | ); 99 | 100 | assert_eq!( 101 | serde_json::from_str::( 102 | r#"{ 103 | "type": { "access": { "kind": "open", "mode": "execute" } }, 104 | "paths": [], 105 | "attrs": {} 106 | }"# 107 | ) 108 | .unwrap(), 109 | Event { 110 | kind: EventKind::Access(AccessKind::Open(AccessMode::Execute)), 111 | paths: Vec::new(), 112 | attrs: EventAttributes::new(), 113 | } 114 | ); 115 | 116 | let mut attrs = EventAttributes::new(); 117 | attrs.set_info("unmount".into()); 118 | 119 | assert_eq!( 120 | serde_json::from_str::( 121 | r#"{ 122 | "type": { "remove": { "kind": "other" } }, 123 | "paths": ["/example"], 124 | "attrs": { "info": "unmount" } 125 | }"# 126 | ) 127 | .unwrap(), 128 | Event { 129 | kind: EventKind::Remove(RemoveKind::Other), 130 | paths: vec!["/example".into()], 131 | attrs 132 | } 133 | ); 134 | } 135 | 136 | #[cfg(feature = "serde")] 137 | #[test] 138 | fn access_events_are_serializable() { 139 | assert_eq!( 140 | json!(EventKind::Access(AccessKind::Any)), 141 | json!({ 142 | "access": { "kind": "any" } 143 | }) 144 | ); 145 | 146 | assert_eq!( 147 | json!(EventKind::Access(AccessKind::Read)), 148 | json!({ 149 | "access": { "kind": "read" } 150 | }) 151 | ); 152 | 153 | assert_eq!( 154 | json!(EventKind::Access(AccessKind::Open(AccessMode::Any))), 155 | json!({ 156 | "access": { "kind": "open", "mode": "any" } 157 | }) 158 | ); 159 | 160 | assert_eq!( 161 | json!(EventKind::Access(AccessKind::Open(AccessMode::Execute))), 162 | json!({ 163 | "access": { "kind": "open", "mode": "execute" } 164 | }) 165 | ); 166 | 167 | assert_eq!( 168 | json!(EventKind::Access(AccessKind::Open(AccessMode::Read))), 169 | json!({ 170 | "access": { "kind": "open", "mode": "read" } 171 | }) 172 | ); 173 | 174 | assert_eq!( 175 | json!(EventKind::Access(AccessKind::Close(AccessMode::Write))), 176 | json!({ 177 | "access": { "kind": "close", "mode": "write" } 178 | }) 179 | ); 180 | 181 | assert_eq!( 182 | json!(EventKind::Access(AccessKind::Close(AccessMode::Other))), 183 | json!({ 184 | "access": { "kind": "close", "mode": "other" } 185 | }) 186 | ); 187 | 188 | assert_eq!( 189 | json!(EventKind::Access(AccessKind::Other)), 190 | json!({ 191 | "access": { "kind": "other" } 192 | }) 193 | ); 194 | } 195 | 196 | #[cfg(feature = "serde")] 197 | #[test] 198 | fn create_events_are_serializable() { 199 | assert_eq!( 200 | json!(EventKind::Create(CreateKind::Any)), 201 | json!({ 202 | "create": { "kind": "any" } 203 | }) 204 | ); 205 | 206 | assert_eq!( 207 | json!(EventKind::Create(CreateKind::File)), 208 | json!({ 209 | "create": { "kind": "file" } 210 | }) 211 | ); 212 | 213 | assert_eq!( 214 | json!(EventKind::Create(CreateKind::Folder)), 215 | json!({ 216 | "create": { "kind": "folder" } 217 | }) 218 | ); 219 | 220 | assert_eq!( 221 | json!(EventKind::Create(CreateKind::Other)), 222 | json!({ 223 | "create": { "kind": "other" } 224 | }) 225 | ); 226 | } 227 | 228 | #[cfg(feature = "serde")] 229 | #[test] 230 | fn modify_events_are_serializable() { 231 | assert_eq!( 232 | json!(EventKind::Modify(ModifyKind::Any)), 233 | json!({ 234 | "modify": { "kind": "any" } 235 | }) 236 | ); 237 | 238 | assert_eq!( 239 | json!(EventKind::Modify(ModifyKind::Data(DataChange::Any))), 240 | json!({ 241 | "modify": { "kind": "data", "mode": "any" } 242 | }) 243 | ); 244 | 245 | assert_eq!( 246 | json!(EventKind::Modify(ModifyKind::Data(DataChange::Size))), 247 | json!({ 248 | "modify": { "kind": "data", "mode": "size" } 249 | }) 250 | ); 251 | 252 | assert_eq!( 253 | json!(EventKind::Modify(ModifyKind::Data(DataChange::Content))), 254 | json!({ 255 | "modify": { "kind": "data", "mode": "content" } 256 | }) 257 | ); 258 | 259 | assert_eq!( 260 | json!(EventKind::Modify(ModifyKind::Data(DataChange::Other))), 261 | json!({ 262 | "modify": { "kind": "data", "mode": "other" } 263 | }) 264 | ); 265 | 266 | assert_eq!( 267 | json!(EventKind::Modify(ModifyKind::Metadata(MetadataKind::Any))), 268 | json!({ 269 | "modify": { "kind": "metadata", "mode": "any" } 270 | }) 271 | ); 272 | 273 | assert_eq!( 274 | json!(EventKind::Modify(ModifyKind::Metadata( 275 | MetadataKind::AccessTime 276 | ))), 277 | json!({ 278 | "modify": { "kind": "metadata", "mode": "access-time" } 279 | }) 280 | ); 281 | 282 | assert_eq!( 283 | json!(EventKind::Modify(ModifyKind::Metadata( 284 | MetadataKind::WriteTime 285 | ))), 286 | json!({ 287 | "modify": { "kind": "metadata", "mode": "write-time" } 288 | }) 289 | ); 290 | 291 | assert_eq!( 292 | json!(EventKind::Modify(ModifyKind::Metadata( 293 | MetadataKind::Permissions 294 | ))), 295 | json!({ 296 | "modify": { "kind": "metadata", "mode": "permissions" } 297 | }) 298 | ); 299 | 300 | assert_eq!( 301 | json!(EventKind::Modify(ModifyKind::Metadata( 302 | MetadataKind::Ownership 303 | ))), 304 | json!({ 305 | "modify": { "kind": "metadata", "mode": "ownership" } 306 | }) 307 | ); 308 | 309 | assert_eq!( 310 | json!(EventKind::Modify(ModifyKind::Metadata( 311 | MetadataKind::Extended 312 | ))), 313 | json!({ 314 | "modify": { "kind": "metadata", "mode": "extended" } 315 | }) 316 | ); 317 | 318 | assert_eq!( 319 | json!(EventKind::Modify(ModifyKind::Metadata(MetadataKind::Other))), 320 | json!({ 321 | "modify": { "kind": "metadata", "mode": "other" } 322 | }) 323 | ); 324 | 325 | assert_eq!( 326 | json!(EventKind::Modify(ModifyKind::Name(RenameMode::Any))), 327 | json!({ 328 | "modify": { "kind": "rename", "mode": "any" } 329 | }) 330 | ); 331 | 332 | assert_eq!( 333 | json!(EventKind::Modify(ModifyKind::Name(RenameMode::To))), 334 | json!({ 335 | "modify": { "kind": "rename", "mode": "to" } 336 | }) 337 | ); 338 | 339 | assert_eq!( 340 | json!(EventKind::Modify(ModifyKind::Name(RenameMode::From))), 341 | json!({ 342 | "modify": { "kind": "rename", "mode": "from" } 343 | }) 344 | ); 345 | 346 | assert_eq!( 347 | json!(EventKind::Modify(ModifyKind::Name(RenameMode::Both))), 348 | json!({ 349 | "modify": { "kind": "rename", "mode": "both" } 350 | }) 351 | ); 352 | 353 | assert_eq!( 354 | json!(EventKind::Modify(ModifyKind::Name(RenameMode::Other))), 355 | json!({ 356 | "modify": { "kind": "rename", "mode": "other" } 357 | }) 358 | ); 359 | 360 | assert_eq!( 361 | json!(EventKind::Modify(ModifyKind::Other)), 362 | json!({ 363 | "modify": { "kind": "other" } 364 | }) 365 | ); 366 | } 367 | 368 | #[cfg(feature = "serde")] 369 | #[test] 370 | fn remove_events_are_serializable() { 371 | assert_eq!( 372 | json!(EventKind::Remove(RemoveKind::Any)), 373 | json!({ 374 | "remove": { "kind": "any" } 375 | }) 376 | ); 377 | 378 | assert_eq!( 379 | json!(EventKind::Remove(RemoveKind::File)), 380 | json!({ 381 | "remove": { "kind": "file" } 382 | }) 383 | ); 384 | 385 | assert_eq!( 386 | json!(EventKind::Remove(RemoveKind::Folder)), 387 | json!({ 388 | "remove": { "kind": "folder" } 389 | }) 390 | ); 391 | 392 | assert_eq!( 393 | json!(EventKind::Remove(RemoveKind::Other)), 394 | json!({ 395 | "remove": { "kind": "other" } 396 | }) 397 | ); 398 | } 399 | --------------------------------------------------------------------------------