mirror of
https://github.com/icewind1991/notify-redis.git
synced 2026-06-03 18:24:12 +02:00
Compare commits
9 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 672094558d | |||
| 7430db6c5b | |||
| f6e341a637 | |||
| e5a506f3fe | |||
| 0e4a7eb35d | |||
| d723a22d9b | |||
| 347f2db450 | |||
| 90af4a497c | |||
| 49652b7288 |
17 changed files with 1376 additions and 694 deletions
1
.envrc
Normal file
1
.envrc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
use flake
|
||||||
90
.github/workflows/nix.yaml
vendored
Normal file
90
.github/workflows/nix.yaml
vendored
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
name: "CI"
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: cachix/install-nix-action@v20
|
||||||
|
- uses: icewind1991/attic-action@v1
|
||||||
|
with:
|
||||||
|
name: ci
|
||||||
|
instance: https://cache.icewind.me
|
||||||
|
authToken: '${{ secrets.ATTIC_TOKEN }}'
|
||||||
|
- run: nix build .#check
|
||||||
|
|
||||||
|
clippy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: check
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: cachix/install-nix-action@v20
|
||||||
|
- uses: icewind1991/attic-action@v1
|
||||||
|
with:
|
||||||
|
name: ci
|
||||||
|
instance: https://cache.icewind.me
|
||||||
|
authToken: '${{ secrets.ATTIC_TOKEN }}'
|
||||||
|
- run: nix build .#clippy
|
||||||
|
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: check
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: cachix/install-nix-action@v20
|
||||||
|
- uses: icewind1991/attic-action@v1
|
||||||
|
with:
|
||||||
|
name: ci
|
||||||
|
instance: https://cache.icewind.me
|
||||||
|
authToken: '${{ secrets.ATTIC_TOKEN }}'
|
||||||
|
- run: nix build .#test
|
||||||
|
|
||||||
|
matrix:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: cachix/install-nix-action@v20
|
||||||
|
- id: set-matrix
|
||||||
|
run: echo "matrix={\"target\":$(nix eval --json ".#targets.x86_64-linux")}" | tee $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [check, matrix]
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix: ${{fromJson(needs.matrix.outputs.matrix)}}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: cachix/install-nix-action@v20
|
||||||
|
- uses: icewind1991/attic-action@v1
|
||||||
|
with:
|
||||||
|
name: ci
|
||||||
|
instance: https://cache.icewind.me
|
||||||
|
authToken: '${{ secrets.ATTIC_TOKEN }}'
|
||||||
|
- run: nix build .#${{ matrix.target }}
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: notify-redis-${{ matrix.target }}
|
||||||
|
path: result/bin/*
|
||||||
|
|
||||||
|
docker:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [build, clippy, test]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: cachix/install-nix-action@v20
|
||||||
|
- uses: icewind1991/attic-action@v1
|
||||||
|
with:
|
||||||
|
name: ci
|
||||||
|
instance: https://cache.icewind.me
|
||||||
|
authToken: '${{ secrets.ATTIC_TOKEN }}'
|
||||||
|
- run: nix build .#dockerImage
|
||||||
|
- name: Push image
|
||||||
|
if: github.ref == 'refs/heads/master'
|
||||||
|
run: |
|
||||||
|
skopeo copy --dest-creds="${{ secrets.DOCKERHUB_USERNAME }}:${{ secrets.DOCKERHUB_TOKEN }}" "docker-archive:$(nix build .#dockerImage --print-out-paths)" "docker://icewind1991/notify-redis"
|
||||||
40
.github/workflows/release.yml
vendored
Normal file
40
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
matrix:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: cachix/install-nix-action@v20
|
||||||
|
- id: set-matrix
|
||||||
|
run: echo "matrix={\"target\":$(nix eval --json ".#targets.x86_64-linux")}" | tee $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: matrix
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix: ${{fromJson(needs.matrix.outputs.matrix)}}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: cachix/install-nix-action@v20
|
||||||
|
- uses: icewind1991/attic-action@v1
|
||||||
|
with:
|
||||||
|
name: ci
|
||||||
|
instance: https://cache.icewind.me
|
||||||
|
authToken: '${{ secrets.ATTIC_TOKEN }}'
|
||||||
|
- run: nix build .#${{ matrix.target }}
|
||||||
|
- name: Upload binary to release
|
||||||
|
uses: svenstaro/upload-release-action@v2
|
||||||
|
with:
|
||||||
|
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
file: result/bin/cube
|
||||||
|
asset_name: notify-redis-${{ matrix.target }}
|
||||||
|
tag: ${{ github.ref }}
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,3 +1,5 @@
|
||||||
/test
|
/test
|
||||||
/target
|
/target
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
|
.direnv
|
||||||
|
result
|
||||||
18
.travis.yml
18
.travis.yml
|
|
@ -1,18 +0,0 @@
|
||||||
sudo: required
|
|
||||||
services:
|
|
||||||
- docker
|
|
||||||
- redis-server
|
|
||||||
install:
|
|
||||||
- docker pull ekidd/rust-musl-builder:latest
|
|
||||||
script:
|
|
||||||
- "./travis-build.sh notify-redis ${TRAVIS_OS_NAME}"
|
|
||||||
- "./test.sh ./notify-redis-${TRAVIS_OS_NAME}"
|
|
||||||
deploy:
|
|
||||||
provider: releases
|
|
||||||
api_key:
|
|
||||||
secure: GBWfTW08qT3K9W9zeVteUJ9RTDnaIguH5Zoe7N+GOTPBGUDscL/3vfCOj5PyfhLgHuPXzlMAGCM42MZFknL5PXFO4ZMFuiTtcSbiSEQNPArQjDrWbCPxU9IrXN7W/94EEcdWmSSpkT/Fc3XRgx9z2BjUSfXCv4R4owwtRWhWlNNnmqVIe7OEfCQEKBCsuGB9PmLTShUd33ztVqVt2+WMC8kT9Wrk9KmeeEANx/K1rz18sMOphd7Ruqx9z1tLF0nFaRGBSRPV4EhpBGLahdglRtq9uvHFA4iMmpf1HWWE/TDVLI/tykE1B39gYfzEircLAJw+D6orO3kPsjqH8gzwwUyZKQr5OmEZUExtdHp/K606Doo3fwdWty3CqZYzDI+p4YwXDL8cI2TShGaQpXVwKXpJK+5RXrsYNY+tBvsQ2BpW4Z5US+uqESUBX4dbdrjZyXKfIJFHRpzn8COa02Hc0nFYf/hqakzTqTylVYTTQaZ4VdgPuU7zUFPHg7rBEPbuv0ncxDOgLjjYLHl00CSSsqiRezJzW2TUEFW6xXI8qz9QTnOAyVM4aBStxF1oAtOia4jcMu7gjIJeeAOZnYSqryW/Xy8YuzRE07axXNk90Uma+Gxn3zcgvGAdwct2jq+iPQ8yvqirCJFNMxfu8pDTYbLZcFNZEvOfLVQv9OmPNqE=
|
|
||||||
file: notify-redis-linux
|
|
||||||
skip_cleanup: true
|
|
||||||
on:
|
|
||||||
repo: icewind1991/notify-redis
|
|
||||||
tags: true
|
|
||||||
1269
Cargo.lock
generated
1269
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
16
Cargo.toml
16
Cargo.toml
|
|
@ -5,8 +5,18 @@ authors = ["Robin Appelman <robin@icewind.nl>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
notify = "4.0"
|
notify = "6.0"
|
||||||
redis = "0.10"
|
notify-debouncer-full = "0.2.0"
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
redis = { version = "0.23", default-features = false }
|
||||||
|
time = { version = "0.3.22", features = ["serde", "formatting", "parsing"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
color-eyre = "0.6"
|
||||||
|
clap = { version = "4.3.9", features = ["derive"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
rand = "0.8.5"
|
||||||
|
tempfile = "3"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
lto = true
|
||||||
|
|
|
||||||
9
Makefile
9
Makefile
|
|
@ -1,9 +0,0 @@
|
||||||
all: target/x86_64-unknown-linux-musl/release/notify-redis
|
|
||||||
|
|
||||||
target/x86_64-unknown-linux-musl/release/notify-redis: Cargo.toml src/main.rs
|
|
||||||
docker run --rm -it -v "$(CURDIR):/home/rust/src" ekidd/rust-musl-builder cargo build --release
|
|
||||||
|
|
||||||
.PHONY: test
|
|
||||||
|
|
||||||
test: target/x86_64-unknown-linux-musl/release/notify-redis
|
|
||||||
./test.sh
|
|
||||||
|
|
@ -9,7 +9,7 @@ Push filesystem notifications into a redis list
|
||||||
There are 3 ways for getting the binary to run
|
There are 3 ways for getting the binary to run
|
||||||
|
|
||||||
- Grab a pre-compiled static binary from the [releases](https://github.com/icewind1991/notify-redis/releases) page.
|
- Grab a pre-compiled static binary from the [releases](https://github.com/icewind1991/notify-redis/releases) page.
|
||||||
- By running `make` to use docker to build a static binary (requires `make` and `docker`)
|
- By running `nix build` to use docker to build a static binary (requires `nix`)
|
||||||
- By running `cargo build` (requires `rust`)
|
- By running `cargo build` (requires `rust`)
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
|
||||||
129
flake.lock
generated
Normal file
129
flake.lock
generated
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"cross-naersk": {
|
||||||
|
"inputs": {
|
||||||
|
"naersk": [
|
||||||
|
"naersk"
|
||||||
|
],
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1687811683,
|
||||||
|
"narHash": "sha256-j0+0y2CBlwrbVkVEZajjAy9gdzHRNCq8hQTRe+QXTAQ=",
|
||||||
|
"owner": "icewind1991",
|
||||||
|
"repo": "cross-naersk",
|
||||||
|
"rev": "5e987fcf0521602914773016b173403d0fa873f9",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "icewind1991",
|
||||||
|
"repo": "cross-naersk",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1687709756,
|
||||||
|
"narHash": "sha256-Y5wKlQSkgEK2weWdOu4J3riRd+kV/VCgHsqLNTTWQ/0=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "dbabf0ca0c0c4bce6ea5eaf65af5cb694d2082c7",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"naersk": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1687852486,
|
||||||
|
"narHash": "sha256-2rXkhKUVQxbVaC+TITPpILiy/dSbordOLs87eoWHYxA=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "naersk",
|
||||||
|
"rev": "df10963b956962913b693a638746a95d6c506404",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "naersk",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1687829761,
|
||||||
|
"narHash": "sha256-QRe1Y8SS3M4GeC58F/6ajz6V0ZLUVWX3ZAMgov2N3/g=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "9790f3242da2152d5aa1976e3e4b8b414f4dd206",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"id": "nixpkgs",
|
||||||
|
"ref": "nixos-23.05",
|
||||||
|
"type": "indirect"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"cross-naersk": "cross-naersk",
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"naersk": "naersk",
|
||||||
|
"nixpkgs": "nixpkgs",
|
||||||
|
"rust-overlay": "rust-overlay"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rust-overlay": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": [
|
||||||
|
"flake-utils"
|
||||||
|
],
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1688005946,
|
||||||
|
"narHash": "sha256-aEK0CNCIfE6ALQuztj86sl4PZUzMDnbp68r6I5YW+AE=",
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"rev": "2925988bbc95f94e7b2f822b914ac5612a636e93",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
90
flake.nix
Normal file
90
flake.nix
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
{
|
||||||
|
inputs = {
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
nixpkgs.url = "nixpkgs/nixos-23.05";
|
||||||
|
naersk.url = "github:nix-community/naersk";
|
||||||
|
naersk.inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
rust-overlay.url = "github:oxalica/rust-overlay";
|
||||||
|
rust-overlay.inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
rust-overlay.inputs.flake-utils.follows = "flake-utils";
|
||||||
|
cross-naersk.url = "github:icewind1991/cross-naersk";
|
||||||
|
cross-naersk.inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
cross-naersk.inputs.naersk.follows = "naersk";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = {
|
||||||
|
self,
|
||||||
|
nixpkgs,
|
||||||
|
flake-utils,
|
||||||
|
naersk,
|
||||||
|
rust-overlay,
|
||||||
|
cross-naersk,
|
||||||
|
}:
|
||||||
|
flake-utils.lib.eachDefaultSystem (
|
||||||
|
system: let
|
||||||
|
overlays = [(import rust-overlay)];
|
||||||
|
pkgs = import nixpkgs {
|
||||||
|
inherit system overlays;
|
||||||
|
};
|
||||||
|
lib = pkgs.lib;
|
||||||
|
|
||||||
|
cross-naersk' = pkgs.callPackage cross-naersk {inherit naersk;};
|
||||||
|
|
||||||
|
hostTarget = pkgs.hostPlatform.config;
|
||||||
|
targets = [
|
||||||
|
"x86_64-unknown-linux-musl"
|
||||||
|
"i686-unknown-linux-musl"
|
||||||
|
"armv7-unknown-linux-musleabihf"
|
||||||
|
"aarch64-unknown-linux-musl"
|
||||||
|
];
|
||||||
|
src = lib.sources.sourceByRegex (lib.cleanSource ./.) ["Cargo.*" "(src|tests)(/.*)?"];
|
||||||
|
|
||||||
|
nearskOpt = {
|
||||||
|
pname = "notify-redis";
|
||||||
|
root = src;
|
||||||
|
};
|
||||||
|
buildTarget = target: (cross-naersk'.buildPackage target) nearskOpt;
|
||||||
|
hostNaersk = cross-naersk'.hostNaersk;
|
||||||
|
in rec {
|
||||||
|
# `nix build`
|
||||||
|
packages = nixpkgs.lib.attrsets.genAttrs targets buildTarget // rec {
|
||||||
|
notify-redis = pkgs.callPackage (import ./package.nix) {};
|
||||||
|
default = notify-redis;
|
||||||
|
check = hostNaersk.buildPackage (nearskOpt // {
|
||||||
|
mode = "check";
|
||||||
|
});
|
||||||
|
clippy = hostNaersk.buildPackage (nearskOpt // {
|
||||||
|
mode = "clippy";
|
||||||
|
});
|
||||||
|
test = hostNaersk.buildPackage (nearskOpt // {
|
||||||
|
mode = "test";
|
||||||
|
nativeBuildInputs = [pkgs.redis];
|
||||||
|
overrideMain = x: x // {
|
||||||
|
preBuild = ''
|
||||||
|
redis-server &
|
||||||
|
export redisPID=$!
|
||||||
|
'';
|
||||||
|
postBuild = ''
|
||||||
|
kill $redisPID
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
});
|
||||||
|
dockerImage = pkgs.dockerTools.buildImage {
|
||||||
|
name = "icewind1991/notify-redis";
|
||||||
|
tag = "latest";
|
||||||
|
copyToRoot = [notify-redis];
|
||||||
|
config = {
|
||||||
|
Cmd = ["${notify-redis}/bin/notify-redis"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
inherit targets;
|
||||||
|
|
||||||
|
# `nix develop`
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
|
nativeBuildInputs = with pkgs; [rustc cargo bacon cargo-edit cargo-outdated clippy];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
25
package.nix
Normal file
25
package.nix
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
rustPlatform,
|
||||||
|
lib,
|
||||||
|
}: let
|
||||||
|
src = lib.sources.sourceByRegex (lib.cleanSource ./.) ["Cargo.*" "(src|tests)(/.*)?"];
|
||||||
|
in
|
||||||
|
rustPlatform.buildRustPackage rec {
|
||||||
|
version = "0.2.1";
|
||||||
|
pname = "notify-redis";
|
||||||
|
|
||||||
|
inherit src;
|
||||||
|
|
||||||
|
cargoLock = {
|
||||||
|
lockFile = ./Cargo.lock;
|
||||||
|
};
|
||||||
|
|
||||||
|
doCheck = false;
|
||||||
|
|
||||||
|
meta = with lib; {
|
||||||
|
description = "Push filesystem notifications into a redis list";
|
||||||
|
homepage = "https://github.com/icewind1991/notify-redis";
|
||||||
|
license = licenses.mit;
|
||||||
|
platforms = platforms.linux;
|
||||||
|
};
|
||||||
|
}
|
||||||
105
src/lib.rs
Normal file
105
src/lib.rs
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
use color_eyre::{eyre::WrapErr, Result};
|
||||||
|
use notify::event::{ModifyKind, RenameMode};
|
||||||
|
use notify::{EventKind, RecursiveMode, Watcher};
|
||||||
|
use notify_debouncer_full::{new_debouncer, DebouncedEvent};
|
||||||
|
use redis::{Client, Commands, Connection, IntoConnectionInfo};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::mpsc::channel;
|
||||||
|
use std::time::Duration;
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
#[serde(tag = "event")]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum Event {
|
||||||
|
Modify {
|
||||||
|
path: PathBuf,
|
||||||
|
#[serde(with = "time::serde::iso8601")]
|
||||||
|
time: OffsetDateTime,
|
||||||
|
},
|
||||||
|
Move {
|
||||||
|
from: PathBuf,
|
||||||
|
to: PathBuf,
|
||||||
|
#[serde(with = "time::serde::iso8601")]
|
||||||
|
time: OffsetDateTime,
|
||||||
|
},
|
||||||
|
Delete {
|
||||||
|
path: PathBuf,
|
||||||
|
#[serde(with = "time::serde::iso8601")]
|
||||||
|
time: OffsetDateTime,
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DebouncedEvent> for Event {
|
||||||
|
fn from(event: DebouncedEvent) -> Self {
|
||||||
|
let now = OffsetDateTime::now_utc();
|
||||||
|
let elapsed = event.time.elapsed();
|
||||||
|
let time = now - elapsed;
|
||||||
|
|
||||||
|
let path_count = event.paths.len();
|
||||||
|
let mut paths = event.event.paths.into_iter();
|
||||||
|
|
||||||
|
match (event.event.kind, path_count) {
|
||||||
|
(EventKind::Modify(ModifyKind::Name(RenameMode::Both)), 2..) => Event::Move {
|
||||||
|
from: paths.next().unwrap(),
|
||||||
|
to: paths.next().unwrap(),
|
||||||
|
time,
|
||||||
|
},
|
||||||
|
(EventKind::Modify(_) | EventKind::Create(_), 1..) => Event::Modify {
|
||||||
|
path: paths.next().unwrap(),
|
||||||
|
time,
|
||||||
|
},
|
||||||
|
(EventKind::Remove(_), 1..) => Event::Delete {
|
||||||
|
path: paths.next().unwrap(),
|
||||||
|
time,
|
||||||
|
},
|
||||||
|
_ => Event::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn watch(
|
||||||
|
path: impl AsRef<Path>,
|
||||||
|
redis_connect: impl IntoConnectionInfo,
|
||||||
|
redis_list: &str,
|
||||||
|
debounce: Duration,
|
||||||
|
) -> Result<()> {
|
||||||
|
let (tx, rx) = channel();
|
||||||
|
|
||||||
|
let mut watcher = new_debouncer(debounce, None, tx)?;
|
||||||
|
let client = Client::open(redis_connect).wrap_err("Invalid redis connection")?;
|
||||||
|
let mut con = client
|
||||||
|
.get_connection()
|
||||||
|
.wrap_err("Failed to open redis connection")?;
|
||||||
|
|
||||||
|
watcher
|
||||||
|
.watcher()
|
||||||
|
.watch(path.as_ref(), RecursiveMode::Recursive)?;
|
||||||
|
|
||||||
|
while let Ok(event) = rx.recv() {
|
||||||
|
for event in event.into_iter().flatten() {
|
||||||
|
push_event(event, &mut con, redis_list).wrap_err("Failed to send event to redis")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_event(event: DebouncedEvent, con: &mut Connection, list: &str) -> Result<()> {
|
||||||
|
match format_event(event) {
|
||||||
|
Some(formatted_event) => {
|
||||||
|
println!("{}", formatted_event);
|
||||||
|
Ok(con.lpush(list, formatted_event)?)
|
||||||
|
}
|
||||||
|
None => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_event(event: DebouncedEvent) -> Option<String> {
|
||||||
|
let event: Event = event.into();
|
||||||
|
match &event {
|
||||||
|
Event::None => None,
|
||||||
|
_ => serde_json::to_string(&event).ok(),
|
||||||
|
}
|
||||||
|
}
|
||||||
143
src/main.rs
143
src/main.rs
|
|
@ -1,129 +1,28 @@
|
||||||
use chrono::{DateTime, Timelike, Utc};
|
use clap::Parser;
|
||||||
use notify::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher};
|
use color_eyre::Result;
|
||||||
use redis::{Client, Commands, Connection, RedisResult};
|
use notify_redis::watch;
|
||||||
use serde::Serialize;
|
|
||||||
use std::env;
|
|
||||||
use std::error::Error;
|
|
||||||
use std::fmt;
|
|
||||||
use std::fmt::Display;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::result::Result;
|
|
||||||
use std::sync::mpsc::channel;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
#[derive(Debug)]
|
/// Push filesystem notifications into a redis list
|
||||||
enum WatchError {
|
#[derive(Parser, Debug)]
|
||||||
Notify(notify::Error),
|
#[command(author, version, about, long_about = None)]
|
||||||
Redis(redis::RedisError),
|
struct Args {
|
||||||
}
|
/// Folder to watch
|
||||||
|
|
||||||
impl From<notify::Error> for WatchError {
|
|
||||||
fn from(err: notify::Error) -> WatchError {
|
|
||||||
WatchError::Notify(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<redis::RedisError> for WatchError {
|
|
||||||
fn from(err: redis::RedisError) -> WatchError {
|
|
||||||
WatchError::Redis(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for WatchError {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
WatchError::Redis(err) => err.fmt(f),
|
|
||||||
WatchError::Notify(err) => err.fmt(f),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Error for WatchError {
|
|
||||||
fn cause(&self) -> Option<&dyn Error> {
|
|
||||||
match self {
|
|
||||||
WatchError::Redis(err) => Some(err),
|
|
||||||
WatchError::Notify(err) => Some(err),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Debug)]
|
|
||||||
#[serde(tag = "event")]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
enum Event {
|
|
||||||
Modify {
|
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
time: DateTime<Utc>,
|
/// Redis connection string
|
||||||
},
|
redis_connect: String,
|
||||||
Move {
|
/// Redis list to push changes to
|
||||||
from: PathBuf,
|
redis_list: String,
|
||||||
to: PathBuf,
|
|
||||||
time: DateTime<Utc>,
|
|
||||||
},
|
|
||||||
Delete {
|
|
||||||
path: PathBuf,
|
|
||||||
time: DateTime<Utc>,
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<DebouncedEvent> for Event {
|
fn main() -> Result<()> {
|
||||||
fn from(event: DebouncedEvent) -> Self {
|
let args: Args = Args::parse();
|
||||||
let time = Utc::now().with_nanosecond(0).unwrap();
|
watch(
|
||||||
|
args.path,
|
||||||
match event {
|
args.redis_connect,
|
||||||
DebouncedEvent::Write(path)
|
&args.redis_list,
|
||||||
| DebouncedEvent::Create(path)
|
Duration::from_secs(2),
|
||||||
| DebouncedEvent::Chmod(path) => Event::Modify { path, time },
|
)?;
|
||||||
DebouncedEvent::Rename(from, to) => Event::Move { from, to, time },
|
Ok(())
|
||||||
DebouncedEvent::Remove(path) => Event::Delete { path, time },
|
|
||||||
_ => Event::None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn watch(path: &str, redis_connect: &str, redis_list: &str) -> Result<(), WatchError> {
|
|
||||||
let (tx, rx) = channel();
|
|
||||||
|
|
||||||
let mut watcher: RecommendedWatcher = Watcher::new(tx, Duration::from_secs(2))?;
|
|
||||||
let client = Client::open(redis_connect)?;
|
|
||||||
let con = client.get_connection()?;
|
|
||||||
|
|
||||||
watcher.watch(path, RecursiveMode::Recursive)?;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
match rx.recv() {
|
|
||||||
Ok(event) => push_event(event, &con, redis_list)?,
|
|
||||||
Err(e) => println!("watch error: {}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn push_event(event: DebouncedEvent, con: &Connection, list: &str) -> RedisResult<()> {
|
|
||||||
match format_event(event) {
|
|
||||||
Some(formatted_event) => {
|
|
||||||
println!("{}", formatted_event);
|
|
||||||
con.lpush(list, formatted_event)
|
|
||||||
}
|
|
||||||
None => Ok(()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn format_event(event: DebouncedEvent) -> Option<String> {
|
|
||||||
let event: Event = event.into();
|
|
||||||
match &event {
|
|
||||||
Event::None => None,
|
|
||||||
_ => serde_json::to_string(&event).ok(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let args: Vec<_> = env::args().collect();
|
|
||||||
if let [_, path, redis, list] = args.as_slice() {
|
|
||||||
if let Err(e) = watch(path, redis, list) {
|
|
||||||
println!("error: {}", e)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
println!("usage: {} <path> <redis_connect> <redis_list>", args[0])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
56
test.sh
56
test.sh
|
|
@ -1,56 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
function assert_notify {
|
|
||||||
actual="$(echo $(redis-cli rpop notify) | sed -e 's/,\"time\":[^Z]*Z\"//g')" # strip time
|
|
||||||
if [ "$actual" != "$1" ]
|
|
||||||
then
|
|
||||||
echo "'$actual' not equal to expected '$1'"
|
|
||||||
killall $(basename $bin)
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
mkdir -p test
|
|
||||||
rm test/*
|
|
||||||
redis-cli del notify
|
|
||||||
|
|
||||||
bin=$1;
|
|
||||||
: ${bin:="target/x86_64-unknown-linux-musl/release/notify-redis"}
|
|
||||||
|
|
||||||
echo "running tests with $bin"
|
|
||||||
|
|
||||||
$bin "$PWD/test" redis://localhost notify &
|
|
||||||
|
|
||||||
sleep 1
|
|
||||||
|
|
||||||
echo foo > test/foo.txt
|
|
||||||
sleep 2
|
|
||||||
|
|
||||||
assert_notify "{\"event\":\"modify\",\"path\":\"$(pwd)/test/foo.txt\"}"
|
|
||||||
assert_notify ''
|
|
||||||
|
|
||||||
mv test/foo.txt test/bar.txt
|
|
||||||
sleep 2
|
|
||||||
|
|
||||||
assert_notify "{\"event\":\"move\",\"from\":\"$(pwd)/test/foo.txt\",\"to\":\"$(pwd)/test/bar.txt\"}"
|
|
||||||
assert_notify ""
|
|
||||||
|
|
||||||
rm test/bar.txt
|
|
||||||
echo asd > test/bar.txt
|
|
||||||
|
|
||||||
sleep 2
|
|
||||||
|
|
||||||
assert_notify "{\"event\":\"modify\",\"path\":\"$(pwd)/test/bar.txt\"}"
|
|
||||||
assert_notify ""
|
|
||||||
|
|
||||||
rm test/bar.txt
|
|
||||||
sleep 2
|
|
||||||
echo asd > test/bar.txt
|
|
||||||
|
|
||||||
sleep 2
|
|
||||||
|
|
||||||
assert_notify "{\"event\":\"delete\",\"path\":\"$(pwd)/test/bar.txt\"}"
|
|
||||||
assert_notify "{\"event\":\"modify\",\"path\":\"$(pwd)/test/bar.txt\"}"
|
|
||||||
assert_notify ""
|
|
||||||
|
|
||||||
killall $(basename $bin)
|
|
||||||
162
tests/tests.rs
Normal file
162
tests/tests.rs
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
use notify_redis::{watch, Event};
|
||||||
|
use redis::{Client, Commands, Connection, ConnectionInfo};
|
||||||
|
use std::fs::{remove_file, rename, write};
|
||||||
|
use std::path::Path;
|
||||||
|
use std::thread::sleep;
|
||||||
|
use std::time::Duration;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
fn cleanup(redis: ConnectionInfo, list: &str) {
|
||||||
|
let client = Client::open(redis).unwrap();
|
||||||
|
let mut con = client.get_connection().unwrap();
|
||||||
|
con.del::<_, ()>(list).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
struct EventList {
|
||||||
|
redis: Connection,
|
||||||
|
list: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventList {
|
||||||
|
fn new(redis: ConnectionInfo, list: &str) -> Self {
|
||||||
|
let client = Client::open(redis).unwrap();
|
||||||
|
let redis = client.get_connection().unwrap();
|
||||||
|
EventList {
|
||||||
|
redis,
|
||||||
|
list: list.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Event> {
|
||||||
|
let raw: Option<String> = self.redis.rpop(&self.list, None).unwrap();
|
||||||
|
raw.map(|raw| serde_json::from_str(&raw).unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_watch(
|
||||||
|
path: &Path,
|
||||||
|
redis_connect: ConnectionInfo,
|
||||||
|
list: &str,
|
||||||
|
) -> std::thread::JoinHandle<()> {
|
||||||
|
let path = path.to_path_buf();
|
||||||
|
let list = list.to_string();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
if let Err(e) = watch(path, redis_connect, &list, Duration::from_millis(1)) {
|
||||||
|
eprintln!("watch error {:#}", e);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_basic() {
|
||||||
|
let list = format!("notify_redis_test_{}", rand::random::<u16>());
|
||||||
|
let redis_connect: ConnectionInfo = "redis://localhost".parse().unwrap();
|
||||||
|
cleanup(redis_connect.clone(), &list);
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let mut event_list = EventList::new(redis_connect.clone(), &list);
|
||||||
|
spawn_watch(dir.path(), redis_connect.clone(), &list);
|
||||||
|
|
||||||
|
sleep(Duration::from_millis(10));
|
||||||
|
|
||||||
|
write(dir.path().join("foo.txt"), "foo").unwrap();
|
||||||
|
|
||||||
|
sleep(Duration::from_millis(10));
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
matches!(event_list.next(), Some(Event::Modify {path ,..}) if path.ends_with("foo.txt"))
|
||||||
|
);
|
||||||
|
assert!(matches!(event_list.next(), None));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rename_debounce() {
|
||||||
|
let list = format!("notify_redis_test_{}", rand::random::<u16>());
|
||||||
|
let redis_connect: ConnectionInfo = "redis://localhost".parse().unwrap();
|
||||||
|
cleanup(redis_connect.clone(), &list);
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let mut event_list = EventList::new(redis_connect.clone(), &list);
|
||||||
|
spawn_watch(dir.path(), redis_connect.clone(), &list);
|
||||||
|
|
||||||
|
sleep(Duration::from_millis(10));
|
||||||
|
|
||||||
|
write(dir.path().join("foo.txt"), "foo").unwrap();
|
||||||
|
rename(dir.path().join("foo.txt"), dir.path().join("bar.txt")).unwrap();
|
||||||
|
|
||||||
|
sleep(Duration::from_millis(10));
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
matches!(event_list.next(), Some(Event::Modify {path ,..}) if path.ends_with("bar.txt"))
|
||||||
|
);
|
||||||
|
assert!(matches!(event_list.next(), None));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rename() {
|
||||||
|
let list = format!("notify_redis_test_{}", rand::random::<u16>());
|
||||||
|
let redis_connect: ConnectionInfo = "redis://localhost".parse().unwrap();
|
||||||
|
cleanup(redis_connect.clone(), &list);
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let mut event_list = EventList::new(redis_connect.clone(), &list);
|
||||||
|
spawn_watch(dir.path(), redis_connect.clone(), &list);
|
||||||
|
|
||||||
|
sleep(Duration::from_millis(10));
|
||||||
|
|
||||||
|
write(dir.path().join("foo.txt"), "foo").unwrap();
|
||||||
|
sleep(Duration::from_millis(10));
|
||||||
|
rename(dir.path().join("foo.txt"), dir.path().join("bar.txt")).unwrap();
|
||||||
|
|
||||||
|
sleep(Duration::from_millis(10));
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
matches!(event_list.next(), Some(Event::Modify {path ,..}) if path.ends_with("foo.txt"))
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
matches!(event_list.next(), Some(Event::Move {from, to ,..}) if from.ends_with("foo.txt") && to.ends_with("bar.txt"))
|
||||||
|
);
|
||||||
|
assert!(matches!(dbg!(event_list.next()), None));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delete() {
|
||||||
|
let list = format!("notify_redis_test_{}", rand::random::<u16>());
|
||||||
|
let redis_connect: ConnectionInfo = "redis://localhost".parse().unwrap();
|
||||||
|
cleanup(redis_connect.clone(), &list);
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let mut event_list = EventList::new(redis_connect.clone(), &list);
|
||||||
|
spawn_watch(dir.path(), redis_connect.clone(), &list);
|
||||||
|
|
||||||
|
sleep(Duration::from_millis(10));
|
||||||
|
|
||||||
|
write(dir.path().join("foo.txt"), "foo").unwrap();
|
||||||
|
sleep(Duration::from_millis(10));
|
||||||
|
remove_file(dir.path().join("foo.txt")).unwrap();
|
||||||
|
|
||||||
|
sleep(Duration::from_millis(10));
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
matches!(event_list.next(), Some(Event::Modify {path ,..}) if path.ends_with("foo.txt"))
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
matches!(event_list.next(), Some(Event::Delete {path ,..}) if path.ends_with("foo.txt"))
|
||||||
|
);
|
||||||
|
assert!(matches!(dbg!(event_list.next()), None));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delete_debounce() {
|
||||||
|
let list = format!("notify_redis_test_{}", rand::random::<u16>());
|
||||||
|
let redis_connect: ConnectionInfo = "redis://localhost".parse().unwrap();
|
||||||
|
cleanup(redis_connect.clone(), &list);
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let mut event_list = EventList::new(redis_connect.clone(), &list);
|
||||||
|
spawn_watch(dir.path(), redis_connect.clone(), &list);
|
||||||
|
|
||||||
|
sleep(Duration::from_millis(10));
|
||||||
|
|
||||||
|
write(dir.path().join("foo.txt"), "foo").unwrap();
|
||||||
|
remove_file(dir.path().join("foo.txt")).unwrap();
|
||||||
|
|
||||||
|
sleep(Duration::from_millis(10));
|
||||||
|
|
||||||
|
assert!(matches!(dbg!(event_list.next()), None));
|
||||||
|
}
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
echo "Building static binaries using ekidd/rust-musl-builder"
|
|
||||||
docker build -t build-"$1"-image .
|
|
||||||
docker run -it --name build-"$1" build-"$1"-image
|
|
||||||
docker cp build-"$1":/home/rust/src/target/x86_64-unknown-linux-musl/release/"$1" "$1-$2"
|
|
||||||
docker rm build-"$1"
|
|
||||||
docker rmi build-"$1"-image
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue