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}