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 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 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 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 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 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)]
203pub struct UnsyncPost {
205 pub source: String,
207 pub title: String,
209 pub content: Vec<UnsyncContent>,
211 pub thumb: Option<UnsyncFileMeta>,
213 pub comments: Vec<Comment>,
215 pub updated: Option<DateTime<Utc>>,
217 pub published: Option<DateTime<Utc>>,
219 pub platform: PlatformId,
221 pub tags: Vec<UnsyncTag>,
223 pub authors: Vec<AuthorId>,
225 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}