Skip to main content

post_archiver/importer/
post.rs

1use std::{collections::HashSet, fmt::Debug};
2
3use chrono::{DateTime, Utc};
4use rusqlite::params;
5
6use crate::{
7    error::Result,
8    manager::{
9        PostArchiverConnection, PostArchiverManager, UpdateAuthor, UpdateCollection, UpdatePost,
10    },
11    AuthorId, CollectionId, Comment, Content, PlatformId, PostId, POSTS_PRE_CHUNK,
12};
13
14use super::{collection::UnsyncCollection, tag::UnsyncTag, UnsyncFileMeta};
15
16impl<T> PostArchiverManager<T>
17where
18    T: PostArchiverConnection,
19{
20    /// Import a post into the archive.
21    ///
22    /// If the post already exists (by source), it updates its title, platform, published date,
23    ///
24    /// # Parameters
25    ///
26    /// - `update_relation`: update the relations of authors and collections after importing.
27    ///
28    /// # Errors
29    ///
30    /// Returns `Error` if there was an error accessing the database.
31    pub fn import_post(
32        &self,
33        post: UnsyncPost,
34        update_relation: bool,
35    ) -> Result<(PostId, Vec<AuthorId>, Vec<CollectionId>)> {
36        macro_rules! import_many {
37            ($vec:expr => $method:ident) => {
38                $vec.into_iter()
39                    .map(|d| self.$method(d))
40                    .collect::<std::result::Result<Vec<_>, _>>()?
41            };
42        }
43
44        // find post by source
45        let existing: Option<PostId> = self.find_post(&post.source)?;
46
47        let id = match existing {
48            Some(id) => {
49                let b = self.bind(id);
50                b.update(
51                    UpdatePost::default()
52                        .title(post.title)
53                        .platform(Some(post.platform))
54                        .published(post.published.unwrap_or_else(Utc::now))
55                        .updated_by_latest(post.updated.unwrap_or_else(Utc::now)),
56                )?;
57                id
58            }
59            None => {
60                // insert new post
61                let mut stmt = self.conn().prepare_cached(
62                    "INSERT INTO posts (title, source, platform, published, updated) VALUES (?, ?, ?, ?, ?) RETURNING id",
63                )?;
64                let published = post.published.unwrap_or_else(Utc::now);
65                let updated = post.updated.unwrap_or_else(Utc::now);
66                stmt.query_row(
67                    params![post.title, post.source, post.platform, published, updated],
68                    |row| row.get(0),
69                )?
70            }
71        };
72
73        let b = self.bind(id);
74
75        let mut thumb = post
76            .thumb
77            .as_ref()
78            .map(|thumb| self.import_file_meta(id, thumb))
79            .transpose()?;
80
81        let content = post
82            .content
83            .iter()
84            .map(|content| {
85                Ok(match content {
86                    UnsyncContent::Text(text) => Content::Text(text.clone()),
87                    UnsyncContent::File(file) => {
88                        let need_thumb = thumb.is_none() && file.mime.starts_with("image/");
89                        let file_meta = self.import_file_meta(id, file)?;
90                        need_thumb.then(|| thumb = Some(file_meta));
91                        Content::File(file_meta)
92                    }
93                })
94            })
95            .collect::<Result<Vec<_>>>()?;
96        b.update(
97            UpdatePost::default()
98                .content(content)
99                .thumb(thumb)
100                .comments(post.comments),
101        )?;
102
103        let tags = import_many!(post.tags => import_tag);
104        b.add_tags(&tags)?;
105
106        let collections = import_many!(post.collections => import_collection);
107        b.add_collections(&collections)?;
108
109        b.add_authors(&post.authors)?;
110
111        //
112        let path = self
113            .path
114            .join((id.raw() / POSTS_PRE_CHUNK).to_string())
115            .join((id.raw() % POSTS_PRE_CHUNK).to_string());
116
117        for f in post
118            .content
119            .into_iter()
120            .flat_map(|c| match c {
121                UnsyncContent::Text(_) => None,
122                UnsyncContent::File(file) => Some(file),
123            })
124            .chain(post.thumb)
125        {
126            let path = path.join(f.filename);
127            if let Some(parent) = path.parent() {
128                std::fs::create_dir_all(parent)?;
129            };
130            f.data.write(&path)?;
131        }
132
133        if update_relation {
134            post.authors.iter().try_for_each(|&author| {
135                self.bind(author).update(
136                    UpdateAuthor::default()
137                        .thumb_by_latest()
138                        .updated_by_latest(),
139                )
140            })?;
141
142            collections.iter().try_for_each(|&collection| {
143                self.bind(collection)
144                    .update(UpdateCollection::default().thumb_by_latest())
145            })?;
146        }
147
148        Ok((
149            id,
150            post.authors.into_iter().collect(),
151            collections.into_iter().collect(),
152        ))
153    }
154
155    /// Import multiple posts into the archive.
156    ///
157    /// This function processes each post, importing its authors, collections, and files.
158    ///
159    /// # Parameters
160    ///
161    /// - `update_relation`: update the relations of authors and collections after importing.
162    ///
163    /// # Errors
164    ///
165    /// Returns `Error` if there was an error accessing the database.
166    pub fn import_posts<U>(
167        &self,
168        posts: impl IntoIterator<Item = UnsyncPost>,
169        update_relation: bool,
170    ) -> Result<Vec<PostId>> {
171        let mut total_author = HashSet::new();
172        let mut total_collections = HashSet::new();
173        let mut results = Vec::new();
174
175        for post in posts {
176            let (id, authors, collections) = self.import_post(post, false)?;
177
178            results.push(id);
179            total_author.extend(authors);
180            total_collections.extend(collections);
181        }
182
183        if update_relation {
184            total_author.into_iter().try_for_each(|author| {
185                self.bind(author).update(
186                    UpdateAuthor::default()
187                        .thumb_by_latest()
188                        .updated_by_latest(),
189                )
190            })?;
191
192            total_collections.into_iter().try_for_each(|collection| {
193                self.bind(collection)
194                    .update(UpdateCollection::default().thumb_by_latest())
195            })?;
196        }
197
198        Ok(results)
199    }
200}
201
202#[derive(Debug, Clone)]
203/// Represents a post that is not yet synchronized with the archive.
204pub struct UnsyncPost {
205    /// The original URL of this post (e.g., "https://example.com/blog/1")
206    pub source: String,
207    /// The title of the post
208    pub title: String,
209    /// The post's content items (text and file references)
210    pub content: Vec<UnsyncContent>,
211    /// Optional thumbnail image for the post
212    pub thumb: Option<UnsyncFileMeta>,
213    /// Comments on the post
214    pub comments: Vec<Comment>,
215    /// When the post was updated
216    pub updated: Option<DateTime<Utc>>,
217    /// When the post was published
218    pub published: Option<DateTime<Utc>>,
219    /// Platform associated with the post
220    pub platform: PlatformId,
221    /// Tags associated with the post
222    pub tags: Vec<UnsyncTag>,
223    /// The IDs of the author who created this post
224    pub authors: Vec<AuthorId>,
225    /// The collections this post belongs to
226    pub collections: Vec<UnsyncCollection>,
227}
228
229impl UnsyncPost {
230    pub fn new(
231        platform: PlatformId,
232        source: String,
233        title: String,
234        content: Vec<UnsyncContent>,
235    ) -> Self {
236        Self {
237            source,
238            title,
239            content,
240            thumb: None,
241            comments: Vec::new(),
242            updated: None,
243            published: None,
244            platform,
245            tags: Vec::new(),
246            authors: Vec::new(),
247            collections: Vec::new(),
248        }
249    }
250
251    pub fn source(self, source: String) -> Self {
252        Self { source, ..self }
253    }
254
255    pub fn title(self, title: String) -> Self {
256        Self { title, ..self }
257    }
258
259    pub fn content(self, content: Vec<UnsyncContent>) -> Self {
260        Self { content, ..self }
261    }
262
263    pub fn thumb(self, thumb: Option<UnsyncFileMeta>) -> Self {
264        Self { thumb, ..self }
265    }
266
267    pub fn comments(self, comments: Vec<Comment>) -> Self {
268        Self { comments, ..self }
269    }
270
271    pub fn updated(self, updated: DateTime<Utc>) -> Self {
272        Self {
273            updated: Some(updated),
274            ..self
275        }
276    }
277
278    pub fn published(self, published: DateTime<Utc>) -> Self {
279        Self {
280            published: Some(published),
281            ..self
282        }
283    }
284
285    pub fn platform(self, platform: PlatformId) -> Self {
286        Self { platform, ..self }
287    }
288
289    pub fn tags(self, tags: Vec<UnsyncTag>) -> Self {
290        Self { tags, ..self }
291    }
292
293    pub fn authors(self, authors: Vec<AuthorId>) -> Self {
294        Self { authors, ..self }
295    }
296
297    pub fn collections(self, collections: Vec<UnsyncCollection>) -> Self {
298        Self {
299            collections,
300            ..self
301        }
302    }
303}
304
305#[derive(Debug, Clone)]
306pub enum UnsyncContent {
307    Text(String),
308    File(UnsyncFileMeta),
309}