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
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}