Simple tool for automatic file management
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 80filter! { 81 "size"; 82 struct Size { 83 size: u64, 84 #[serde(with = "Ordering")] 85 ordering: std::cmp::Ordering, 86 }; 87 (self, path) -> { 88 with_metadata(path, |metadata| { 89 let len = metadata.len(); 90 91 len.cmp(&self.size) == self.ordering 92 }) 93 .await 94 } 95} 96 97#[derive(Debug, Deserialize)] 98#[serde(tag = "is", rename_all = "snake_case")] 99pub enum FileType { 100 Dir, 101 File, 102 Symlink, 103} 104 105#[typetag::deserialize(name = "file_type")] 106#[async_trait] 107impl Filter for FileType { 108 async fn matches(&self, path: &Path) -> bool { 109 use FileType::*; 110 111 with_metadata(path, |metadata| { 112 let ft = metadata.file_type(); 113 114 match *self { 115 Dir => ft.is_dir(), 116 File => ft.is_file(), 117 Symlink => ft.is_symlink(), 118 } 119 }) 120 .await 121 } 122} 123 124async fn with_metadata<F>(path: &Path, fun: F) -> bool 125where 126 F: FnOnce(std::fs::Metadata) -> bool, 127{ 128 fs::metadata(path).await.map(fun).unwrap_or(false) 129} 130 131filter! { 132 "not"; 133 struct Not { filter: Box<dyn Filter>, }; 134 (self, path) -> { 135 !self.filter.matches(path).await 136 } 137} 138 139filter! { 140 "any"; 141 struct Any { filters: Box<[Box<dyn Filter>]>, }; 142 (self, path) -> { 143 stream::iter(&*self.filters) 144 .any(|f| f.matches(path)) 145 .await 146 } 147} 148 149filter! { 150 "all"; 151 struct All { filters: Box<[Box<dyn Filter>]>, }; 152 (self, path) -> { 153 stream::iter(&*self.filters) 154 .all(|f| f.matches(path)) 155 .await 156 } 157} 158 159#[derive(Debug, Deserialize)] 160#[serde(rename_all = "snake_case")] 161pub enum Magic { 162 Mime(String), 163 Magic { 164 bytes: Box<[u8]>, 165 #[serde(default)] 166 offset: u64, 167 }, 168} 169 170async fn read_first_bytes(n: usize, path: &Path, offset: u64) -> io::Result<Box<[u8]>> { 171 use std::io::SeekFrom; 172 173 let mut file = fs::File::open(path).await?; 174 175 let mut buf = vec![0; n]; 176 177 file.seek(SeekFrom::Start(offset)).await?; 178 file.read_exact(&mut buf).await?; 179 180 Ok(buf.into()) 181} 182 183async fn guess_mime(path: &Path) -> Option<infer::Type> { 184 let mut file = fs::File::open(path).await.ok()?; 185 186 let mut buf = vec![0; 8192]; 187 188 let len = file.read(&mut buf).await.ok()?; 189 190 infer::get(&buf[0..len]) 191} 192 193#[typetag::deserialize(name = "content_type")] 194#[async_trait] 195impl Filter for Magic { 196 async fn matches(&self, file: &Path) -> bool { 197 match *self { 198 Magic::Magic { ref bytes, offset } => read_first_bytes(bytes.len(), file, offset) 199 .await 200 .map(|read| read == *bytes) 201 .unwrap_or(false), 202 Magic::Mime(ref mime_type) => guess_mime(file) 203 .await 204 .map(|typ| typ.mime_type() == mime_type) 205 .unwrap_or(false), 206 } 207 } 208}