Skip to main content

post_archiver/manager/
file_meta.rs

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