Something went wrong. Try again.
Simple tool for automatic file management
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213// SPDX-FileCopyrightText: 2024 Łukasz Niemier <#@hauleth.dev>//// SPDX-License-Identifier: EUPL-1.2
use std::path::Path;
use async_trait::async_trait;use serde::Deserialize;
use tokio::fs;use tokio::io::{self, AsyncReadExt, AsyncSeekExt};
use futures::prelude::*;
use crate::pattern::Pattern;
#[typetag::deserialize(tag = "type")]#[async_trait]pub trait Filter: std::fmt::Debug + Sync { async fn matches(&self, path: &Path) -> bool;}
macro_rules! filter { { $name:literal; $(#[$sattr:meta])* struct $sname:ident {$($(#[$attr:meta])* $fname:ident : $ftype:ty,)*}; ($self_:ident, $path:ident) -> $body:block } => { #[derive(Debug, serde::Deserialize)] $(#[$sattr])* pub struct $sname { $($(#[$attr])* $fname: $ftype),* }
#[typetag::deserialize(name = $name)] #[async_trait] impl Filter for $sname { async fn matches(&$self_, $path: &Path) -> bool { $body } } };
{ $name:literal; $(#[$sattr:meta])* struct $sname:ident ($($field:ty),*); ($self_:ident, $path:ident) -> $body:block } => { #[derive(Debug, serde::Deserialize)] $(#[$sattr])* pub struct $sname ($($field),*);
#[typetag::deserialize(name = $name)] #[async_trait] impl Filter for $sname { async fn matches(&$self_, $path: &Path) -> bool { $body } } }}
filter! { "name"; struct Name(Pattern); (self, path) -> { self.0.matches(path) }}
#[derive(Deserialize)]#[serde(remote = "std::cmp::Ordering")]#[serde(rename_all = "snake_case")]enum Ordering { Less, Equal, Greater,}
impl Ordering { fn default() -> std::cmp::Ordering { std::cmp::Ordering::Equal }}
filter! { "size"; struct Size { size: u64, #[serde(with = "Ordering", default = "Ordering::default")] ordering: std::cmp::Ordering, }; (self, path) -> { with_metadata(path, |metadata| { let len = metadata.len();
len.cmp(&self.size) == self.ordering }) .await }}
#[derive(Debug, Deserialize)]#[serde(tag = "is", rename_all = "snake_case")]pub enum FileType { Dir, File, Symlink,}
#[typetag::deserialize(name = "file_type")]#[async_trait]impl Filter for FileType { async fn matches(&self, path: &Path) -> bool { use FileType::*;
with_metadata(path, |metadata| { let ft = metadata.file_type();
match *self { Dir => ft.is_dir(), File => ft.is_file(), Symlink => ft.is_symlink(), } }) .await }}
async fn with_metadata<F>(path: &Path, fun: F) -> boolwhere F: FnOnce(std::fs::Metadata) -> bool,{ fs::metadata(path).await.map(fun).unwrap_or(false)}
filter! { "not"; struct Not { filter: Box<dyn Filter>, }; (self, path) -> { !self.filter.matches(path).await }}
filter! { "any"; struct Any { filters: Box<[Box<dyn Filter>]>, }; (self, path) -> { stream::iter(&*self.filters) .any(|f| f.matches(path)) .await }}
filter! { "all"; struct All { filters: Box<[Box<dyn Filter>]>, }; (self, path) -> { stream::iter(&*self.filters) .all(|f| f.matches(path)) .await }}
#[derive(Debug, Deserialize)]#[serde(rename_all = "snake_case")]pub enum Magic { Mime(String), Magic { bytes: Box<[u8]>, #[serde(default)] offset: u64, },}
async fn read_first_bytes(n: usize, path: &Path, offset: u64) -> io::Result<Box<[u8]>> { use std::io::SeekFrom;
let mut file = fs::File::open(path).await?;
let mut buf = vec![0; n];
file.seek(SeekFrom::Start(offset)).await?; file.read_exact(&mut buf).await?;
Ok(buf.into())}
async fn guess_mime(path: &Path) -> Option<infer::Type> { let mut file = fs::File::open(path).await.ok()?;
let mut buf = vec![0; 8192];
let len = file.read(&mut buf).await.ok()?;
infer::get(&buf[0..len])}
#[typetag::deserialize(name = "content_type")]#[async_trait]impl Filter for Magic { async fn matches(&self, file: &Path) -> bool { match *self { Self::Magic { ref bytes, offset } => read_first_bytes(bytes.len(), file, offset) .await .map(|read| read == *bytes) .unwrap_or(false), Self::Mime(ref mime_type) => guess_mime(file) .await .map(|typ| typ.mime_type() == mime_type) .unwrap_or(false), } }}