Skip to main content

post_archiver/importer/
file_meta.rs

1use std::{collections::HashMap, fs::File, hash::Hash, path::PathBuf};
2
3use rusqlite::params;
4use serde_json::Value;
5
6use crate::{
7    error::Result,
8    manager::{PostArchiverConnection, PostArchiverManager, UpdateFileMeta, WritableFileMeta},
9    FileMetaId, Post, PostId,
10};
11
12impl<T> PostArchiverManager<T>
13where
14    T: PostArchiverConnection,
15{
16    /// Create or update a file metadata entry in the archive.
17    ///
18    /// Takes a file metadata object and either creates a new entry or updates an existing one.
19    /// if a file metadata with the same filename (and post id) already exists, it only updates metadata
20    ///
21    /// # Errors
22    ///
23    /// Returns `Error` if there was an error accessing the database.
24    pub fn import_file_meta<U>(
25        &self,
26        post: PostId,
27        file_meta: &UnsyncFileMeta<U>,
28    ) -> Result<FileMetaId> {
29        // find
30        if let Some(id) = self.find_file_meta(post, &file_meta.filename)? {
31            // update extra
32            self.bind(id)
33                .update(UpdateFileMeta::default().extra(file_meta.extra.clone()))?;
34            return Ok(id);
35        }
36
37        // insert
38        let mut ins_stmt = self.conn().prepare_cached(
39            "INSERT INTO file_metas (post, filename, mime, extra) VALUES (?, ?, ?, ?) RETURNING id",
40        )?;
41        Ok(ins_stmt.query_row(
42            params![
43                post,
44                file_meta.filename,
45                file_meta.mime,
46                serde_json::to_string(&file_meta.extra).unwrap()
47            ],
48            |row| row.get(0),
49        )?)
50    }
51
52    /// Create or update a file metadata entry in the archive, and write `file_meta.data` to disk.
53    ///
54    /// Behaves like [`import_file_meta`](Self::import_file_meta) for the database entry, then
55    /// writes the content of `file_meta.data` to
56    /// `<archive_path>/<post_dir>/<filename>`, creating intermediate directories as needed.
57    ///
58    /// # Errors
59    ///
60    /// Returns `Error` if there was an error accessing the database.
61    pub fn import_file_meta_with_content<U>(
62        &self,
63        post: PostId,
64        file_meta: &UnsyncFileMeta<U>,
65    ) -> Result<FileMetaId>
66    where
67        U: WritableFileMeta,
68    {
69        let id = self.import_file_meta(post, file_meta)?;
70
71        let path = self
72            .path
73            .join(Post::directory(post))
74            .join(&file_meta.filename);
75
76        if let Some(parent) = path.parent() {
77            std::fs::create_dir_all(parent)?;
78        }
79
80        let mut file = File::create(&path)?;
81        file_meta.data.write_to_file(&mut file)?;
82
83        Ok(id)
84    }
85
86    /// Create or update a file metadata entry in the archive by moving a buffered file into it.
87    ///
88    /// Behaves like [`import_file_meta`](Self::import_file_meta) for the database entry, then
89    /// moves the already-buffered file at `file_meta.data` to
90    /// `<archive_path>/<post_dir>/<filename>`, creating intermediate directories as needed.
91    ///
92    /// Uses [`std::fs::rename`] for an atomic, zero-copy move. The source file and the archive
93    /// **must reside on the same filesystem**; cross-device moves will fail.
94    /// Use [`import_file_meta_with_content`](Self::import_file_meta_with_content) instead when
95    /// the source and destination may be on different filesystems.
96    ///
97    /// # Errors
98    ///
99    /// Returns `Error` if there was an error accessing the database.
100    pub fn import_file_meta_by_rename(
101        &self,
102        post: PostId,
103        file_meta: &UnsyncFileMeta<PathBuf>,
104    ) -> Result<FileMetaId> {
105        let id = self.import_file_meta(post, file_meta)?;
106
107        let path = self
108            .path
109            .join(Post::directory(post))
110            .join(&file_meta.filename);
111
112        if let Some(parent) = path.parent() {
113            std::fs::create_dir_all(parent)?;
114        }
115
116        std::fs::rename(&file_meta.data, &path)?;
117
118        Ok(id)
119    }
120}
121
122/// Represents a file metadata that is not yet synced to the database.
123#[derive(Debug, Clone)]
124pub struct UnsyncFileMeta<T> {
125    pub filename: String,
126    pub mime: String,
127    pub extra: HashMap<String, Value>,
128    pub data: T,
129}
130
131impl<T> Hash for UnsyncFileMeta<T> {
132    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
133        self.filename.hash(state);
134        self.mime.hash(state);
135    }
136}
137
138impl<T> PartialEq for UnsyncFileMeta<T> {
139    fn eq(&self, other: &Self) -> bool {
140        self.filename == other.filename && self.mime == other.mime && self.extra == other.extra
141    }
142}
143
144impl<T> Eq for UnsyncFileMeta<T> {}
145
146impl<T> UnsyncFileMeta<T> {
147    pub fn new(filename: String, mime: String, data: T) -> Self {
148        Self {
149            filename,
150            mime,
151            data,
152            extra: HashMap::new(),
153        }
154    }
155
156    pub fn extra(mut self, extra: HashMap<String, Value>) -> Self {
157        self.extra = extra;
158        self
159    }
160}