Simple tool for automatic file management
1use std::path::Path;
2
3use async_trait::async_trait;
4use serde::Deserialize;
5
6use tokio::io::{self, AsyncReadExt};
7use tokio::fs;
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 "any";
129 struct Any { filters: Box<[Box<dyn Filter>]>, };
130 (self, path) -> {
131 stream::iter(&*self.filters)
132 .any(|f| f.matches(path))
133 .await
134 }
135}
136
137filter! {
138 "all";
139 struct All { filters: Box<[Box<dyn Filter>]>, };
140 (self, path) -> {
141 stream::iter(&*self.filters)
142 .all(|f| f.matches(path))
143 .await
144 }
145}
146
147#[derive(Debug, Deserialize)]
148#[serde(rename_all = "snake_case")]
149pub enum Magic {
150 Mime(String),
151 Bytes(Box<[u8]>),
152}
153
154async fn read_first_bytes(n: usize, path: &Path) -> io::Result<Box<[u8]>> {
155 let mut file = fs::File::open(path).await?;
156
157 let mut buf = vec![0; n];
158
159 file.read_exact(&mut buf).await?;
160
161 Ok(buf.into())
162}
163
164#[typetag::deserialize(name = "content_type")]
165#[async_trait]
166impl Filter for Magic {
167 async fn matches(&self, file: &Path) -> bool {
168 match *self {
169 Magic::Bytes(ref bytes) => {
170 read_first_bytes(bytes.len(), file).await.map(|read| read == *bytes).unwrap_or(false)
171 },
172 Magic::Mime(_) => unimplemented!()
173 }
174 }
175}