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