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