Simple tool for automatic file management
1use std::path::Path; 2 3use async_trait::async_trait; 4use serde::Deserialize; 5 6use tokio::fs; 7use tokio::io::{self, AsyncReadExt, AsyncSeekExt}; 8 9use futures::prelude::*; 10 11use crate::pattern::Pattern; 12 13#[typetag::deserialize(tag = "type")] 14#[async_trait] 15pub trait Filter: std::fmt::Debug + Sync { 16 async fn matches(&self, path: &Path) -> bool; 17} 18 19macro_rules! filter { 20 { 21 $name:literal; 22 $(#[$sattr:meta])* 23 struct $sname:ident {$($(#[$attr:meta])* $fname:ident : $ftype:ty,)*}; 24 ($self_:ident, $path:ident) -> $body:block 25 } => { 26 #[derive(Debug, serde::Deserialize)] 27 $(#[$sattr])* 28 pub struct $sname { $($(#[$attr])* $fname: $ftype),* } 29 30 #[typetag::deserialize(name = $name)] 31 #[async_trait] 32 impl Filter for $sname { 33 async fn matches(&$self_, $path: &Path) -> bool { 34 $body 35 } 36 } 37 }; 38 39 { 40 $name:literal; 41 $(#[$sattr:meta])* 42 struct $sname:ident ($($field:ty),*); 43 ($self_:ident, $path:ident) -> $body:block 44 } => { 45 #[derive(Debug, serde::Deserialize)] 46 $(#[$sattr])* 47 pub struct $sname ($($field),*); 48 49 #[typetag::deserialize(name = $name)] 50 #[async_trait] 51 impl Filter for $sname { 52 async fn matches(&$self_, $path: &Path) -> bool { 53 $body 54 } 55 } 56 } 57} 58 59filter! { 60 "name"; 61 struct Name(Pattern); 62 (self, path) -> { 63 self.0.matches(path) 64 } 65} 66 67#[derive(Deserialize)] 68#[serde(remote = "std::cmp::Ordering")] 69#[serde(rename_all = "snake_case")] 70enum Ordering { 71 Less, 72 Equal, 73 Greater, 74} 75 76filter! { 77 "size"; 78 struct Size { 79 size: u64, 80 #[serde(with = "Ordering")] 81 ordering: std::cmp::Ordering, 82 }; 83 (self, path) -> { 84 with_metadata(path, |metadata| { 85 let len = metadata.len(); 86 87 len.cmp(&self.size) == self.ordering 88 }) 89 .await 90 } 91} 92 93#[derive(Debug, Deserialize)] 94#[serde(tag = "is", rename_all = "snake_case")] 95pub enum FileType { 96 Dir, 97 File, 98 Symlink, 99} 100 101#[typetag::deserialize(name = "file_type")] 102#[async_trait] 103impl Filter for FileType { 104 async fn matches(&self, path: &Path) -> bool { 105 use FileType::*; 106 107 with_metadata(path, |metadata| { 108 let ft = metadata.file_type(); 109 110 match *self { 111 Dir => ft.is_dir(), 112 File => ft.is_file(), 113 Symlink => ft.is_symlink(), 114 } 115 }) 116 .await 117 } 118} 119 120async fn with_metadata<F>(path: &Path, fun: F) -> bool 121where 122 F: FnOnce(std::fs::Metadata) -> bool, 123{ 124 fs::metadata(path).await.map(fun).unwrap_or(false) 125} 126 127filter! { 128 "not"; 129 struct Not { filter: Box<dyn Filter>, }; 130 (self, path) -> { 131 !self.filter.matches(path).await 132 } 133} 134 135filter! { 136 "any"; 137 struct Any { filters: Box<[Box<dyn Filter>]>, }; 138 (self, path) -> { 139 stream::iter(&*self.filters) 140 .any(|f| f.matches(path)) 141 .await 142 } 143} 144 145filter! { 146 "all"; 147 struct All { filters: Box<[Box<dyn Filter>]>, }; 148 (self, path) -> { 149 stream::iter(&*self.filters) 150 .all(|f| f.matches(path)) 151 .await 152 } 153} 154 155#[derive(Debug, Deserialize)] 156#[serde(rename_all = "snake_case")] 157pub enum Magic { 158 Mime(String), 159 Magic { 160 bytes: Box<[u8]>, 161 #[serde(default)] 162 offset: u64, 163 }, 164} 165 166async fn read_first_bytes(n: usize, path: &Path, offset: u64) -> io::Result<Box<[u8]>> { 167 use std::io::SeekFrom; 168 169 let mut file = fs::File::open(path).await?; 170 171 let mut buf = vec![0; n]; 172 173 file.seek(SeekFrom::Start(offset)).await?; 174 file.read_exact(&mut buf).await?; 175 176 Ok(buf.into()) 177} 178 179async fn guess_mime(path: &Path) -> Option<infer::Type> { 180 let mut file = fs::File::open(path).await.ok()?; 181 182 let mut buf = vec![0; 8192]; 183 184 let len = file.read(&mut buf).await.ok()?; 185 186 infer::get(&buf[0..len]) 187} 188 189#[typetag::deserialize(name = "content_type")] 190#[async_trait] 191impl Filter for Magic { 192 async fn matches(&self, file: &Path) -> bool { 193 match *self { 194 Magic::Magic { ref bytes, offset } => read_first_bytes(bytes.len(), file, offset) 195 .await 196 .map(|read| read == *bytes) 197 .unwrap_or(false), 198 Magic::Mime(ref mime_type) => guess_mime(file) 199 .await 200 .map(|typ| typ.mime_type() == mime_type) 201 .unwrap_or(false), 202 } 203 } 204}