Skip to main content

post_archiver/manager/
file_meta.rs

1use std::{collections::HashMap, fs::File, io::Write, path::PathBuf};
2
3use serde_json::Value;
4
5use crate::{
6    error::Result,
7    manager::{binded::Binded, PostArchiverConnection},
8    query::FromQuery,
9    FileMeta, FileMetaId, Post, PostId,
10};
11
12/// Builder for updating a file metadata's fields.
13///
14/// Fields left as `None` are not modified.
15#[derive(Debug, Clone)]
16pub struct UpdateFileMeta<T> {
17    pub mime: Option<String>,
18    pub extra: Option<HashMap<String, Value>>,
19    pub content: Option<T>,
20}
21
22impl Default for UpdateFileMeta<()> {
23    fn default() -> Self {
24        UpdateFileMeta {
25            mime: None,
26            extra: None,
27            content: None,
28        }
29    }
30}
31
32impl<T> UpdateFileMeta<T> {
33    /// Set the MIME type.
34    pub fn mime(mut self, mime: String) -> Self {
35        self.mime = Some(mime);
36        self
37    }
38    /// Set the extra metadata.
39    pub fn extra(mut self, extra: HashMap<String, Value>) -> Self {
40        self.extra = Some(extra);
41        self
42    }
43    pub fn content<U: WritableFileMeta>(self, content: U) -> UpdateFileMeta<U> {
44        UpdateFileMeta {
45            content: Some(content),
46            mime: self.mime,
47            extra: self.extra,
48        }
49    }
50}
51
52impl UpdateFileMeta<()> {
53    /// Convert this update to a version without content, for use in the `update` method.
54    pub fn new() -> UpdateFileMeta<()> {
55        UpdateFileMeta {
56            content: None,
57            mime: None,
58            extra: None,
59        }
60    }
61}
62
63pub trait WritableFileMeta {
64    fn write_to_file(&self, file: &mut File) -> std::io::Result<()>;
65}
66
67macro_rules! can_be_content {
68    ($t:ty) => {
69        impl UpdateFileMeta<$t> {
70            pub fn new(content: $t) -> Self {
71                Self {
72                    content: Some(content),
73                    mime: None,
74                    extra: None,
75                }
76            }
77        }
78    };
79}
80
81can_be_content!(File);
82impl WritableFileMeta for File {
83    fn write_to_file(&self, file: &mut File) -> std::io::Result<()> {
84        let mut src_file = self.try_clone()?;
85        std::io::copy(&mut src_file, file)?;
86        file.sync_data()?;
87        Ok(())
88    }
89}
90
91can_be_content!(Vec<u8>);
92impl WritableFileMeta for Vec<u8> {
93    fn write_to_file(&self, file: &mut File) -> std::io::Result<()> {
94        file.write_all(self)?;
95        file.sync_data()?;
96        Ok(())
97    }
98}
99
100can_be_content!(PathBuf);
101impl WritableFileMeta for PathBuf {
102    fn write_to_file(&self, file: &mut File) -> std::io::Result<()> {
103        let mut src_file = File::open(self)?;
104        std::io::copy(&mut src_file, file)?;
105        file.sync_data()?;
106        Ok(())
107    }
108}
109
110can_be_content!(String);
111impl WritableFileMeta for String {
112    fn write_to_file(&self, file: &mut File) -> std::io::Result<()> {
113        file.write_all(self.as_bytes())?;
114        file.sync_data()?;
115        Ok(())
116    }
117}
118
119//=============================================================
120// Update / Delete
121//=============================================================
122impl<'a, C: PostArchiverConnection> Binded<'a, FileMetaId, C> {
123    /// Get this file metadata's current data from the database.
124    pub fn value(&self) -> Result<FileMeta> {
125        let mut stmt = self
126            .conn()
127            .prepare_cached("SELECT * FROM file_metas WHERE id = ?")?;
128        Ok(stmt.query_row([self.id()], FileMeta::from_row)?)
129    }
130
131    /// Remove this file metadata from the archive.
132    ///
133    /// This operation will also remove all associated thumb references.
134    /// But it will not delete post.content related to this file.
135    pub fn delete(self) -> Result<()> {
136        let mut stmt = self
137            .conn()
138            .prepare_cached("DELETE FROM file_metas WHERE id = ?")?;
139        stmt.execute([self.id()])?;
140        Ok(())
141    }
142
143    /// Apply a batch of field updates to this file metadata in a single SQL statement.
144    ///
145    /// Only fields set on `update` (i.e. `Some(...)`) are written to the database.
146    pub fn update<T>(&self, update: UpdateFileMeta<T>) -> Result<()> {
147        use rusqlite::types::ToSql;
148
149        let extra_json = update.extra.map(|e| serde_json::to_string(&e).unwrap());
150
151        let mut sets: Vec<&str> = Vec::new();
152        let mut params: Vec<&dyn ToSql> = Vec::new();
153
154        macro_rules! push {
155            ($field:expr, $col:expr) => {
156                if let Some(ref v) = $field {
157                    sets.push($col);
158                    params.push(v);
159                }
160            };
161        }
162
163        push!(update.mime, "mime = ?");
164        push!(extra_json, "extra = ?");
165
166        let sql = format!("UPDATE file_metas SET {} WHERE id = ?", sets.join(", "));
167        let id = self.id();
168        params.push(&id);
169        self.conn().execute(&sql, params.as_slice())?;
170
171        Ok(())
172    }
173
174    pub fn update_with_content<T>(&self, mut update: UpdateFileMeta<T>) -> Result<()>
175    where
176        T: WritableFileMeta,
177    {
178        let content = update.content.take();
179        self.update(UpdateFileMeta {
180            content: None,
181            ..update
182        })?;
183
184        let path = self.get_path()?;
185
186        if let Some(content) = content {
187            let mut file = File::create(path)?;
188            content.write_to_file(&mut file)?;
189        }
190
191        Ok(())
192    }
193
194    /// Get the file path of this file metadata.
195    pub fn get_path(&self) -> Result<PathBuf> {
196        let mut stmt = self
197            .conn()
198            .prepare_cached("SELECT post, filename FROM file_metas WHERE id = ?")?;
199        Ok(stmt.query_row([self.id()], |row| {
200            let post_id: PostId = row.get(0)?;
201            let filename: String = row.get(1)?;
202            Ok(Post::directory(post_id).join(filename))
203        })?)
204    }
205}