├── src ├── lib.rs ├── shared_config.rs ├── client │ ├── subcommand │ │ ├── mod.rs │ │ ├── start_subcommand.rs │ │ ├── stop_subcommand.rs │ │ ├── modify_subcommand.rs │ │ ├── status_subcommand.rs │ │ ├── generate_subcommand.rs │ │ ├── register_subcommand.rs │ │ ├── trace_subcommand.rs │ │ └── subcommand_utils.rs │ └── main.rs ├── shared_utils.rs └── foldend │ ├── main.rs │ ├── config.rs │ ├── server │ ├── mod.rs │ └── handler_service.rs │ ├── handler_mapping.rs │ ├── startup │ ├── windows.rs │ └── mod.rs │ └── mapping.rs ├── generated_types ├── src │ └── lib.rs ├── Cargo.toml ├── build.rs ├── handler_types.proto └── handler_service.proto ├── .gitattributes ├── debian ├── foldend_config.toml └── foldend.service ├── pipelines ├── src │ ├── lib.rs │ ├── pipeline_context_input.rs │ ├── pipeline_config.rs │ ├── event.rs │ ├── pipeline_execution_context.rs │ ├── actions │ │ ├── mod.rs │ │ ├── run_cmd.rs │ │ └── move_to_dir.rs │ └── pipeline_handler.rs └── Cargo.toml ├── .gitignore ├── examples └── example_pipelines │ ├── execute_make.toml │ └── move_to_subfolder_by_current_date.toml ├── LICENSE.md ├── FAQ.md ├── .github └── workflows │ └── integration.yml ├── Cargo.toml ├── ARCHITECTURE.md ├── README.md └── wix └── main.wxs /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod shared_config; 2 | pub mod shared_utils; 3 | -------------------------------------------------------------------------------- /src/shared_config.rs: -------------------------------------------------------------------------------- 1 | pub const DEFAULT_PORT: &str = "4575"; 2 | -------------------------------------------------------------------------------- /generated_types/src/lib.rs: -------------------------------------------------------------------------------- 1 | include!(concat!(env!("OUT_DIR"), "/handler_service.rs")); 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | *.{cmd,[cC][mM][dD]} text eol=crlf 3 | *.{bat,[bB][aA][tT]} text eol=crlf -------------------------------------------------------------------------------- /debian/foldend_config.toml: -------------------------------------------------------------------------------- 1 | mapping_state_path = "/etc/foldend/foldend_mapping.toml" 2 | tracing_file_path = "/etc/foldend/foldend_logs.log" 3 | concurrent_threads_limit = 10 -------------------------------------------------------------------------------- /pipelines/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod actions; 2 | pub mod event; 3 | pub mod pipeline_config; 4 | mod pipeline_context_input; 5 | mod pipeline_execution_context; 6 | pub mod pipeline_handler; 7 | -------------------------------------------------------------------------------- /debian/foldend.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Folden Service 3 | 4 | [Service] 5 | ExecStart=/usr/sbin/foldend --config /etc/foldend/foldend_config.toml run 6 | 7 | [Install] 8 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /pipelines/src/pipeline_context_input.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Clone, Copy, Debug, Serialize, Deserialize)] 4 | pub enum PipelineContextInput { 5 | EventFilePath, 6 | ActionFilePath, 7 | } 8 | -------------------------------------------------------------------------------- /src/client/subcommand/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod generate_subcommand; 2 | pub mod modify_subcommand; 3 | pub mod register_subcommand; 4 | pub mod start_subcommand; 5 | pub mod status_subcommand; 6 | pub mod stop_subcommand; 7 | pub mod subcommand_utils; 8 | pub mod trace_subcommand; 9 | -------------------------------------------------------------------------------- /src/shared_utils.rs: -------------------------------------------------------------------------------- 1 | use clap::Arg; 2 | 3 | use crate::shared_config::DEFAULT_PORT; 4 | 5 | pub fn construct_port_arg<'a, 'b>() -> Arg<'a, 'b> { 6 | Arg::with_name("port") 7 | .short("p") 8 | .long("port") 9 | .default_value(DEFAULT_PORT) 10 | .empty_values(false) 11 | .takes_value(true) 12 | } 13 | -------------------------------------------------------------------------------- /generated_types/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "generated_types" 3 | version = "0.3.0" 4 | authors = ["STRONG-MAD "] 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 | futures = "0.3.15" 11 | prost = "0.7.0" 12 | prost-types = "0.7.0" 13 | tonic = "0.4.3" 14 | 15 | [build-dependencies] 16 | tonic-build = "0.4.2" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | **/target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # Workspace files generated by IDE 14 | .idea/** 15 | .vscode/** -------------------------------------------------------------------------------- /examples/example_pipelines/execute_make.toml: -------------------------------------------------------------------------------- 1 | # Pipeline to run `make` command when any change to a relevant file is noticed - 2 | # In the current directory or in any subdirectory. 3 | watch_recursive = true 4 | apply_on_startup_on_existing_files = false 5 | panic_handler_on_error = false 6 | 7 | [event] 8 | events = ["modify"] 9 | naming_regex_match = "\\.(js|css|html)$" 10 | 11 | [[actions]] 12 | type = "RunCmd" 13 | input = "EventFilePath" 14 | command = "make" 15 | input_formatting = true 16 | datetime_formatting = true 17 | -------------------------------------------------------------------------------- /generated_types/build.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | fn main() { 4 | let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 5 | let proto_files = vec![ 6 | root.join("handler_types.proto"), 7 | root.join("handler_service.proto"), 8 | ]; 9 | 10 | // Tell cargo to recompile if any of these proto files are changed 11 | for proto_file in &proto_files { 12 | println!("cargo:rerun-if-changed={}", proto_file.display()); 13 | } 14 | 15 | tonic_build::configure() 16 | .compile(&proto_files, &[root]) 17 | .unwrap(); 18 | } 19 | -------------------------------------------------------------------------------- /generated_types/handler_types.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package handler_service; 3 | 4 | message HandlerSummary { 5 | bool is_alive = 1; 6 | string config_path = 2; 7 | bool is_auto_startup = 3; 8 | string description = 4; 9 | } 10 | 11 | message HandlerStateResponse { 12 | bool is_alive = 1; 13 | string message = 2; 14 | } 15 | 16 | message HandlerStatesMapResponse { 17 | map states_map = 1; // key is directory path 18 | } 19 | 20 | message HandlerSummaryMapResponse { 21 | map summary_map = 1; // key is directory path 22 | } 23 | -------------------------------------------------------------------------------- /examples/example_pipelines/move_to_subfolder_by_current_date.toml: -------------------------------------------------------------------------------- 1 | # Pipeline to store files in a directory in an inner subdirectory. 2 | # Based on the date (ISO-8601 format) the handler noticed the files - 3 | # When the handler is started up, any files already in the dir will be moved based on the current date. 4 | watch_recursive = false 5 | apply_on_startup_on_existing_files = true 6 | panic_handler_on_error = false 7 | 8 | [event] 9 | events = ["create", "modify"] 10 | naming_regex_match = ".*" 11 | 12 | [[actions]] 13 | type = "MoveToDir" 14 | input = "EventFilePath" 15 | directory_path = "%Y-%m-%d" # Or "%F" for shorthand ISO-8601 format 16 | requires_directory_exists = false 17 | replace_older_files = true 18 | keep_input_file_intact = false 19 | datetime_formatting = true -------------------------------------------------------------------------------- /pipelines/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pipelines" 3 | version = "0.3.0" 4 | authors = ["STRONG-MAD "] 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 | generated_types = { path = "../generated_types" } 11 | 12 | strum = "0.21.0" 13 | strum_macros = "0.21.1" 14 | clap = "2.33.3" 15 | regex = "1.5.4" 16 | itertools = "0.10.0" 17 | chrono = "0.4.19" 18 | crossbeam = "0.8.1" 19 | notify = "5.0.0-pre.7" 20 | serde = {version = "1.0.117", features = ["derive"] } 21 | serde_json = "1.0.64" 22 | toml = "0.5.8" 23 | typetag = "0.1.7" 24 | tonic = "0.4.3" 25 | tokio = { version = "1.6.1", features = ["sync"] } 26 | lazy_static = "1.4.0" 27 | tracing = "0.1.26" 28 | tracing-futures = "0.2.5" -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Mayan Shoshani 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/foldend/main.rs: -------------------------------------------------------------------------------- 1 | mod config; 2 | mod handler_mapping; 3 | mod mapping; 4 | mod server; 5 | mod startup; 6 | 7 | #[cfg(windows)] 8 | use futures::executor::block_on; 9 | 10 | #[cfg(windows)] 11 | fn main() { 12 | match startup::windows::run() { 13 | Ok(_) => {} // Service finished running 14 | Err(e) => { 15 | match e { 16 | startup::windows::Error::Winapi(winapi_err) => { 17 | // If not being run inside windows service framework attempt commandline execution. 18 | if winapi_err.raw_os_error().unwrap() == 1063 { 19 | tracing::warn!( 20 | "Attempting Foldend execution outside of Windows service framework" 21 | ); 22 | block_on(startup::windows::sync_main(None)).unwrap(); 23 | } 24 | } 25 | _ => { 26 | tracing::error!("{}", e); 27 | } 28 | } 29 | } 30 | } 31 | } 32 | 33 | #[cfg(not(windows))] 34 | #[tokio::main] 35 | async fn main() -> Result<(), Box> { 36 | startup::main_service_runtime().await 37 | } 38 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | # State of the project 2 | 3 | The project isn't yet at a stage to be officially regarded as stable (v1.0): 4 | 5 | - Missing vital QOL features & optimizations, including various common actions. 6 | - Missing installation for OS's planned to be supported. 7 | - Needs to be tested for a much longer period over all supported OS's. 8 | 9 | Do take this in mind if you're considering trying it out! 10 | 11 | Folden, if used haphazardly, can be very dangerous. 12 | 13 | Aside from planned restrictions to be added - 14 | 15 | Keep in mind this application is designed to allow such power. 16 | 17 | Check out the [roadmap](https://github.com/STRONG-MAD/Folden/projects/1) to see progress and what's planned. 18 | 19 | If you find any issues or have an idea in mind, open an issue :) 20 | 21 | # Differences to other rust crates 22 | 23 | - [notify](https://github.com/notify-rs/notify): 24 | 25 | Library used to create file watchers. Folden uses it behind the scenes. 26 | 27 | - [watchexec](https://github.com/watchexec/watchexec): 28 | 29 | CLI tool used to apply a command on a watched file / directory. 30 | 31 | - `Folden`: 32 | 33 | System wide service and application; 34 | 35 | Used for managing multiple file watching command handlers. 36 | 37 | Installation comes with a CLI and system service. 38 | -------------------------------------------------------------------------------- /src/foldend/config.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | convert::TryFrom, 3 | fs, 4 | path::{Path, PathBuf}, 5 | }; 6 | 7 | use serde::{Deserialize, Serialize}; 8 | 9 | use folden::shared_config::DEFAULT_PORT; 10 | 11 | pub const DEFAULT_CONCURRENT_THREADS_LIMIT: u8 = 10; 12 | 13 | #[derive(Debug, Serialize, Deserialize)] 14 | pub struct Config { 15 | pub mapping_state_path: PathBuf, 16 | pub tracing_file_path: PathBuf, 17 | pub concurrent_threads_limit: u8, 18 | #[serde(skip)] 19 | pub port: u16, 20 | } 21 | 22 | impl Config { 23 | pub fn save(&self, file_path: &Path) -> Result<(), std::io::Error> { 24 | let config_data: Vec = self.into(); 25 | fs::write(file_path, config_data) 26 | } 27 | } 28 | 29 | impl Default for Config { 30 | fn default() -> Self { 31 | Self { 32 | mapping_state_path: PathBuf::from("foldend_mapping.toml"), 33 | tracing_file_path: PathBuf::from("foldend.log"), 34 | concurrent_threads_limit: DEFAULT_CONCURRENT_THREADS_LIMIT, 35 | port: DEFAULT_PORT.parse().unwrap(), 36 | } 37 | } 38 | } 39 | 40 | impl TryFrom> for Config { 41 | type Error = toml::de::Error; 42 | 43 | fn try_from(bytes: Vec) -> Result { 44 | toml::from_slice(&bytes) 45 | } 46 | } 47 | 48 | impl From<&Config> for Vec { 49 | fn from(val: &Config) -> Self { 50 | toml::to_vec(&val).unwrap() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /pipelines/src/pipeline_config.rs: -------------------------------------------------------------------------------- 1 | use std::{convert::TryFrom, fs, io, path::Path}; 2 | 3 | use clap::Values; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::{actions::PipelineActions, event::PipelineEvent}; 7 | 8 | #[derive(Clone, Debug, Serialize, Deserialize)] 9 | pub struct PipelineConfig { 10 | pub watch_recursive: bool, 11 | pub apply_on_startup_on_existing_files: bool, 12 | pub panic_handler_on_error: bool, 13 | pub event: PipelineEvent, 14 | pub actions: Vec, 15 | } 16 | 17 | impl PipelineConfig { 18 | pub fn default_new(events: Option, actions: Option) -> Self { 19 | Self { 20 | watch_recursive: false, 21 | apply_on_startup_on_existing_files: false, 22 | panic_handler_on_error: false, 23 | event: match events { 24 | Some(events) => PipelineEvent::from(events), 25 | None => PipelineEvent::default(), 26 | }, 27 | actions: match actions { 28 | Some(actions) => PipelineActions::defaults(actions), 29 | None => vec![PipelineActions::default()], 30 | }, 31 | } 32 | } 33 | 34 | pub fn generate_config(&self, path: &Path) -> io::Result<()> { 35 | fs::write(path, toml::to_vec(*Box::new(self)).unwrap()) 36 | } 37 | } 38 | 39 | impl TryFrom> for PipelineConfig { 40 | type Error = toml::de::Error; 41 | 42 | fn try_from(bytes: Vec) -> Result { 43 | toml::from_slice(&bytes) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /pipelines/src/event.rs: -------------------------------------------------------------------------------- 1 | use clap::Values; 2 | use itertools::Itertools; 3 | use notify::EventKind; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[derive(Clone, Debug, Serialize, Deserialize)] 7 | pub struct PipelineEvent { 8 | pub events: Vec, // Can flag multiple events in the config to initiate the pipeline against. 9 | pub naming_regex_match: Option, 10 | } 11 | 12 | pub const EVENT_TYPES: [&str; 2] = ["create", "modify"]; 13 | 14 | impl PipelineEvent { 15 | fn is_handled_event_kind(name: &str, kind: &EventKind) -> bool { 16 | match name.to_lowercase().as_str() { 17 | "create" => kind.is_create(), 18 | "modify" => kind.is_modify(), 19 | _ => false, 20 | } 21 | } 22 | 23 | pub fn is_handled_event(&self, kind: &EventKind) -> bool { 24 | for event_name in &self.events { 25 | if PipelineEvent::is_handled_event_kind(event_name, kind) { 26 | return true; 27 | } 28 | } 29 | false 30 | } 31 | } 32 | 33 | impl From> for PipelineEvent { 34 | fn from(events: Values) -> Self { 35 | Self { 36 | events: events.map(|event| event.to_string()).unique().collect(), 37 | naming_regex_match: Some(String::from(".*")), 38 | } 39 | } 40 | } 41 | 42 | impl Default for PipelineEvent { 43 | fn default() -> Self { 44 | Self { 45 | events: EVENT_TYPES.iter().map(|event| event.to_string()).collect(), 46 | naming_regex_match: Some(String::from(".*")), 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /generated_types/handler_service.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package handler_service; 3 | 4 | import "handler_types.proto"; 5 | import "google/protobuf/empty.proto"; 6 | import "google/protobuf/wrappers.proto"; 7 | 8 | service HandlerService { 9 | rpc RegisterToDirectory (RegisterToDirectoryRequest) returns (HandlerStateResponse); 10 | rpc GetDirectoryStatus (GetDirectoryStatusRequest) returns (HandlerSummaryMapResponse); 11 | rpc StartHandler (StartHandlerRequest) returns (HandlerStatesMapResponse); 12 | rpc StopHandler (StopHandlerRequest) returns (HandlerStatesMapResponse); 13 | rpc ModifyHandler (ModifyHandlerRequest) returns (google.protobuf.Empty); 14 | rpc TraceHandler (TraceHandlerRequest) returns (stream TraceHandlerResponse); 15 | } 16 | 17 | message RegisterToDirectoryRequest { 18 | string directory_path = 1; 19 | string handler_config_path = 2; 20 | bool is_start_on_register = 3; 21 | bool is_auto_startup = 4; 22 | } 23 | 24 | message GetDirectoryStatusRequest { 25 | string directory_path = 1; 26 | } 27 | 28 | message StartHandlerRequest { 29 | string directory_path = 1; 30 | } 31 | 32 | message StopHandlerRequest { 33 | string directory_path = 1; 34 | bool remove = 2; 35 | } 36 | 37 | message ModifyHandlerRequest { 38 | string directory_path = 1; 39 | google.protobuf.BoolValue is_auto_startup = 2; 40 | google.protobuf.StringValue modify_description = 3; 41 | } 42 | 43 | message TraceHandlerRequest { 44 | string directory_path = 1; 45 | } 46 | 47 | message TraceHandlerResponse { 48 | string directory_path = 1; 49 | google.protobuf.StringValue action = 2; 50 | string message = 3; 51 | } -------------------------------------------------------------------------------- /.github/workflows/integration.yml: -------------------------------------------------------------------------------- 1 | name: Testing & distribution 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main, dev ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | name: 'build-${{matrix.os}}' 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | matrix: 18 | os: [ubuntu-latest, windows-latest] 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Build members 22 | run: cargo build --workspace --verbose 23 | - name: Run clippy lint test 24 | run: cargo clippy --all-targets --all-features -- -D warnings 25 | - name: Run rustfmt test 26 | run: cargo fmt --all -- --check 27 | - name: Run tests 28 | run: cargo test --workspace --verbose 29 | deb-build: 30 | needs: [build] 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: actions/checkout@v2 34 | - name: Ensure cargo-deb crate 35 | run: cargo install cargo-deb 36 | - name: Generate package 37 | run: cargo deb 38 | - name: 'Upload Artifact' 39 | uses: actions/upload-artifact@v2 40 | with: 41 | name: folden_amd64.deb 42 | path: target/debian 43 | retention-days: 1 44 | windows-build: 45 | needs: [build] 46 | runs-on: windows-latest 47 | steps: 48 | - uses: actions/checkout@v2 49 | - name: Ensure cargo-wix crate 50 | run: cargo install cargo-wix 51 | - name: Generate installer 52 | run: cargo wix -v --nocapture -o . 53 | - name: Upload Artifact 54 | uses: actions/upload-artifact@v2 55 | with: 56 | name: folden.msi 57 | path: ./*.msi 58 | retention-days: 1 -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "folden" 3 | version = "0.3.0" 4 | authors = ["STRONG-MAD "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [[bin]] 10 | name = "folden" 11 | path = "src/client/main.rs" 12 | 13 | [[bin]] 14 | name = "foldend" 15 | path = "src/foldend/main.rs" 16 | 17 | [workspace] 18 | members = [ 19 | "generated_types", 20 | "pipelines" 21 | ] 22 | 23 | [dependencies] 24 | generated_types = {path = "generated_types"} 25 | pipelines = {path = "pipelines"} 26 | 27 | strum = "0.21.0" 28 | strum_macros = "0.21.1" 29 | toml = "0.5.8" 30 | tonic = "0.4.3" 31 | clap = "2.33.3" 32 | typetag = "0.1.7" 33 | futures = "0.3.15" 34 | cli-table = "0.4.6" 35 | dyn-clone = "1.0.4" 36 | async-stream = "0.3.2" 37 | serde = {version = "1.0.117", features = ["derive"] } 38 | tokio = { version = "1.6.1", features = ["full"] } 39 | tokio-stream = { version ="0.1.6", features = ["default", "sync"] } 40 | 41 | # foldend dependencies 42 | serde_json = "1.0.64" 43 | crossbeam = "0.8.1" 44 | notify = "5.0.0-pre.7" 45 | tracing = "0.1.26" 46 | tracing-appender = "0.1.2" 47 | tracing-futures = "0.2.5" 48 | tracing-subscriber = "0.2.18" 49 | 50 | [target.'cfg(windows)'.dependencies] 51 | windows-service = "0.3.1" 52 | 53 | [package.metadata.deb] 54 | section = "utility" 55 | maintainer-scripts = "debian/" 56 | systemd-units = { enable = false } 57 | assets = [ 58 | ["debian/foldend_config.toml", "/etc/foldend/foldend_config.toml", "644"], 59 | ["debian/foldend.service", "/lib/systemd/system/foldend.service", "644"], 60 | ["target/release/folden", "/usr/bin/", "755"], 61 | ["target/release/foldend", "/usr/sbin/", "755"] 62 | ] -------------------------------------------------------------------------------- /src/foldend/server/mod.rs: -------------------------------------------------------------------------------- 1 | use std::pin::Pin; 2 | use std::{ops::Deref, sync::Arc}; 3 | 4 | use tokio::sync::broadcast; 5 | use tokio::sync::RwLock; 6 | use tokio_stream::Stream; 7 | 8 | use crate::config::Config; 9 | use crate::mapping::Mapping; 10 | 11 | pub mod handler_service; 12 | 13 | #[derive(Debug)] 14 | pub struct Server { 15 | pub config: Arc, 16 | pub mapping: Arc>, 17 | pub handlers_trace_tx: 18 | Arc>>, 19 | } 20 | 21 | impl Server { 22 | pub fn is_concurrent_handlers_limit_reached(&self, mapping: &T) -> bool 23 | where 24 | T: Deref, 25 | { 26 | let mut live_handlers_count: u8 = 0; 27 | if live_handlers_count >= self.config.concurrent_threads_limit { 28 | return true; 29 | } 30 | for handler_mapping in mapping.directory_mapping.values() { 31 | if handler_mapping.is_alive() { 32 | live_handlers_count += 1; 33 | if live_handlers_count >= self.config.concurrent_threads_limit { 34 | return true; 35 | } 36 | } 37 | } 38 | false 39 | } 40 | 41 | fn convert_trace_channel_reciever_to_stream(&self) -> TraceHandlerStream { 42 | let mut rx = self.handlers_trace_tx.subscribe(); 43 | Box::pin(async_stream::stream! { 44 | while let Ok(item) = rx.recv().await { 45 | yield item; 46 | } 47 | }) 48 | } 49 | } 50 | 51 | pub type TraceHandlerStream = Pin< 52 | Box< 53 | dyn Stream> 54 | + Send 55 | + Sync 56 | + 'static, 57 | >, 58 | >; 59 | -------------------------------------------------------------------------------- /src/client/subcommand/start_subcommand.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use clap::{App, ArgMatches}; 4 | use futures::executor::block_on; 5 | 6 | use super::subcommand_utils::{ 7 | construct_directory_or_all_args, construct_simple_output_arg, 8 | get_path_from_matches_or_current_path, print_handler_states, SubCommandUtil, 9 | }; 10 | use folden::shared_utils::construct_port_arg; 11 | use generated_types::{handler_service_client::HandlerServiceClient, StartHandlerRequest}; 12 | 13 | #[derive(Clone)] 14 | pub struct StartSubCommand; 15 | 16 | impl SubCommandUtil for StartSubCommand { 17 | fn name(&self) -> &str { 18 | "start" 19 | } 20 | 21 | fn alias(&self) -> &str { 22 | "" 23 | } 24 | 25 | fn requires_connection(&self) -> bool { 26 | true 27 | } 28 | 29 | fn construct_subcommand(&self) -> App { 30 | self.create_instance() 31 | .about("Start handler on directory") 32 | .args(construct_directory_or_all_args().as_slice()) 33 | .arg(construct_port_arg()) 34 | .arg(construct_simple_output_arg()) 35 | } 36 | 37 | fn subcommand_connection_runtime( 38 | &self, 39 | sub_matches: &ArgMatches, 40 | mut client: HandlerServiceClient, 41 | ) { 42 | let mut path = PathBuf::new(); 43 | if !sub_matches.is_present("all") { 44 | path = get_path_from_matches_or_current_path(sub_matches, "directory").unwrap(); 45 | } 46 | let response = client.start_handler(StartHandlerRequest { 47 | directory_path: String::from(path.as_os_str().to_str().unwrap()), 48 | }); 49 | match block_on(response) { 50 | Ok(response) => print_handler_states(response.into_inner(), sub_matches), 51 | Err(e) => println!("{}", e.message()), 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/client/main.rs: -------------------------------------------------------------------------------- 1 | pub mod subcommand; 2 | 3 | use clap::{crate_version, App, AppSettings}; 4 | 5 | use subcommand::subcommand_utils::{connect_client, construct_server_url, SubCommandCollection}; 6 | 7 | #[tokio::main] 8 | async fn main() { 9 | let mut subcommands = SubCommandCollection::new(); 10 | subcommands.add(Box::new( 11 | subcommand::register_subcommand::RegisterSubCommand {}, 12 | )); 13 | subcommands.add(Box::new(subcommand::status_subcommand::StatusSubCommand {})); 14 | subcommands.add(Box::new(subcommand::start_subcommand::StartSubCommand {})); 15 | subcommands.add(Box::new(subcommand::stop_subcommand::StopSubCommand {})); 16 | subcommands.add(Box::new( 17 | subcommand::generate_subcommand::GenerateSubCommand {}, 18 | )); 19 | subcommands.add(Box::new(subcommand::modify_subcommand::ModifySubCommand {})); 20 | subcommands.add(Box::new(subcommand::trace_subcommand::TraceSubCommand {})); 21 | let subcommands_clone = subcommands.clone(); 22 | 23 | let app = App::new("Folden") 24 | .version(crate_version!()) 25 | .about("System-wide folder event handling") 26 | .setting(AppSettings::SubcommandRequiredElseHelp) 27 | .subcommands(subcommands_clone.collect_as_apps()); 28 | 29 | let matches = app.get_matches(); 30 | for subcommand in subcommands { 31 | if let Some(sub_matches) = subcommand.subcommand_matches(&matches) { 32 | if subcommand.requires_connection() { 33 | if let Some(server_url) = construct_server_url(sub_matches) { 34 | match connect_client(server_url) { 35 | Ok(client) => subcommand.subcommand_connection_runtime(sub_matches, client), 36 | Err(e) => println!("{}", e), 37 | } 38 | } else { 39 | println!("Couldn't send request - No valid endpoint could be parsed"); 40 | } 41 | } else { 42 | subcommand.subcommand_runtime(sub_matches); 43 | } 44 | return; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ARCHITECTURE.md: -------------------------------------------------------------------------------- 1 | # Roles 2 | 3 | - Client: Cli tool to contact the background service 4 | 5 | - Foldend: Background server service. 6 | 7 | Stores configuration and mapping of handlers. 8 | 9 | Foldend instantiates pipeline handler threads and is responsible for their lifetime. 10 | 11 | - Pipeline handler: File watching handler responsible for executing pipeline logic on it's directory. 12 | 13 | # Foldend lifecycle 14 | 15 | 1. Read configuration 16 | 17 | 2. Startup registered handlers configured to start alongside service startup. 18 | 19 | 3. Listen for client requests 20 | 21 | # Foldend handler thread lifecycle 22 | 23 | 1. Start file watcher thread on directory. 24 | 25 | 2. (Optional based on configuration) Apply pipeline on all files in directory. 26 | 27 | 3. Read file watcher events to decide on if to execute pipeline. 28 | 29 | 4. Pipeline execution - 30 | 31 | Each `action` is applied sequentially, and is required to succeed, 32 | 33 | To advance to the next `action`. Otherwise ending the pipeline for the current event. 34 | 35 | File watching events are handled sequentially as well to not apply pipeline on same file. 36 | 37 | # Features & Core concepts 38 | 39 | - `Pipeline handler` - A thread designated to file watching a specific directory. 40 | 41 | Can also refer to the configuration of the handler. 42 | 43 | - `Event` - Ruleset on what file watching events to apply pipeline on. 44 | - `Action` - Common logic applied as a stage in a pipeline. 45 | - `Input` - References file paths relevant to a single pipeline: 46 | - `EventFilePath` - File path of the original file the event was referring to. 47 | - `ActionFilePath` - File path of the previous file that an action digested. 48 | 49 | Some `actions` create / move the file after working with the original file; 50 | 51 | This field's value will change at the end of every `action` as part of the pipeline. 52 | 53 | Can't be used on the first action in a pipeline. 54 | - Input file path formatting on specific action fields using the keyword - `$input$`. 55 | - Datetime formatting on specific action fields using [strftime conventions](https://docs.rs/chrono/latest/chrono/format/strftime/). 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What is Folden? 2 | 3 | Application to handle and modify files for common scenarios. System wide. 4 | 5 | Using simple pipelines, designed to be easily created & monitored from your CLI. 6 | 7 | # Motivation 8 | 9 | Folden is meant to allow anyone to easily apply common logic to specific directories. 10 | 11 | Not recommended currently for production critical or overtly complex operation needs. 12 | 13 | # How does it work? 14 | 15 | After [installing Folden](https://github.com/STRONG-MAD/Folden/releases), the application service runs in the background. 16 | 17 | Use the `folden` command to apply and check directories being handled (as long as the service is up). 18 | 19 | # Example usage 20 | 21 | 1. Create a *new pipeline file (Be sure to modify the file itself accordingly): 22 | 23 | ```cmd 24 | USAGE: 25 | folden generate [FLAGS] [OPTIONS] [--] [path] 26 | 27 | OPTIONS: 28 | --actions ... [possible values: runcmd, movetodir] 29 | --events ... [possible values: create, modify] 30 | 31 | ARGS: 32 | File path. Leave empty to generate default name. 33 | ``` 34 | 35 | \* Alternatively check out the [example pipeline files](examples/example_pipelines/execute_make.toml) for common use cases 36 | 37 | 2. Register pipeline to directory: 38 | 39 | ```cmd 40 | USAGE: 41 | folden register [FLAGS] [OPTIONS] [directory] 42 | 43 | ARGS: 44 | Handler pipeline configuration file 45 | Directory to register to. Leave empty to apply on current. 46 | ``` 47 | 48 | 3. That's it! You can interact with registered handlers (be sure to check out all options using `--help`): 49 | 50 | ```cmd 51 | folden status ... 52 | folden start ... 53 | folden stop ... 54 | folden modify ... 55 | ``` 56 | 57 | Example interaction - Setting handler to start with service startup: 58 | 59 | ```cmd 60 | folden modify --startup auto 61 | ``` 62 | 63 | # Learn more 64 | 65 |

66 | 67 | Architecture  •   68 | FAQ  •   69 | Download 70 | 71 |

72 | -------------------------------------------------------------------------------- /src/foldend/handler_mapping.rs: -------------------------------------------------------------------------------- 1 | use crossbeam::channel::Sender; 2 | use notify::ErrorKind as NotifyErrorKind; 3 | use notify::{Event, EventKind}; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use generated_types::{HandlerSummary, ModifyHandlerRequest}; 7 | 8 | #[derive(Clone, Debug, Serialize, Deserialize)] 9 | pub struct HandlerMapping { 10 | #[serde(skip)] 11 | pub watcher_tx: Option>>, // Channel sender providing thread health and allowing manual thread shutdown 12 | pub handler_config_path: String, 13 | pub is_auto_startup: bool, 14 | pub description: String, 15 | } 16 | 17 | impl HandlerMapping { 18 | pub fn new(handler_config_path: String, is_auto_startup: bool, description: String) -> Self { 19 | Self { 20 | watcher_tx: None, 21 | handler_config_path, 22 | is_auto_startup, 23 | description, 24 | } 25 | } 26 | 27 | pub fn is_alive(&self) -> bool { 28 | match self.watcher_tx.clone() { 29 | Some(tx) => tx.send(Ok(Event::new(EventKind::Other))).is_ok(), 30 | None => false, 31 | } 32 | } 33 | 34 | pub fn summary(&self) -> HandlerSummary { 35 | HandlerSummary { 36 | is_alive: self.is_alive(), 37 | config_path: self.handler_config_path.clone(), 38 | is_auto_startup: self.is_auto_startup, 39 | description: self.description.to_owned(), 40 | } 41 | } 42 | 43 | pub fn stop_handler_thread(&self) -> Result { 44 | match self 45 | .watcher_tx 46 | .clone() 47 | .unwrap() 48 | .send(Err(notify::Error::new(NotifyErrorKind::WatchNotFound))) 49 | { 50 | Ok(_) => Ok(String::from("Handler stopped")), 51 | Err(error) => Err(format!("Failed to stop handler\nError: {:?}", error)), 52 | } 53 | } 54 | 55 | pub fn modify(&mut self, request: &ModifyHandlerRequest) { 56 | if let Some(is_auto_startup) = request.is_auto_startup { 57 | self.is_auto_startup = is_auto_startup; 58 | } 59 | if let Some(ref description) = request.modify_description { 60 | self.description = description.to_string(); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/client/subcommand/stop_subcommand.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use clap::{App, Arg, ArgMatches}; 4 | use futures::executor::block_on; 5 | 6 | use super::subcommand_utils::{ 7 | construct_directory_or_all_args, construct_simple_output_arg, 8 | get_path_from_matches_or_current_path, print_handler_states, SubCommandUtil, 9 | }; 10 | use folden::shared_utils::construct_port_arg; 11 | use generated_types::{handler_service_client::HandlerServiceClient, StopHandlerRequest}; 12 | 13 | #[derive(Clone)] 14 | pub struct StopSubCommand; 15 | 16 | impl SubCommandUtil for StopSubCommand { 17 | fn name(&self) -> &str { 18 | "stop" 19 | } 20 | 21 | fn alias(&self) -> &str { 22 | "" 23 | } 24 | 25 | fn requires_connection(&self) -> bool { 26 | true 27 | } 28 | 29 | fn construct_subcommand(&self) -> App { 30 | self.create_instance() 31 | .about("Stop handler on directory") 32 | .arg( 33 | Arg::with_name("remove") 34 | .long("remove") 35 | .visible_alias("rm") 36 | .required(false) 37 | .takes_value(false) 38 | .help("Deregister handler from directory"), 39 | ) 40 | .arg(construct_port_arg()) 41 | .arg(construct_simple_output_arg()) 42 | .args(construct_directory_or_all_args().as_slice()) 43 | } 44 | 45 | fn subcommand_connection_runtime( 46 | &self, 47 | sub_matches: &ArgMatches, 48 | mut client: HandlerServiceClient, 49 | ) { 50 | let is_handler_to_be_removed = sub_matches.is_present("remove"); 51 | let mut path = PathBuf::new(); 52 | if !sub_matches.is_present("all") { 53 | path = get_path_from_matches_or_current_path(sub_matches, "directory").unwrap(); 54 | } 55 | let response = client.stop_handler(StopHandlerRequest { 56 | directory_path: String::from(path.as_os_str().to_str().unwrap()), 57 | remove: is_handler_to_be_removed, 58 | }); 59 | match block_on(response) { 60 | Ok(response) => print_handler_states(response.into_inner(), sub_matches), 61 | Err(e) => println!("{}", e.message()), 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /pipelines/src/pipeline_execution_context.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | path::{Path, PathBuf}, 3 | sync::Arc, 4 | }; 5 | 6 | use crate::{pipeline_config::PipelineConfig, pipeline_context_input::PipelineContextInput}; 7 | use generated_types::TraceHandlerResponse; 8 | 9 | type OutputTraceSender = 10 | Arc>>; 11 | 12 | pub struct PipelineExecutionContext { 13 | pub config: PipelineConfig, 14 | pub event_file_path: PathBuf, 15 | pub action_file_path: Option, 16 | pub trace_tx: OutputTraceSender, 17 | pub action_name: Option, 18 | } 19 | 20 | impl<'a> PipelineExecutionContext { 21 | pub fn new(event_file_path: T, config: PipelineConfig, trace_tx: OutputTraceSender) -> Self 22 | where 23 | T: AsRef, 24 | { 25 | Self { 26 | config, 27 | event_file_path: event_file_path.as_ref().to_path_buf(), 28 | action_file_path: None, 29 | trace_tx, 30 | action_name: None, 31 | } 32 | } 33 | 34 | pub fn get_input(&self, input: PipelineContextInput) -> Option { 35 | match input { 36 | PipelineContextInput::EventFilePath => Some(self.event_file_path.clone()), 37 | PipelineContextInput::ActionFilePath => self.action_file_path.clone(), 38 | } 39 | } 40 | 41 | pub fn log(&self, msg: T) 42 | where 43 | T: AsRef, 44 | { 45 | tracing::info!("{}", msg.as_ref()); 46 | self.send_trace_message(msg); 47 | } 48 | 49 | pub fn handle_error(&self, msg: T) -> bool 50 | where 51 | T: AsRef, 52 | { 53 | tracing::error!("{}", msg.as_ref()); 54 | self.send_trace_message(msg.as_ref()); 55 | if self.config.panic_handler_on_error { 56 | panic!("{}", msg.as_ref()); 57 | } 58 | false 59 | } 60 | 61 | fn send_trace_message(&self, msg: T) 62 | where 63 | T: AsRef, 64 | { 65 | let _ = self.trace_tx.send(Ok(TraceHandlerResponse { 66 | directory_path: self 67 | .event_file_path 68 | .parent() 69 | .unwrap() 70 | .to_str() 71 | .unwrap() 72 | .to_string(), 73 | action: self.action_name.to_owned(), 74 | message: msg.as_ref().to_string(), 75 | })); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /pipelines/src/actions/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | borrow::Cow, 3 | path::{Path, PathBuf}, 4 | }; 5 | 6 | use lazy_static::lazy_static; 7 | use regex::Regex; 8 | use serde::{Deserialize, Serialize}; 9 | use strum_macros::{EnumVariantNames, IntoStaticStr}; 10 | 11 | mod move_to_dir; 12 | mod run_cmd; 13 | use self::{move_to_dir::MoveToDir, run_cmd::RunCmd}; 14 | use crate::pipeline_execution_context::PipelineExecutionContext; 15 | 16 | pub trait PipelineAction { 17 | // Execute action. Returns if action deemed successful. 18 | fn run(&self, context: &mut PipelineExecutionContext) -> bool; 19 | 20 | fn format_input(text: &str, input: PathBuf) -> Cow { 21 | lazy_static! { 22 | static ref INPUT_RE: Regex = Regex::new(r"(\$input\$)").unwrap(); 23 | } 24 | INPUT_RE.replace_all(text, input.to_string_lossy()) 25 | } 26 | 27 | fn format_datetime(text: S) -> String 28 | where 29 | S: AsRef, 30 | { 31 | chrono::Local::now().format(text.as_ref()).to_string() 32 | } 33 | } 34 | 35 | pub fn construct_working_dir(input_path: &Path, directory_path: &Path) -> PathBuf { 36 | let mut working_path = PathBuf::from(input_path.parent().unwrap()); 37 | working_path.push(directory_path); // If directory_path is absolute will replace the entire path 38 | working_path 39 | } 40 | 41 | #[derive(Clone, Debug, EnumVariantNames, IntoStaticStr, Serialize, Deserialize)] 42 | #[serde(tag = "type")] 43 | pub enum PipelineActions { 44 | MoveToDir(MoveToDir), 45 | RunCmd(RunCmd), 46 | } 47 | 48 | impl PipelineActions { 49 | pub fn defaults<'a, I>(actions: I) -> Vec 50 | where 51 | I: Iterator, 52 | { 53 | actions 54 | .map(|action_name| match action_name.to_lowercase().as_str() { 55 | "runcmd" => Self::RunCmd(RunCmd::default()), 56 | "movetodir" => Self::MoveToDir(MoveToDir::default()), 57 | _ => panic!("Incompatible action provided"), 58 | }) 59 | .collect() 60 | } 61 | } 62 | 63 | impl PipelineAction for PipelineActions { 64 | fn run(&self, context: &mut PipelineExecutionContext) -> bool { 65 | match self { 66 | PipelineActions::MoveToDir(action) => action.run(context), 67 | PipelineActions::RunCmd(action) => action.run(context), 68 | } 69 | } 70 | } 71 | 72 | impl Default for PipelineActions { 73 | fn default() -> Self { 74 | Self::RunCmd(RunCmd::default()) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/client/subcommand/modify_subcommand.rs: -------------------------------------------------------------------------------- 1 | use clap::{App, Arg, ArgMatches}; 2 | use futures::executor::block_on; 3 | 4 | use super::subcommand_utils::{ 5 | construct_directory_or_all_args, construct_startup_type_arg, 6 | get_path_from_matches_or_current_path, SubCommandUtil, 7 | }; 8 | use folden::shared_utils::construct_port_arg; 9 | use generated_types::{handler_service_client::HandlerServiceClient, ModifyHandlerRequest}; 10 | 11 | #[derive(Clone)] 12 | pub struct ModifySubCommand; 13 | 14 | impl SubCommandUtil for ModifySubCommand { 15 | fn name(&self) -> &str { 16 | "modify" 17 | } 18 | 19 | fn alias(&self) -> &str { 20 | "mod" 21 | } 22 | 23 | fn requires_connection(&self) -> bool { 24 | true 25 | } 26 | 27 | fn construct_subcommand(&self) -> App { 28 | self.create_instance() 29 | .about("Modify directory handler") 30 | .arg( 31 | Arg::with_name("description") 32 | .long("description") 33 | .visible_alias("desc") 34 | .required(false) 35 | .takes_value(true), 36 | ) 37 | .arg(construct_port_arg()) 38 | .arg(construct_startup_type_arg()) 39 | .args(construct_directory_or_all_args().as_slice()) 40 | } 41 | 42 | fn subcommand_connection_runtime( 43 | &self, 44 | sub_matches: &ArgMatches, 45 | mut client: HandlerServiceClient, 46 | ) { 47 | let is_auto_startup = sub_matches 48 | .value_of("startup") 49 | .map(|value| value.to_lowercase() == "auto"); 50 | let modify_description = sub_matches 51 | .value_of("description") 52 | .map(|description| description.to_string()); 53 | let mut directory_path = String::new(); 54 | if !sub_matches.is_present("all") { 55 | let path = get_path_from_matches_or_current_path(sub_matches, "directory").unwrap(); 56 | directory_path = path.into_os_string().into_string().unwrap(); 57 | } else if let Some(path) = sub_matches.value_of_os("directory") { 58 | directory_path = path.to_os_string().into_string().unwrap(); 59 | } 60 | let response = client.modify_handler(ModifyHandlerRequest { 61 | directory_path, 62 | is_auto_startup, 63 | modify_description, 64 | }); 65 | let response = block_on(response); 66 | match response { 67 | Ok(_) => {} 68 | Err(e) => println!("{}", e), 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/client/subcommand/status_subcommand.rs: -------------------------------------------------------------------------------- 1 | use clap::{App, ArgMatches}; 2 | use futures::executor::block_on; 3 | 4 | use super::subcommand_utils::{ 5 | construct_directory_or_all_args, construct_simple_output_arg, 6 | get_path_from_matches_or_current_path, print_handler_summaries, SubCommandUtil, 7 | }; 8 | use folden::shared_utils::construct_port_arg; 9 | use generated_types::{handler_service_client::HandlerServiceClient, GetDirectoryStatusRequest}; 10 | 11 | #[derive(Clone)] 12 | pub struct StatusSubCommand; 13 | 14 | impl SubCommandUtil for StatusSubCommand { 15 | fn name(&self) -> &str { 16 | "status" 17 | } 18 | 19 | fn alias(&self) -> &str { 20 | "stat" 21 | } 22 | 23 | fn requires_connection(&self) -> bool { 24 | true 25 | } 26 | 27 | fn construct_subcommand(&self) -> App { 28 | self.create_instance() 29 | .about("Status of a registered handler given a directory") 30 | .args(construct_directory_or_all_args().as_slice()) 31 | .arg(construct_port_arg()) 32 | .arg(construct_simple_output_arg()) 33 | } 34 | 35 | fn subcommand_connection_runtime( 36 | &self, 37 | sub_matches: &ArgMatches, 38 | mut client: HandlerServiceClient, 39 | ) { 40 | let mut directory_path = String::new(); 41 | let all_directories = sub_matches.is_present("all"); 42 | if all_directories { 43 | if let Some(path) = sub_matches.value_of_os("directory") { 44 | directory_path = path.to_os_string().into_string().unwrap(); 45 | } 46 | } else { 47 | let path = get_path_from_matches_or_current_path(sub_matches, "directory").unwrap(); 48 | directory_path = path.into_os_string().into_string().unwrap(); 49 | } 50 | let response = client.get_directory_status(GetDirectoryStatusRequest { directory_path }); 51 | match block_on(response) { 52 | Ok(response) => { 53 | let response = response.into_inner(); 54 | if response.summary_map.is_empty() { 55 | println!( 56 | "No handler registered on {}", 57 | if all_directories { 58 | "file system" 59 | } else { 60 | "directory" 61 | } 62 | ); 63 | } else { 64 | print_handler_summaries(response, sub_matches); 65 | } 66 | } 67 | Err(e) => println!("{}", e.message()), 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/client/subcommand/generate_subcommand.rs: -------------------------------------------------------------------------------- 1 | use std::{env, ops::Deref, path::PathBuf}; 2 | 3 | use clap::{App, Arg, ArgMatches}; 4 | use strum::VariantNames; 5 | 6 | use super::subcommand_utils::SubCommandUtil; 7 | use pipelines::{actions::PipelineActions, event::EVENT_TYPES, pipeline_config::PipelineConfig}; 8 | 9 | #[derive(Clone)] 10 | pub struct GenerateSubCommand; 11 | 12 | impl GenerateSubCommand { 13 | fn construct_config_path(file_name: &str, path: Option<&str>) -> PathBuf { 14 | match path { 15 | None => { 16 | let mut path_buf = env::current_dir().unwrap(); 17 | path_buf.push(file_name); 18 | path_buf.set_extension("toml"); 19 | path_buf 20 | } 21 | Some(path) => { 22 | let mut path_buf = PathBuf::from(path); 23 | if path_buf.is_dir() { 24 | path_buf.push(file_name); 25 | path_buf.set_extension("toml"); 26 | } 27 | path_buf 28 | } 29 | } 30 | } 31 | } 32 | 33 | impl SubCommandUtil for GenerateSubCommand { 34 | fn name(&self) -> &str { 35 | "generate" 36 | } 37 | 38 | fn alias(&self) -> &str { 39 | "gen" 40 | } 41 | 42 | fn requires_connection(&self) -> bool { 43 | false 44 | } 45 | 46 | fn construct_subcommand(&self) -> App { 47 | self.create_instance() 48 | .about("Generate default handler pipeline config") 49 | .arg( 50 | Arg::with_name("events") 51 | .long("events") 52 | .required(false) 53 | .multiple(true) 54 | .empty_values(false) 55 | .case_insensitive(true) 56 | .possible_values(&EVENT_TYPES), 57 | ) 58 | .arg( 59 | Arg::with_name("actions") 60 | .long("actions") 61 | .required(false) 62 | .multiple(true) 63 | .empty_values(false) 64 | .case_insensitive(true) 65 | .possible_values(&PipelineActions::VARIANTS), 66 | ) 67 | .arg( 68 | Arg::with_name("path") 69 | .required(false) 70 | .help("File path. Leave empty to generate with default name."), 71 | ) 72 | } 73 | 74 | fn subcommand_runtime(&self, sub_matches: &ArgMatches) { 75 | let events = sub_matches.values_of("events"); 76 | let actions = sub_matches.values_of("actions"); 77 | let path = GenerateSubCommand::construct_config_path( 78 | "folden_pipeline", 79 | sub_matches.value_of("path"), 80 | ); 81 | let config = PipelineConfig::default_new(events, actions); 82 | config.generate_config(path.deref()).unwrap(); 83 | } 84 | 85 | fn subcommand_connection_runtime( 86 | &self, 87 | sub_matches: &ArgMatches, 88 | _client: generated_types::handler_service_client::HandlerServiceClient< 89 | tonic::transport::Channel, 90 | >, 91 | ) { 92 | self.subcommand_runtime(sub_matches); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /pipelines/src/actions/run_cmd.rs: -------------------------------------------------------------------------------- 1 | use std::process::{Child, Command, Stdio}; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use super::PipelineAction; 6 | use crate::{ 7 | pipeline_context_input::PipelineContextInput, 8 | pipeline_execution_context::PipelineExecutionContext, 9 | }; 10 | 11 | #[derive(Clone, Debug, Serialize, Deserialize)] 12 | pub struct RunCmd { 13 | pub input: PipelineContextInput, 14 | pub command: String, 15 | pub input_formatting: bool, 16 | pub datetime_formatting: bool, 17 | } 18 | 19 | impl RunCmd { 20 | fn format_command(&self, context: &mut PipelineExecutionContext) -> String { 21 | let mut formatted_command = self.command.to_owned(); 22 | if self.input_formatting { 23 | if let Some(input_path) = context.get_input(self.input) { 24 | formatted_command = Self::format_input(&self.command, input_path).to_string(); 25 | } 26 | } 27 | if self.datetime_formatting { 28 | formatted_command = Self::format_datetime(formatted_command); 29 | }; 30 | formatted_command 31 | } 32 | } 33 | 34 | impl PipelineAction for RunCmd { 35 | fn run(&self, context: &mut PipelineExecutionContext) -> bool { 36 | let formatted_command = self.format_command(context); 37 | match spawn_command(&formatted_command, context) { 38 | Ok(process) => { 39 | let output = process.wait_with_output(); 40 | return match output { 41 | Ok(out) => { 42 | if out.stdout.is_empty() { 43 | let stderr = String::from_utf8(out.stderr).unwrap(); 44 | context.handle_error(format!("Stderr - {:?}", stderr)) 45 | } else { 46 | let stdout = String::from_utf8(out.stdout).unwrap(); 47 | context.log(format!("Stdout - {:?}", stdout)); 48 | true 49 | } 50 | } 51 | Err(e) => context.handle_error(format!("Error - {:?}", e)), 52 | }; 53 | } 54 | Err(e) => context.handle_error(format!( 55 | "Could not spawn command.\nCommand: {:?}\nError: {:?}", 56 | formatted_command, e 57 | )), 58 | } 59 | } 60 | } 61 | 62 | impl Default for RunCmd { 63 | fn default() -> Self { 64 | Self { 65 | input: PipelineContextInput::EventFilePath, 66 | command: String::from("echo $input$"), 67 | input_formatting: true, 68 | datetime_formatting: true, 69 | } 70 | } 71 | } 72 | 73 | fn spawn_command(input: &S, context: &mut PipelineExecutionContext) -> std::io::Result 74 | where 75 | S: AsRef, 76 | { 77 | let parent_dir_path = context.event_file_path.parent().unwrap(); 78 | if cfg!(windows) { 79 | Command::new("cmd.exe") 80 | .arg(format!("/C {}", input.as_ref())) 81 | .current_dir(parent_dir_path) 82 | .stdout(Stdio::piped()) 83 | .stderr(Stdio::piped()) 84 | .spawn() 85 | } else { 86 | Command::new(input.as_ref()) 87 | .current_dir(parent_dir_path) 88 | .stdout(Stdio::piped()) 89 | .stderr(Stdio::piped()) 90 | .spawn() 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/client/subcommand/register_subcommand.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use clap::Error as CliError; 4 | use clap::{App, Arg, ArgMatches, ErrorKind}; 5 | use futures::executor::block_on; 6 | 7 | use super::subcommand_utils::{ 8 | construct_startup_type_arg, get_path_from_matches_or_current_path, 9 | is_existing_directory_validator, SubCommandUtil, 10 | }; 11 | use folden::shared_utils::construct_port_arg; 12 | use generated_types::{handler_service_client::HandlerServiceClient, RegisterToDirectoryRequest}; 13 | 14 | #[derive(Clone)] 15 | pub struct RegisterSubCommand; 16 | 17 | impl SubCommandUtil for RegisterSubCommand { 18 | fn name(&self) -> &str { 19 | "register" 20 | } 21 | 22 | fn alias(&self) -> &str { 23 | "reg" 24 | } 25 | 26 | fn requires_connection(&self) -> bool { 27 | true 28 | } 29 | 30 | fn construct_subcommand(&self) -> App { 31 | self.create_instance() 32 | .about("Register handler pipeline to directory") 33 | .arg( 34 | Arg::with_name("handler_config") 35 | .value_name("FILE") 36 | .takes_value(true) 37 | .required(true) 38 | .help("Handler pipeline configuration file"), 39 | ) 40 | .arg( 41 | Arg::with_name("directory") 42 | .required(false) 43 | .empty_values(false) 44 | .takes_value(true) 45 | .validator_os(is_existing_directory_validator) 46 | .help("Directory to register to. Leave empty to apply on current"), 47 | ) 48 | .arg( 49 | Arg::with_name("start") 50 | .long("start") 51 | .required(false) 52 | .takes_value(false) 53 | .help("Start handler on register"), 54 | ) 55 | .arg(construct_port_arg()) 56 | .arg(construct_startup_type_arg().default_value("manual")) 57 | } 58 | 59 | fn subcommand_connection_runtime( 60 | &self, 61 | sub_matches: &ArgMatches, 62 | mut client: HandlerServiceClient, 63 | ) { 64 | let handler_config_match = sub_matches.value_of("handler_config").unwrap(); 65 | let handler_config_path = Path::new(handler_config_match); 66 | let is_start_on_register = sub_matches.is_present("start"); 67 | let is_auto_startup = match sub_matches.value_of("startup") { 68 | Some(value) => value.to_lowercase() == "auto", 69 | None => false, 70 | }; 71 | match handler_config_path.canonicalize() { 72 | Ok(handler_config_abs_path) => { 73 | let path = get_path_from_matches_or_current_path(sub_matches, "directory").unwrap(); 74 | let response = client.register_to_directory(RegisterToDirectoryRequest { 75 | directory_path: String::from(path.as_os_str().to_str().unwrap()), 76 | handler_config_path: handler_config_abs_path.to_str().unwrap().to_string(), 77 | is_start_on_register, 78 | is_auto_startup, 79 | }); 80 | match block_on(response) { 81 | Ok(response) => println!("{}", response.into_inner().message), 82 | Err(e) => println!("{}", e.message()), 83 | } 84 | } 85 | Err(_) => { 86 | CliError::with_description("Config file doesn't exist", ErrorKind::InvalidValue) 87 | .exit(); 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/client/subcommand/trace_subcommand.rs: -------------------------------------------------------------------------------- 1 | use clap::{App, ArgMatches}; 2 | use futures::executor::block_on; 3 | 4 | use super::subcommand_utils::{ 5 | construct_directory_or_all_args, get_path_from_matches_or_current_path, SubCommandUtil, 6 | }; 7 | use folden::shared_utils::construct_port_arg; 8 | use generated_types::{ 9 | handler_service_client::HandlerServiceClient, GetDirectoryStatusRequest, TraceHandlerRequest, 10 | }; 11 | 12 | #[derive(Clone)] 13 | pub struct TraceSubCommand; 14 | 15 | impl SubCommandUtil for TraceSubCommand { 16 | fn name(&self) -> &str { 17 | "trace" 18 | } 19 | 20 | fn alias(&self) -> &str { 21 | "" 22 | } 23 | 24 | fn requires_connection(&self) -> bool { 25 | true 26 | } 27 | 28 | fn construct_subcommand(&self) -> App { 29 | self.create_instance() 30 | .about("Trace directory handler output") 31 | .arg(construct_port_arg()) 32 | .args(construct_directory_or_all_args().as_slice()) 33 | } 34 | 35 | fn subcommand_connection_runtime( 36 | &self, 37 | sub_matches: &ArgMatches, 38 | mut client: HandlerServiceClient, 39 | ) { 40 | let mut directory_path = String::new(); 41 | let trace_all_directories = sub_matches.is_present("all"); 42 | if !trace_all_directories { 43 | let path = get_path_from_matches_or_current_path(sub_matches, "directory").unwrap(); 44 | directory_path = path.into_os_string().into_string().unwrap(); 45 | } 46 | let response = client.trace_handler(TraceHandlerRequest { 47 | directory_path: directory_path.clone(), 48 | }); 49 | let response = block_on(response); 50 | match response { 51 | Ok(response) => { 52 | let mut stream = response.into_inner(); 53 | while let Ok(response) = block_on(stream.message()) { 54 | let response = response.unwrap(); 55 | if trace_all_directories { 56 | let trace_ended = response.action.is_none(); 57 | print_response(response, true); 58 | if trace_ended && !is_any_handler_alive(&mut client) { 59 | break; 60 | } 61 | } else if response.directory_path == directory_path { 62 | let exit_trace = response.action.is_none(); 63 | print_response(response, false); 64 | if exit_trace { 65 | break; 66 | } 67 | } 68 | } 69 | } 70 | Err(e) => println!("{}", e.message()), 71 | } 72 | } 73 | } 74 | 75 | fn print_response(response: generated_types::TraceHandlerResponse, print_directory: bool) { 76 | if print_directory { 77 | println!( 78 | " 79 | Directory - {} 80 | Action - {} 81 | Message - {} 82 | ", 83 | response.directory_path, 84 | response.action.unwrap_or_else(|| "None".to_string()), 85 | response.message 86 | ); 87 | } else { 88 | println!( 89 | " 90 | Action - {} 91 | Message - {} 92 | ", 93 | response.action.unwrap_or_else(|| "None".to_string()), 94 | response.message 95 | ); 96 | } 97 | } 98 | 99 | fn is_any_handler_alive(client: &mut HandlerServiceClient) -> bool { 100 | let response = client.get_directory_status(GetDirectoryStatusRequest { 101 | directory_path: String::new(), 102 | }); 103 | if let Ok(response) = block_on(response) { 104 | let response = response.into_inner(); 105 | return response 106 | .summary_map 107 | .iter() 108 | .any(|(_dir, handler)| handler.is_alive); 109 | } 110 | false 111 | } 112 | -------------------------------------------------------------------------------- /pipelines/src/pipeline_handler.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::path::Path; 3 | use std::sync::Arc; 4 | 5 | use crossbeam::channel::Receiver; 6 | use notify::{RecommendedWatcher, RecursiveMode, Watcher}; 7 | use regex::Regex; 8 | 9 | use crate::actions::PipelineAction; 10 | use crate::pipeline_config::PipelineConfig; 11 | use crate::pipeline_execution_context::PipelineExecutionContext; 12 | use generated_types::TraceHandlerResponse; 13 | 14 | type OutputTraceSender = 15 | Arc>>; 16 | 17 | pub struct PipelineHandler { 18 | pub config: PipelineConfig, 19 | pub naming_regex: Option, 20 | pub trace_tx: OutputTraceSender, 21 | } 22 | 23 | impl PipelineHandler { 24 | pub fn new(config: PipelineConfig, trace_tx: OutputTraceSender) -> Self { 25 | let mut naming_regex: Option = None; 26 | if let Some(naming_regex_match) = config.event.naming_regex_match.to_owned() { 27 | naming_regex = Some(Regex::new(&naming_regex_match).unwrap()); 28 | } 29 | Self { 30 | config, 31 | naming_regex, 32 | trace_tx, 33 | } 34 | } 35 | 36 | fn handle(&self, file_path: &Path) { 37 | if let Some(naming_regex) = &self.naming_regex { 38 | if !naming_regex.is_match(file_path.to_str().unwrap()) { 39 | return; 40 | } 41 | } 42 | self.execute_pipeline(file_path); 43 | } 44 | 45 | fn execute_pipeline(&self, file_path: &Path) { 46 | let mut context = 47 | PipelineExecutionContext::new(file_path, self.config.clone(), self.trace_tx.clone()); 48 | for action in &self.config.actions { 49 | let action_name: &'static str = action.into(); 50 | context.action_name = Some(action_name.into()); 51 | context.log("Starting action"); 52 | let action_succeeded = action.run(&mut context); 53 | if !action_succeeded { 54 | break; 55 | } 56 | } 57 | } 58 | 59 | fn apply_on_existing_files(&self, path: &Path) { 60 | for entry in fs::read_dir(path).unwrap() { 61 | let entry = entry.unwrap(); 62 | let metadata = entry.metadata().unwrap(); 63 | if metadata.is_file() { 64 | self.handle(&entry.path()); 65 | } 66 | } 67 | } 68 | 69 | fn on_watch(&self, watcher_rx: Receiver>) { 70 | for result in watcher_rx { 71 | match result { 72 | Ok(event) => { 73 | if self.config.event.is_handled_event(&event.kind) { 74 | tracing::debug!("Event to handle - {:?}", &event.kind); 75 | let event_file_path = event.paths.first().unwrap(); 76 | self.handle(event_file_path); 77 | } 78 | } 79 | Err(error) => { 80 | tracing::warn!("Watcher error - {:?}", error); 81 | if let notify::ErrorKind::WatchNotFound = error.kind { 82 | break; 83 | } 84 | } 85 | } 86 | } 87 | } 88 | 89 | pub fn watch( 90 | &mut self, 91 | path: &Path, 92 | mut watcher: RecommendedWatcher, 93 | events_rx: Receiver>, 94 | ) { 95 | let recursive_mode = if self.config.watch_recursive { 96 | RecursiveMode::Recursive 97 | } else { 98 | RecursiveMode::NonRecursive 99 | }; 100 | watcher.watch(&*path, recursive_mode).unwrap(); 101 | if self.config.apply_on_startup_on_existing_files { 102 | self.apply_on_existing_files(path); 103 | tracing::info!("Ended startup phase"); 104 | } 105 | self.on_watch(events_rx); 106 | tracing::info!("Ending watch"); 107 | let _ = self.trace_tx.send(Ok(TraceHandlerResponse { 108 | directory_path: path.to_str().unwrap().to_string(), 109 | action: None, 110 | message: "Handler runtime ended".to_string(), 111 | })); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/foldend/startup/windows.rs: -------------------------------------------------------------------------------- 1 | use std::{ffi::OsString, time::Duration}; 2 | 3 | use futures::executor; 4 | use tokio::runtime::Runtime; 5 | use tokio::sync::broadcast; 6 | pub use windows_service::Error; 7 | use windows_service::{ 8 | define_windows_service, 9 | service::{ 10 | ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus, 11 | ServiceType, 12 | }, 13 | service_control_handler::{self, ServiceControlHandlerResult}, 14 | service_dispatcher, 15 | }; 16 | 17 | const SERVICE_NAME: &str = "Folden Service"; 18 | 19 | define_windows_service!(ffi_service_main, service_main); 20 | 21 | fn service_main(arguments: Vec) { 22 | if let Err(_e) = run_service(arguments) { 23 | // Handle errors in some way. 24 | } 25 | } 26 | 27 | pub fn run() -> Result<(), windows_service::Error> { 28 | // Register generated `ffi_service_main` with the system and start the service, blocking 29 | // this thread until the service is stopped. 30 | service_dispatcher::start(SERVICE_NAME, ffi_service_main) 31 | } 32 | 33 | pub async fn sync_main( 34 | shutdown_rx: Option>, 35 | ) -> Result<(), Box> { 36 | let rt = Runtime::new()?; 37 | match shutdown_rx { 38 | Some(mut rx) => { 39 | // Exit runtime at service execution termination or if recieved a termination message 40 | rt.block_on(async { 41 | tokio::select! { 42 | result = super::main_service_runtime() => { 43 | match result { 44 | Ok(res) => Ok(res), 45 | Err(e) => Err(e), 46 | } 47 | }, 48 | _ = rx.recv() => Ok(()) 49 | } 50 | }) 51 | } 52 | None => { 53 | // Exit runtime at service execution termination 54 | rt.block_on(async { super::main_service_runtime().await }) 55 | } 56 | } 57 | } 58 | 59 | fn run_service(_arguments: Vec) -> Result<(), windows_service::Error> { 60 | let (shutdown_tx, shutdown_rx) = broadcast::channel(1); 61 | let event_handler = move |control_event| -> ServiceControlHandlerResult { 62 | match control_event { 63 | ServiceControl::Stop => { 64 | // Handle stop event and return control back to the system. 65 | shutdown_tx.send(1).unwrap(); 66 | ServiceControlHandlerResult::NoError 67 | } 68 | // All services must accept Interrogate even if it's a no-op. 69 | ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, 70 | _ => ServiceControlHandlerResult::NotImplemented, 71 | } 72 | }; 73 | 74 | // Register system service event handler 75 | let status_handle = service_control_handler::register(SERVICE_NAME, event_handler)?; 76 | 77 | // Tell the system that the service is running now 78 | status_handle.set_service_status(ServiceStatus { 79 | // Should match the one from system service registry 80 | service_type: ServiceType::OWN_PROCESS, 81 | current_state: ServiceState::Running, 82 | // Accept stop events when running 83 | controls_accepted: ServiceControlAccept::STOP, 84 | // Used to report an error when starting or stopping only, otherwise must be zero 85 | exit_code: ServiceExitCode::Win32(0), 86 | // Only used for pending states, otherwise must be zero 87 | checkpoint: 0, 88 | // Only used for pending states, otherwise must be zero 89 | wait_hint: Duration::default(), 90 | process_id: None, 91 | })?; 92 | 93 | let sync_main_result = executor::block_on(sync_main(Some(shutdown_rx))); 94 | let exit_code = if sync_main_result.is_ok() { 0 } else { 1 }; 95 | 96 | // Tell the system that service has stopped 97 | status_handle.set_service_status(ServiceStatus { 98 | service_type: ServiceType::OWN_PROCESS, 99 | current_state: ServiceState::Stopped, 100 | controls_accepted: ServiceControlAccept::empty(), 101 | exit_code: ServiceExitCode::Win32(exit_code), 102 | checkpoint: 0, 103 | wait_hint: Duration::default(), 104 | process_id: None, 105 | })?; 106 | 107 | Ok(()) 108 | } 109 | -------------------------------------------------------------------------------- /pipelines/src/actions/move_to_dir.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | ffi::OsStr, 3 | fs, 4 | io::ErrorKind, 5 | path::{Path, PathBuf}, 6 | }; 7 | 8 | use serde::{Deserialize, Serialize}; 9 | 10 | use super::{construct_working_dir, PipelineAction}; 11 | use crate::{ 12 | pipeline_context_input::PipelineContextInput, 13 | pipeline_execution_context::PipelineExecutionContext, 14 | }; 15 | 16 | #[derive(Clone, Debug, Serialize, Deserialize)] 17 | pub struct MoveToDir { 18 | pub input: PipelineContextInput, 19 | pub directory_path: PathBuf, 20 | pub requires_directory_exists: bool, 21 | pub replace_older_files: bool, 22 | pub keep_input_file_intact: bool, 23 | pub datetime_formatting: bool, 24 | } 25 | 26 | impl MoveToDir { 27 | fn ensure_dir_exists( 28 | &self, 29 | context: &mut PipelineExecutionContext, 30 | working_dir_path: &Path, 31 | ) -> bool { 32 | if !working_dir_path.is_dir() { 33 | if self.requires_directory_exists { 34 | return context.handle_error("Directory required to exist"); 35 | } else { 36 | fs::create_dir_all(&working_dir_path).unwrap(); 37 | return true; 38 | } 39 | } 40 | true 41 | } 42 | 43 | fn apply( 44 | &self, 45 | context: &mut PipelineExecutionContext, 46 | working_dir_path: &Path, 47 | input_path: &Path, 48 | input_file_name: &OsStr, 49 | ) -> bool { 50 | if !self.ensure_dir_exists(context, working_dir_path) { 51 | return false; 52 | } 53 | let mut new_file_path = PathBuf::from(working_dir_path); 54 | new_file_path.push(input_file_name); 55 | if new_file_path.is_file() && !self.replace_older_files { 56 | context.handle_error("Can't replace older file") 57 | } else { 58 | match fs::copy(&input_path, &new_file_path) { 59 | Ok(_) => { 60 | context.log("Copied file"); 61 | if self.keep_input_file_intact { 62 | context.action_file_path = Some(new_file_path); 63 | true 64 | } else { 65 | match fs::remove_file(input_path) { 66 | Ok(_) => { 67 | context.log("Deleted original file"); 68 | context.action_file_path = Some(new_file_path); 69 | true 70 | } 71 | Err(err) => context.handle_error(format!("{}", err)), 72 | } 73 | } 74 | } 75 | Err(err) => context.handle_error(format!("{:?}", err)), 76 | } 77 | } 78 | } 79 | } 80 | 81 | impl Default for MoveToDir { 82 | fn default() -> Self { 83 | Self { 84 | input: PipelineContextInput::EventFilePath, 85 | directory_path: PathBuf::from("output_dir_path"), 86 | requires_directory_exists: false, 87 | replace_older_files: true, 88 | keep_input_file_intact: false, 89 | datetime_formatting: true, 90 | } 91 | } 92 | } 93 | 94 | impl PipelineAction for MoveToDir { 95 | fn run(&self, context: &mut PipelineExecutionContext) -> bool { 96 | match context.get_input(self.input) { 97 | Some(input_path) => match input_path.file_name() { 98 | Some(input_file_name) => { 99 | let output_directory_path = if self.datetime_formatting { 100 | PathBuf::from(Self::format_datetime( 101 | &self.directory_path.to_string_lossy(), 102 | )) 103 | } else { 104 | self.directory_path.to_path_buf() 105 | }; 106 | let working_dir_path = 107 | construct_working_dir(&input_path, &output_directory_path); 108 | match working_dir_path.canonicalize() { 109 | Ok(working_dir_path) => { 110 | self.apply(context, &working_dir_path, &input_path, input_file_name) 111 | } 112 | Err(err) => match err.kind() { 113 | ErrorKind::NotFound => { 114 | if !self.ensure_dir_exists(context, &working_dir_path) { 115 | return false; 116 | } 117 | self.apply( 118 | context, 119 | &working_dir_path.canonicalize().unwrap(), 120 | &input_path, 121 | input_file_name, 122 | ) 123 | } 124 | _ => context.handle_error(format!("{:?}", err)), 125 | }, 126 | } 127 | } 128 | None => context.handle_error("Path can't be parsed as file"), 129 | }, 130 | None => context.handle_error("Input doesn't contain value"), 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/foldend/mapping.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | convert::TryFrom, 4 | fs, 5 | path::{Path, PathBuf}, 6 | result::Result, 7 | sync::Arc, 8 | thread, 9 | }; 10 | 11 | use notify::{RecommendedWatcher, Watcher}; 12 | use serde::{Deserialize, Serialize}; 13 | use tokio::sync::broadcast; 14 | 15 | use crate::{config::Config, handler_mapping::HandlerMapping}; 16 | use generated_types::HandlerStateResponse; 17 | use pipelines::{pipeline_config::PipelineConfig, pipeline_handler::PipelineHandler}; 18 | 19 | // Mapping data used to handle known directories to handle 20 | // If a handler thread has ceased isn't known at realtime rather will be verified via channel whenever needed to check given a client request 21 | 22 | #[derive(Clone, Debug, Serialize, Deserialize)] 23 | pub struct Mapping { 24 | pub directory_mapping: HashMap, // Hash map key binds to directory path 25 | } 26 | 27 | impl Mapping { 28 | pub fn save(&self, mapping_state_path: &Path) -> Result<(), std::io::Error> { 29 | let mapping_data: Vec = self.into(); 30 | fs::write(mapping_state_path, mapping_data) 31 | } 32 | 33 | pub fn get_live_handlers(&self) -> impl Iterator { 34 | self.directory_mapping 35 | .iter() 36 | .filter(|(_dir, mapping)| mapping.is_alive()) 37 | } 38 | 39 | pub fn start_handler( 40 | &mut self, 41 | directory_path: &str, 42 | handler_mapping: &mut HandlerMapping, 43 | trace_tx: Arc< 44 | broadcast::Sender>, 45 | >, 46 | ) -> HandlerStateResponse { 47 | if handler_mapping.is_alive() { 48 | HandlerStateResponse { 49 | is_alive: true, 50 | message: String::from("Handler already up"), 51 | } 52 | } else { 53 | match self.spawn_handler_thread(directory_path.to_string(), handler_mapping, trace_tx) { 54 | Ok(_) => HandlerStateResponse { 55 | is_alive: true, 56 | message: String::from("Started handler"), 57 | }, 58 | Err(err) => HandlerStateResponse { 59 | is_alive: false, 60 | message: format!("Failed to start handler.\nError: {}", err), 61 | }, 62 | } 63 | } 64 | } 65 | 66 | pub fn spawn_handler_thread( 67 | &mut self, 68 | directory_path: String, 69 | handler_mapping: &mut HandlerMapping, 70 | trace_tx: Arc< 71 | broadcast::Sender>, 72 | >, 73 | ) -> Result<(), String> { 74 | let path = PathBuf::from(directory_path.clone()); 75 | let config_path = PathBuf::from(&handler_mapping.handler_config_path); 76 | match fs::read(&config_path) { 77 | Ok(data) => { 78 | match PipelineConfig::try_from(data) { 79 | Ok(config) => { 80 | let (events_tx, events_rx) = crossbeam::channel::unbounded(); 81 | let events_thread_tx = events_tx.clone(); 82 | let mut watcher: RecommendedWatcher = Watcher::new_immediate(move |res| events_thread_tx.send(res).unwrap()).unwrap(); 83 | let _ = watcher.configure(notify::Config::PreciseEvents(true)); 84 | thread::spawn(move || { 85 | let mut handler = PipelineHandler::new(config, trace_tx); 86 | handler.watch(&path, watcher, events_rx); 87 | }); 88 | // Insert or update the value of the current handled directory 89 | handler_mapping.watcher_tx = Option::Some(events_tx); 90 | self.directory_mapping.insert(directory_path, handler_mapping.to_owned()); 91 | Ok(()) 92 | } 93 | Err(err) => Err(format!("Pipeline config parsing failure.\nPath: {:?}\nError: {:?}", config_path, err)) 94 | } 95 | } 96 | Err(err) => { 97 | Err(format!("Pipeline file read failure.\nMake sure the file is at the registered path\nPath: {:?}\nError: {:?}", config_path, err)) 98 | } 99 | } 100 | } 101 | 102 | pub async fn stop_handler( 103 | &mut self, 104 | config: &Config, 105 | directory_path: &str, 106 | handler_mapping: &mut HandlerMapping, 107 | remove: bool, 108 | ) -> HandlerStateResponse { 109 | if handler_mapping.is_alive() { 110 | match handler_mapping.stop_handler_thread() { 111 | Ok(mut message) => { 112 | if remove { 113 | self.directory_mapping.remove(directory_path); 114 | message.push_str(" & removed"); 115 | let _result = self.save(&config.mapping_state_path); 116 | } else { 117 | handler_mapping.watcher_tx = None; 118 | } 119 | HandlerStateResponse { 120 | is_alive: false, 121 | message, 122 | } 123 | } 124 | Err(message) => HandlerStateResponse { 125 | is_alive: true, 126 | message, 127 | }, 128 | } 129 | } else { 130 | let mut message = String::from("Handler already stopped"); 131 | if remove { 132 | self.directory_mapping.remove(directory_path); 133 | message.push_str(" & removed"); 134 | let _result = self.save(&config.mapping_state_path); 135 | } else { 136 | handler_mapping.watcher_tx = None; 137 | } 138 | HandlerStateResponse { 139 | is_alive: false, 140 | message, 141 | } 142 | } 143 | } 144 | } 145 | 146 | impl TryFrom> for Mapping { 147 | type Error = &'static str; 148 | 149 | fn try_from(bytes: Vec) -> Result { 150 | match toml::from_slice(&bytes) { 151 | Ok(mapping) => Ok(mapping), 152 | Err(_) => Err("Couldn't deserialize data to mapping"), 153 | } 154 | } 155 | } 156 | 157 | impl From for Vec { 158 | fn from(val: Mapping) -> Self { 159 | toml::to_vec(&val).unwrap() 160 | } 161 | } 162 | 163 | impl From<&Mapping> for Vec { 164 | fn from(val: &Mapping) -> Self { 165 | toml::to_vec(val).unwrap() 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/client/subcommand/subcommand_utils.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | env, 3 | ffi::{OsStr, OsString}, 4 | option::Option, 5 | path::Path, 6 | }; 7 | 8 | use clap::{App, Arg, ArgMatches, SubCommand}; 9 | use cli_table::{self, print_stdout, Cell, CellStruct, Table, TableStruct}; 10 | use dyn_clone::DynClone; 11 | 12 | use generated_types::{ 13 | handler_service_client::HandlerServiceClient, HandlerStatesMapResponse, 14 | HandlerSummaryMapResponse, 15 | }; 16 | 17 | const STARTUP_TYPES: [&str; 2] = ["auto", "manual"]; 18 | 19 | pub trait SubCommandUtil: DynClone { 20 | fn name(&self) -> &str; 21 | 22 | fn alias(&self) -> &str; 23 | 24 | fn requires_connection(&self) -> bool; 25 | 26 | fn construct_subcommand(&self) -> App; 27 | 28 | fn subcommand_runtime(&self, _sub_matches: &ArgMatches) { 29 | panic!("Command execution without connection unsupported") 30 | } 31 | 32 | fn subcommand_connection_runtime( 33 | &self, 34 | sub_matches: &ArgMatches, 35 | client: HandlerServiceClient, 36 | ); 37 | 38 | fn create_instance(&self) -> App { 39 | if self.alias().is_empty() { 40 | SubCommand::with_name(self.name()) 41 | } else { 42 | SubCommand::with_name(self.name()).visible_alias(self.alias()) 43 | } 44 | } 45 | 46 | fn subcommand_matches<'a>(&self, matches: &'a ArgMatches) -> Option<&clap::ArgMatches<'a>> { 47 | matches.subcommand_matches(self.name()) 48 | } 49 | } 50 | 51 | dyn_clone::clone_trait_object!(SubCommandUtil); 52 | 53 | #[derive(Clone)] 54 | pub struct SubCommandCollection(Vec>); 55 | 56 | impl SubCommandCollection { 57 | pub fn new() -> Self { 58 | Self(Vec::new()) 59 | } 60 | 61 | pub fn add(&mut self, elem: Box) { 62 | self.0.push(elem); 63 | } 64 | 65 | pub fn collect_as_apps(&self) -> Vec { 66 | self.0 67 | .as_slice() 68 | .iter() 69 | .map(|item| item.construct_subcommand()) 70 | .collect() 71 | } 72 | } 73 | 74 | impl Default for SubCommandCollection { 75 | fn default() -> Self { 76 | Self::new() 77 | } 78 | } 79 | 80 | impl IntoIterator for SubCommandCollection { 81 | type Item = Box; 82 | type IntoIter = std::vec::IntoIter; 83 | 84 | fn into_iter(self) -> Self::IntoIter { 85 | self.0.into_iter() 86 | } 87 | } 88 | 89 | pub fn connect_client( 90 | dst: D, 91 | ) -> Result, tonic::transport::Error> 92 | where 93 | D: std::convert::TryInto, 94 | D::Error: Into, 95 | { 96 | let client_connect_future = HandlerServiceClient::connect(dst); 97 | futures::executor::block_on(client_connect_future) 98 | } 99 | 100 | pub fn construct_directory_or_all_args<'a, 'b>() -> Vec> { 101 | vec![ 102 | Arg::with_name("directory") 103 | .long("directory") 104 | .visible_alias("dir") 105 | .required(false) 106 | .empty_values(false) 107 | .takes_value(true) 108 | .validator_os(is_existing_directory_validator), 109 | Arg::with_name("all") 110 | .long("all") 111 | .help("Apply on all registered directory handlers") 112 | .required(false) 113 | .takes_value(false) 114 | .conflicts_with("directory"), 115 | ] 116 | } 117 | 118 | pub fn construct_startup_type_arg<'a, 'b>() -> Arg<'a, 'b> { 119 | Arg::with_name("startup") 120 | .long("startup") 121 | .visible_alias("up") 122 | .help("Set if handler starts on service startup") 123 | .required(false) 124 | .takes_value(true) 125 | .case_insensitive(true) 126 | .possible_values(&STARTUP_TYPES) 127 | } 128 | 129 | pub fn construct_simple_output_arg<'a, 'b>() -> Arg<'a, 'b> { 130 | Arg::with_name("simple") 131 | .long("simple") 132 | .visible_alias("smpl") 133 | .help("Output in simplified format") 134 | .takes_value(false) 135 | } 136 | 137 | pub fn get_path_from_matches_or_current_path( 138 | sub_matches: &ArgMatches, 139 | match_name: &str, 140 | ) -> Result { 141 | match sub_matches.value_of(match_name) { 142 | Some(directory_match) => Path::new(directory_match).canonicalize(), 143 | None => env::current_dir().unwrap().canonicalize(), 144 | } 145 | } 146 | 147 | pub fn construct_server_url(sub_matches: &ArgMatches) -> Option { 148 | if let Some(value) = sub_matches.value_of("port") { 149 | return Some(format!("http://localhost:{}/", value)); 150 | } 151 | None 152 | } 153 | 154 | pub fn is_existing_directory_validator(val: &OsStr) -> Result<(), OsString> { 155 | let path = Path::new(val); 156 | if path.is_dir() && path.exists() { 157 | Ok(()) 158 | } else { 159 | Err(OsString::from("Input value isn't a directory")) 160 | } 161 | } 162 | 163 | fn get_handler_states_table(states_map_response: HandlerStatesMapResponse) -> TableStruct { 164 | states_map_response 165 | .states_map 166 | .into_iter() 167 | .map(|(dir, state)| vec![dir.cell(), state.is_alive.cell(), state.message.cell()]) 168 | .collect::>>() 169 | .table() 170 | .title(vec!["Path".cell(), "Alive".cell(), "Message".cell()]) 171 | } 172 | 173 | fn get_handler_summaries_table(summary_map_response: HandlerSummaryMapResponse) -> TableStruct { 174 | summary_map_response 175 | .summary_map 176 | .into_iter() 177 | .map(|(dir, summary)| { 178 | vec![ 179 | dir.cell(), 180 | summary.description.cell(), 181 | summary.is_alive.cell(), 182 | (if summary.is_auto_startup { 183 | "auto" 184 | } else { 185 | "manual" 186 | }) 187 | .cell(), 188 | ] 189 | }) 190 | .collect::>>() 191 | .table() 192 | .title(vec![ 193 | "Path".cell(), 194 | "Description".cell(), 195 | "Alive".cell(), 196 | "Startup".cell(), 197 | ]) 198 | } 199 | 200 | pub fn print_handler_states( 201 | states_map_response: HandlerStatesMapResponse, 202 | sub_matches: &ArgMatches, 203 | ) { 204 | if sub_matches.is_present("simple") { 205 | for (dir, state) in states_map_response.states_map { 206 | println!( 207 | " 208 | {} 209 | Alive: {} 210 | Message: {}", 211 | dir, state.is_alive, state.message 212 | ); 213 | } 214 | } else { 215 | let table = get_handler_states_table(states_map_response); 216 | print_stdout(table).unwrap(); 217 | } 218 | } 219 | 220 | pub fn print_handler_summaries( 221 | summary_map_response: HandlerSummaryMapResponse, 222 | sub_matches: &ArgMatches, 223 | ) { 224 | if sub_matches.is_present("simple") { 225 | for (dir, summary) in summary_map_response.summary_map { 226 | println!( 227 | " 228 | {} 229 | Description: {} 230 | Alive: {} 231 | Startup: {}", 232 | dir, 233 | summary.description, 234 | summary.is_alive, 235 | if summary.is_auto_startup { 236 | "auto" 237 | } else { 238 | "manual" 239 | } 240 | ); 241 | } 242 | } else { 243 | let table = get_handler_summaries_table(summary_map_response); 244 | print_stdout(table).unwrap(); 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /src/foldend/startup/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(windows)] 2 | pub mod windows; 3 | 4 | use std::collections::HashMap; 5 | use std::net::{IpAddr, Ipv4Addr, SocketAddr}; 6 | use std::path::Path; 7 | use std::{convert::TryFrom, sync::Arc}; 8 | use std::{fs, path::PathBuf}; 9 | 10 | use clap::{crate_version, App, AppSettings, Arg, ArgMatches, SubCommand}; 11 | use tokio::sync::{broadcast, RwLock}; 12 | use tonic::transport::Server as TonicServer; 13 | use tonic::Request; 14 | use tracing_subscriber::{fmt, prelude::__tracing_subscriber_SubscriberExt, EnvFilter}; 15 | 16 | use crate::config::Config; 17 | use crate::mapping::Mapping; 18 | use crate::server::Server; 19 | use folden::shared_utils::construct_port_arg; 20 | use generated_types::{ 21 | handler_service_server::{HandlerService, HandlerServiceServer}, 22 | StartHandlerRequest, 23 | }; 24 | 25 | fn construct_app<'a, 'b>() -> App<'a, 'b> { 26 | App::new("Foldend") 27 | .version(crate_version!()) 28 | .about("Folden background manager service") 29 | .setting(AppSettings::SubcommandRequiredElseHelp) 30 | .arg( 31 | Arg::with_name("config") 32 | .short("c") 33 | .long("config") 34 | .required(true) 35 | .empty_values(false) 36 | .takes_value(true) 37 | .help("Startup config file"), 38 | ) 39 | .subcommand( 40 | SubCommand::with_name("run") 41 | .about("Startup server") 42 | .arg(construct_port_arg()) 43 | .arg( 44 | Arg::with_name("mapping") 45 | .short("m") 46 | .long("mapping") 47 | .required(false) 48 | .empty_values(false) 49 | .takes_value(true) 50 | .help("Startup mapping file. Defaults to [foldend_mapping.toml]"), 51 | ) 52 | .arg( 53 | Arg::with_name("limit") 54 | .long("limit") 55 | .empty_values(false) 56 | .takes_value(true) 57 | .help("Concurrently running handler threads limit"), 58 | ) 59 | .arg( 60 | Arg::with_name("log") 61 | .short("l") 62 | .long("log") 63 | .empty_values(false) 64 | .takes_value(true) 65 | .help("Override file path to store logs at. Defaults to [foldend.log]"), 66 | ), 67 | ) 68 | } 69 | 70 | async fn startup_handlers(server: &Server) { 71 | let mapping = server.mapping.read().await; 72 | let handler_requests: Vec = mapping 73 | .directory_mapping 74 | .iter() 75 | .filter_map(|(directory_path, handler_mapping)| { 76 | if handler_mapping.is_auto_startup { 77 | Some(StartHandlerRequest { 78 | directory_path: directory_path.to_string(), 79 | }) 80 | } else { 81 | None 82 | } 83 | }) 84 | .collect(); 85 | drop(mapping); 86 | for request in handler_requests { 87 | match server.start_handler(Request::new(request.clone())).await { 88 | Ok(response) => { 89 | let response = response.into_inner(); 90 | tracing::info!("{}", format!("{:?}", response.states_map)); 91 | } 92 | Err(err) => { 93 | tracing::error!( 94 | "Handler [DOWN] - {:?}\n Error - {:?}", 95 | request.directory_path, 96 | err 97 | ); 98 | } 99 | } 100 | } 101 | } 102 | 103 | #[tracing::instrument] 104 | async fn startup_server( 105 | config: Config, 106 | mapping: Mapping, 107 | ) -> Result<(), Box> { 108 | // Setup tracing 109 | let file_appender = tracing_appender::rolling::daily( 110 | &config.tracing_file_path.parent().unwrap(), 111 | &config.tracing_file_path, 112 | ); 113 | let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender); 114 | let subscriber = tracing_subscriber::registry() 115 | .with(EnvFilter::from_default_env().add_directive(tracing::Level::DEBUG.into())) 116 | .with(fmt::Layer::new().with_writer(std::io::stdout)) 117 | .with(fmt::Layer::new().with_writer(non_blocking)); 118 | tracing::subscriber::set_global_default(subscriber).expect("Unable to set a global collector"); 119 | 120 | let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), config.port); 121 | let (trace_tx, _) = broadcast::channel(10); 122 | let server = Server { 123 | config: Arc::new(config), 124 | mapping: Arc::new(RwLock::new(mapping)), 125 | handlers_trace_tx: Arc::new(trace_tx), 126 | }; 127 | 128 | startup_handlers(&server).await; // Handlers are raised before being able to accept client calls. 129 | 130 | tracing::info!("Server up on port {}", socket.port()); 131 | TonicServer::builder() 132 | .add_service(HandlerServiceServer::new(server)) 133 | .serve(socket) 134 | .await?; 135 | Ok(()) 136 | } 137 | 138 | #[tracing::instrument] 139 | fn get_mapping(config: &Config) -> Mapping { 140 | let mapping = Mapping { 141 | directory_mapping: HashMap::new(), 142 | }; 143 | let mapping_file_path = &config.mapping_state_path; 144 | match fs::read(mapping_file_path) { 145 | Ok(mapping_file_data) => match Mapping::try_from(mapping_file_data) { 146 | Ok(read_mapping) => read_mapping, 147 | Err(_) => { 148 | tracing::error!("Mapping file invalid / empty"); 149 | mapping 150 | } 151 | }, 152 | Err(err) => { 153 | tracing::warn!( 154 | "Mapping file not found. Creating file - {:?}", 155 | mapping_file_path 156 | ); 157 | match err.kind() { 158 | std::io::ErrorKind::NotFound => { 159 | let mapping_file_parent_path = mapping_file_path.parent().unwrap(); 160 | if !mapping_file_parent_path.exists() { 161 | fs::create_dir_all(mapping_file_parent_path).unwrap(); 162 | } 163 | match fs::write(mapping_file_path, b"") { 164 | Ok(_) => mapping, 165 | Err(err) => panic!("{}", err), 166 | } 167 | } 168 | err => panic!("{:?}", err), 169 | } 170 | } 171 | } 172 | } 173 | 174 | fn get_config(file_path: &Path) -> Result> { 175 | match fs::read(&file_path) { 176 | Ok(data) => Ok(Config::try_from(data)?), 177 | Err(e) => Err(e.into()), 178 | } 179 | } 180 | 181 | fn modify_config( 182 | config: &mut Config, 183 | sub_matches: &ArgMatches, 184 | ) -> Result<(), Box> { 185 | if let Some(mapping_file_path) = sub_matches.value_of("mapping") { 186 | config 187 | .mapping_state_path 188 | .clone_from(&PathBuf::from(mapping_file_path)); 189 | } 190 | if let Some(port) = sub_matches.value_of("port") { 191 | config.port = port.parse()?; 192 | } 193 | if let Some(limit) = sub_matches.value_of("limit") { 194 | config.concurrent_threads_limit = limit.parse()?; 195 | } 196 | if let Some(log_file_path) = sub_matches.value_of("log") { 197 | config.tracing_file_path = PathBuf::from(log_file_path); 198 | } 199 | Ok(()) 200 | } 201 | 202 | #[tracing::instrument] 203 | pub async fn main_service_runtime() -> Result<(), Box> { 204 | let app = construct_app(); 205 | let matches = app.get_matches(); 206 | match matches.value_of("config") { 207 | Some(config_str_path) => { 208 | let config_file_path = PathBuf::from(config_str_path); 209 | match get_config(&config_file_path) { 210 | Ok(mut config) => match matches.subcommand() { 211 | ("run", Some(sub_matches)) => { 212 | modify_config(&mut config, sub_matches)?; 213 | config.save(&config_file_path).unwrap(); 214 | let mapping = get_mapping(&config); 215 | startup_server(config, mapping).await?; 216 | } 217 | ("logs", Some(sub_matches)) => { 218 | if sub_matches.value_of("view").is_some() { 219 | unimplemented!("View logs from file") 220 | } else if sub_matches.value_of("clear").is_some() { 221 | unimplemented!("Clear logs file") 222 | } 223 | } 224 | _ => {} 225 | }, 226 | Err(e) => { 227 | match matches.subcommand() { 228 | ("run", Some(sub_matches)) => { 229 | tracing::warn!("Invalid config file:{path:?}\nError:{error}\nCreating default config", path=&config_file_path, error=e); 230 | let mut config = Config::default(); 231 | modify_config(&mut config, sub_matches)?; 232 | config.save(&config_file_path).unwrap(); 233 | let mapping = get_mapping(&config); 234 | startup_server(config, mapping).await?; 235 | } 236 | ("logs", Some(_sub_matches)) => { 237 | tracing::error!( 238 | "Invalid config file:{path:?}\nError:{error}", 239 | path = &config_file_path, 240 | error = e 241 | ); 242 | } 243 | _ => {} 244 | } 245 | } 246 | } 247 | } 248 | None => return Err("Config path not provided".into()), 249 | } 250 | Ok(()) 251 | } 252 | -------------------------------------------------------------------------------- /wix/main.wxs: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 36 | 37 | 46 | 47 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 65 | 66 | 80 | 85 | 86 | 87 | 93 | 94 | 95 | 96 | 102 | 113 | 118 | 124 | 125 | 126 | 127 | 128 | 129 | 138 | 139 | 143 | 144 | 145 | 146 | 147 | 148 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 167 | 168 | 169 | 170 | 171 | 180 | 181 | 182 | 183 | 184 | 185 | 195 | 1 196 | 1 197 | 198 | 199 | 200 | 201 | 206 | 207 | 208 | 209 | 217 | 218 | 219 | 220 | 228 | 229 | 230 | 231 | 232 | 233 | -------------------------------------------------------------------------------- /src/foldend/server/handler_service.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use tonic::{Request, Response}; 4 | 5 | use super::Server; 6 | use super::TraceHandlerStream; 7 | use crate::handler_mapping::HandlerMapping; 8 | use generated_types::{ 9 | handler_service_server::HandlerService, HandlerStateResponse, HandlerStatesMapResponse, 10 | HandlerSummary, HandlerSummaryMapResponse, 11 | }; 12 | 13 | #[tonic::async_trait] 14 | impl HandlerService for Server { 15 | type TraceHandlerStream = TraceHandlerStream; 16 | 17 | #[tracing::instrument] 18 | async fn register_to_directory( 19 | &self, 20 | request: Request, 21 | ) -> Result, tonic::Status> { 22 | tracing::info!("Registering handler to directory"); 23 | let request = request.into_inner(); 24 | let mut mapping = self.mapping.write().await; 25 | let request_directory_path = request.directory_path.as_str(); 26 | if mapping 27 | .directory_mapping 28 | .get(request_directory_path) 29 | .is_some() 30 | { 31 | Ok(Response::new(HandlerStateResponse { 32 | is_alive: true, 33 | message: String::from("Directory already handled by handler"), 34 | })) 35 | } else { 36 | // Check if requested directory is a child / parent of any handled directory 37 | for directory_path in mapping.directory_mapping.keys() { 38 | if request_directory_path.contains(directory_path) { 39 | return Ok(Response::new(HandlerStateResponse { 40 | is_alive: false, 41 | message: format!( 42 | "Couldn't register\nDirectory is a child of handled directory - {}", 43 | directory_path 44 | ), 45 | })); 46 | } else if directory_path.contains(request_directory_path) { 47 | return Ok(Response::new(HandlerStateResponse { 48 | is_alive: false, 49 | message: format!( 50 | "Couldn't register\nDirectory is a parent of requested directory - {}", 51 | directory_path 52 | ), 53 | })); 54 | } 55 | } 56 | let mut handler_mapping = HandlerMapping::new( 57 | request.handler_config_path, 58 | request.is_auto_startup, 59 | String::new(), 60 | ); 61 | if request.is_start_on_register { 62 | if self.is_concurrent_handlers_limit_reached(&mapping) { 63 | mapping 64 | .directory_mapping 65 | .insert(request.directory_path, handler_mapping); 66 | let _result = mapping.save(&self.config.mapping_state_path); 67 | return Ok(Response::new(HandlerStateResponse { 68 | is_alive: false, 69 | message: format!("Registered handler without starting - Reached concurrent live handler limit ({})", self.config.concurrent_threads_limit), 70 | })); 71 | } 72 | let trace_tx = self.handlers_trace_tx.clone(); 73 | match mapping.spawn_handler_thread( 74 | request.directory_path, 75 | &mut handler_mapping, 76 | trace_tx, 77 | ) { 78 | Ok(_) => { 79 | let _result = mapping.save(&self.config.mapping_state_path); 80 | Ok(Response::new(HandlerStateResponse { 81 | is_alive: true, 82 | message: String::from("Registered and started handler"), 83 | })) 84 | } 85 | Err(err) => Ok(Response::new(HandlerStateResponse { 86 | is_alive: false, 87 | message: format!("Failed to register and start handler.\nError: {}", err), 88 | })), 89 | } 90 | } else { 91 | mapping 92 | .directory_mapping 93 | .insert(request.directory_path, handler_mapping); 94 | let _result = mapping.save(&self.config.mapping_state_path); 95 | Ok(Response::new(HandlerStateResponse { 96 | is_alive: false, 97 | message: String::from("Registered handler"), 98 | })) 99 | } 100 | } 101 | } 102 | 103 | #[tracing::instrument] 104 | async fn get_directory_status( 105 | &self, 106 | request: Request, 107 | ) -> Result, tonic::Status> { 108 | tracing::info!("Getting directory status"); 109 | let request = request.into_inner(); 110 | let mapping = &*self.mapping.read().await; 111 | let directory_path = request.directory_path.as_str(); 112 | let mut summary_map: HashMap = HashMap::new(); 113 | 114 | match mapping.directory_mapping.get(directory_path) { 115 | Some(handler_mapping) => { 116 | let state = handler_mapping.summary(); 117 | summary_map.insert(directory_path.to_string(), state); 118 | Ok(Response::new(HandlerSummaryMapResponse { summary_map })) 119 | } 120 | None => { 121 | // If empty - All directories are requested 122 | if directory_path.is_empty() { 123 | for (directory_path, handler_mapping) in mapping.directory_mapping.iter() { 124 | summary_map.insert(directory_path.to_owned(), handler_mapping.summary()); 125 | } 126 | } 127 | Ok(Response::new(HandlerSummaryMapResponse { summary_map })) 128 | } 129 | } 130 | } 131 | 132 | #[tracing::instrument] 133 | async fn start_handler( 134 | &self, 135 | request: Request, 136 | ) -> Result, tonic::Status> { 137 | tracing::info!("Starting handler"); 138 | let request = request.into_inner(); 139 | let mut mapping = self.mapping.write().await; 140 | let directory_path = request.directory_path.as_str(); 141 | let mut states_map: HashMap = HashMap::new(); 142 | 143 | match mapping.clone().directory_mapping.get_mut(directory_path) { 144 | Some(handler_mapping) => { 145 | if !handler_mapping.is_alive() 146 | && self.is_concurrent_handlers_limit_reached(&mapping) 147 | { 148 | return Err(tonic::Status::failed_precondition(format!( 149 | "Aborted start handler - Reached concurrent live handler limit ({})", 150 | self.config.concurrent_threads_limit 151 | ))); 152 | } 153 | let trace_tx = self.handlers_trace_tx.clone(); 154 | let response = mapping.start_handler(directory_path, handler_mapping, trace_tx); 155 | states_map.insert(directory_path.to_owned(), response); 156 | Ok(Response::new(HandlerStatesMapResponse { states_map })) 157 | } 158 | None => { 159 | // If empty - All directories are requested 160 | if request.directory_path.is_empty() { 161 | if mapping.directory_mapping.len() > self.config.concurrent_threads_limit.into() 162 | { 163 | return Err(tonic::Status::failed_precondition( 164 | format!("Aborted start handlers - Would pass concurrent live handler limit ({})\nCurrently live: {}", 165 | self.config.concurrent_threads_limit, mapping.get_live_handlers().count()))); 166 | } 167 | for (directory_path, handler_mapping) in 168 | mapping.clone().directory_mapping.iter_mut() 169 | { 170 | let trace_tx = self.handlers_trace_tx.clone(); 171 | let response = 172 | mapping.start_handler(directory_path, handler_mapping, trace_tx); 173 | states_map.insert(directory_path.to_owned(), response); 174 | } 175 | } else { 176 | states_map.insert( 177 | directory_path.to_owned(), 178 | HandlerStateResponse { 179 | is_alive: false, 180 | message: String::from("Directory unhandled"), 181 | }, 182 | ); 183 | } 184 | Ok(Response::new(HandlerStatesMapResponse { states_map })) 185 | } 186 | } 187 | } 188 | 189 | #[tracing::instrument] 190 | async fn stop_handler( 191 | &self, 192 | request: Request, 193 | ) -> Result, tonic::Status> { 194 | tracing::info!("Stopping handler"); 195 | let request = request.into_inner(); 196 | let mut mapping = self.mapping.write().await; 197 | let directory_path = request.directory_path.as_str(); 198 | let mut states_map: HashMap = HashMap::new(); 199 | 200 | match mapping 201 | .clone() 202 | .directory_mapping 203 | .get_mut(&request.directory_path) 204 | { 205 | Some(handler_mapping) => { 206 | let response = mapping 207 | .stop_handler( 208 | &self.config, 209 | directory_path, 210 | handler_mapping, 211 | request.remove, 212 | ) 213 | .await; 214 | states_map.insert(directory_path.to_owned(), response); 215 | Ok(Response::new(HandlerStatesMapResponse { states_map })) 216 | } 217 | None => { 218 | if request.directory_path.is_empty() { 219 | // If empty - All directories are requested 220 | for (directory_path, handler_mapping) in 221 | mapping.clone().directory_mapping.iter_mut() 222 | { 223 | let response = mapping 224 | .stop_handler( 225 | &self.config, 226 | directory_path, 227 | handler_mapping, 228 | request.remove, 229 | ) 230 | .await; 231 | states_map.insert(directory_path.to_owned(), response); 232 | } 233 | } else { 234 | states_map.insert( 235 | directory_path.to_owned(), 236 | HandlerStateResponse { 237 | is_alive: false, 238 | message: String::from("Directory unhandled"), 239 | }, 240 | ); 241 | } 242 | Ok(Response::new(HandlerStatesMapResponse { states_map })) 243 | } 244 | } 245 | } 246 | 247 | #[tracing::instrument] 248 | async fn modify_handler( 249 | &self, 250 | request: Request, 251 | ) -> Result, tonic::Status> { 252 | tracing::info!("Modifying handler"); 253 | let request = request.into_inner(); 254 | let mut mapping = self.mapping.write().await; 255 | 256 | match mapping.directory_mapping.get_mut(&request.directory_path) { 257 | Some(handler_mapping) => handler_mapping.modify(&request), 258 | None => { 259 | if request.directory_path.is_empty() { 260 | // If empty - All directories are requested 261 | for handler_mapping in mapping.directory_mapping.values_mut() { 262 | handler_mapping.modify(&request); 263 | } 264 | } else { 265 | return Err(tonic::Status::not_found( 266 | "Directory isn't registered to handle", 267 | )); 268 | } 269 | } 270 | } 271 | 272 | match mapping.save(&self.config.mapping_state_path) { 273 | Ok(result) => Ok(Response::new(result)), 274 | Err(e) => Err(tonic::Status::unknown(format!( 275 | "Failed to save modifications to mapping file.\nErr - {:?}", 276 | e 277 | ))), 278 | } 279 | } 280 | 281 | async fn trace_handler( 282 | &self, 283 | request: Request, 284 | ) -> Result, tonic::Status> { 285 | tracing::info!("Tracing directory handler"); 286 | let request = request.into_inner(); 287 | let mapping = self.mapping.read().await; 288 | 289 | if !request.directory_path.is_empty() { 290 | // If empty - All directories are requested 291 | match mapping.directory_mapping.get(&request.directory_path) { 292 | Some(handler_mapping) => { 293 | if !handler_mapping.is_alive() { 294 | return Err(tonic::Status::failed_precondition( 295 | "Handler isn't alive to trace", 296 | )); 297 | } 298 | } 299 | None => { 300 | return Err(tonic::Status::not_found( 301 | "Directory isn't registered to handle", 302 | )) 303 | } 304 | } 305 | } else if mapping.directory_mapping.is_empty() { 306 | return Err(tonic::Status::not_found( 307 | "No handler registered to filesystem to trace", 308 | )); 309 | } else if !is_any_handler_alive(self).await { 310 | return Err(tonic::Status::not_found("No handler is alive to trace")); 311 | } 312 | 313 | let rx_stream = self.convert_trace_channel_reciever_to_stream(); 314 | tracing::debug!( 315 | "Handler trace receivers live: {}", 316 | self.handlers_trace_tx.receiver_count() 317 | ); 318 | return Ok(Response::new(rx_stream)); 319 | } 320 | } 321 | 322 | async fn is_any_handler_alive(server: &Server) -> bool { 323 | let response = 324 | server.get_directory_status(Request::new(generated_types::GetDirectoryStatusRequest { 325 | directory_path: String::new(), 326 | })); 327 | if let Ok(response) = response.await { 328 | let response = response.into_inner(); 329 | return response 330 | .summary_map 331 | .iter() 332 | .any(|(_dir, handler)| handler.is_alive); 333 | } 334 | false 335 | } 336 | --------------------------------------------------------------------------------