Compare commits

...

3 commits

Author SHA1 Message Date
672094558d more precise event times 2023-06-29 18:42:10 +02:00
7430db6c5b bumb dependencies 2023-06-29 17:50:27 +02:00
f6e341a637 nix ci 2023-06-29 17:44:15 +02:00
15 changed files with 1009 additions and 446 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
use flake

90
.github/workflows/nix.yaml vendored Normal file
View 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"

View file

@ -6,34 +6,35 @@ on:
jobs: jobs:
release:
name: Build release
runs-on: ubuntu-20.04
strategy:
matrix: matrix:
target: runs-on: ubuntu-latest
- x86_64-unknown-linux-musl outputs:
- armv7-unknown-linux-musleabihf matrix: ${{ steps.set-matrix.outputs.matrix }}
- aarch64-unknown-linux-musl
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1 - 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: with:
profile: minimal name: ci
toolchain: stable instance: https://cache.icewind.me
override: true authToken: '${{ secrets.ATTIC_TOKEN }}'
target: ${{ matrix.target }} - run: nix build .#${{ matrix.target }}
- uses: Swatinem/rust-cache@v1
- uses: actions-rs/cargo@v1
with:
use-cross: true
command: build
args: --release --target ${{ matrix.target }}
- name: Upload binary to release - name: Upload binary to release
uses: svenstaro/upload-release-action@v2 uses: svenstaro/upload-release-action@v2
with: with:
repo_token: ${{ secrets.GITHUB_TOKEN }} repo_token: ${{ secrets.GITHUB_TOKEN }}
file: target/${{ matrix.target }}/release/notify-redis file: result/bin/cube
asset_name: notify-redis-${{ matrix.target }} asset_name: notify-redis-${{ matrix.target }}
tag: ${{ github.ref }} tag: ${{ github.ref }}

View file

@ -1,71 +0,0 @@
on: [push, pull_request]
name: CI
jobs:
check:
name: Check
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- uses: Swatinem/rust-cache@v1
- uses: actions-rs/cargo@v1
with:
command: check
build:
name: Build
runs-on: ubuntu-20.04
strategy:
matrix:
target:
- x86_64-unknown-linux-musl
- armv7-unknown-linux-musleabihf
- aarch64-unknown-linux-musl
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
target: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v1
- uses: actions-rs/cargo@v1
with:
use-cross: true
command: build
args: --release --target ${{ matrix.target }}
- uses: actions/upload-artifact@v2
with:
name: notify-redis-${{ matrix.target }}
path: target/${{ matrix.target }}/release/notify-redis
test:
name: Tests
runs-on: ubuntu-20.04
services:
redis:
image: redis
ports:
- 6379:6379
options: --entrypoint redis-server
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- uses: Swatinem/rust-cache@v1
- uses: actions-rs/cargo@v1
with:
command: test

2
.gitignore vendored
View file

@ -1,3 +1,5 @@
/test /test
/target /target
**/*.rs.bk **/*.rs.bk
.direnv
result

936
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -5,15 +5,17 @@ authors = ["Robin Appelman <robin@icewind.nl>"]
edition = "2018" edition = "2018"
[dependencies] [dependencies]
notify = "4.0" notify = "6.0"
redis = { version = "0.20", default-features = false } 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.5" color-eyre = "0.6"
clap = { version = "4.3.9", features = ["derive"] }
[dev-dependencies] [dev-dependencies]
rand = "0.8.3" rand = "0.8.5"
tempfile = "3" tempfile = "3"
[profile.release] [profile.release]

View file

@ -1,4 +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

View file

@ -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
View 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
View 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
View 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;
};
}

View file

@ -1,11 +1,13 @@
use chrono::{DateTime, Timelike, Utc};
use color_eyre::{eyre::WrapErr, Result}; use color_eyre::{eyre::WrapErr, Result};
use notify::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher}; use notify::event::{ModifyKind, RenameMode};
use notify::{EventKind, RecursiveMode, Watcher};
use notify_debouncer_full::{new_debouncer, DebouncedEvent};
use redis::{Client, Commands, Connection, IntoConnectionInfo}; use redis::{Client, Commands, Connection, IntoConnectionInfo};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::mpsc::channel; use std::sync::mpsc::channel;
use std::time::Duration; use std::time::Duration;
use time::OffsetDateTime;
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "event")] #[serde(tag = "event")]
@ -13,30 +15,46 @@ use std::time::Duration;
pub enum Event { pub enum Event {
Modify { Modify {
path: PathBuf, path: PathBuf,
time: DateTime<Utc>, #[serde(with = "time::serde::iso8601")]
time: OffsetDateTime,
}, },
Move { Move {
from: PathBuf, from: PathBuf,
to: PathBuf, to: PathBuf,
time: DateTime<Utc>, #[serde(with = "time::serde::iso8601")]
time: OffsetDateTime,
}, },
Delete { Delete {
path: PathBuf, path: PathBuf,
time: DateTime<Utc>, #[serde(with = "time::serde::iso8601")]
time: OffsetDateTime,
}, },
None, None,
} }
impl From<DebouncedEvent> for Event { impl From<DebouncedEvent> for Event {
fn from(event: DebouncedEvent) -> Self { fn from(event: DebouncedEvent) -> Self {
let time = Utc::now().with_nanosecond(0).unwrap(); let now = OffsetDateTime::now_utc();
let elapsed = event.time.elapsed();
let time = now - elapsed;
match event { let path_count = event.paths.len();
DebouncedEvent::Write(path) let mut paths = event.event.paths.into_iter();
| DebouncedEvent::Create(path)
| DebouncedEvent::Chmod(path) => Event::Modify { path, time }, match (event.event.kind, path_count) {
DebouncedEvent::Rename(from, to) => Event::Move { from, to, time }, (EventKind::Modify(ModifyKind::Name(RenameMode::Both)), 2..) => Event::Move {
DebouncedEvent::Remove(path) => Event::Delete { path, time }, 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, _ => Event::None,
} }
} }
@ -50,17 +68,21 @@ pub fn watch(
) -> Result<()> { ) -> Result<()> {
let (tx, rx) = channel(); let (tx, rx) = channel();
let mut watcher: RecommendedWatcher = Watcher::new(tx, debounce)?; let mut watcher = new_debouncer(debounce, None, tx)?;
let client = Client::open(redis_connect).wrap_err("Invalid redis connection")?; let client = Client::open(redis_connect).wrap_err("Invalid redis connection")?;
let mut con = client let mut con = client
.get_connection() .get_connection()
.wrap_err("Failed to open redis connection")?; .wrap_err("Failed to open redis connection")?;
watcher.watch(path, RecursiveMode::Recursive)?; watcher
.watcher()
.watch(path.as_ref(), RecursiveMode::Recursive)?;
while let Ok(event) = rx.recv() { 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")?; push_event(event, &mut con, redis_list).wrap_err("Failed to send event to redis")?;
} }
}
Ok(()) Ok(())
} }

View file

@ -1,14 +1,28 @@
use clap::Parser;
use color_eyre::Result; use color_eyre::Result;
use notify_redis::watch; use notify_redis::watch;
use std::env; use std::path::PathBuf;
use std::time::Duration; use std::time::Duration;
/// Push filesystem notifications into a redis list
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
/// Folder to watch
path: PathBuf,
/// Redis connection string
redis_connect: String,
/// Redis list to push changes to
redis_list: String,
}
fn main() -> Result<()> { fn main() -> Result<()> {
let args: Vec<_> = env::args().collect(); let args: Args = Args::parse();
if let [_, path, redis, list] = args.as_slice() { watch(
watch(path, redis.as_str(), list, Duration::from_secs(2))?; args.path,
} else { args.redis_connect,
println!("usage: {} <path> <redis_connect> <redis_list>", args[0]) &args.redis_list,
} Duration::from_secs(2),
)?;
Ok(()) Ok(())
} }

View file

@ -28,7 +28,7 @@ impl EventList {
} }
fn next(&mut self) -> Option<Event> { fn next(&mut self) -> Option<Event> {
let raw: Option<String> = self.redis.rpop(&self.list).unwrap(); let raw: Option<String> = self.redis.rpop(&self.list, None).unwrap();
raw.map(|raw| serde_json::from_str(&raw).unwrap()) raw.map(|raw| serde_json::from_str(&raw).unwrap())
} }
} }