1use std::collections::BTreeSet;
4
5use crate::client::FigshareClient;
6use crate::error::FigshareError;
7use crate::ids::ArticleId;
8use crate::metadata::ArticleMetadata;
9use crate::model::{Article, ArticleFile};
10use crate::upload::{FileReplacePolicy, UploadSource, UploadSpec};
11
12#[derive(Clone, Debug, PartialEq)]
14pub struct PublishedArticle {
15 pub article: Article,
17 pub public_article: Article,
19}
20
21impl FigshareClient {
22 pub(crate) async fn reconcile_files(
30 &self,
31 article: &Article,
32 policy: FileReplacePolicy,
33 uploads: Vec<UploadSpec>,
34 ) -> Result<Vec<ArticleFile>, FigshareError> {
35 let upload_filenames = validate_upload_filenames(&uploads)?;
36 let existing = self.list_files(article.id).await?;
37
38 match policy {
39 FileReplacePolicy::KeepExistingAndAdd => {
40 for upload in &uploads {
41 if existing.iter().any(|file| file.name == upload.filename) {
42 return Err(FigshareError::ConflictingDraftFile {
43 filename: upload.filename.clone(),
44 });
45 }
46 }
47 }
48 FileReplacePolicy::ReplaceAll | FileReplacePolicy::UpsertByFilename => {}
49 }
50
51 let mut uploaded = Vec::new();
52 for upload in uploads {
53 let result = match upload.source {
54 UploadSource::Path(path) => {
55 self.upload_path_with_filename(article.id, &upload.filename, &path)
56 .await
57 }
58 UploadSource::Reader {
59 reader,
60 content_length,
61 } => {
62 self.upload_reader(article.id, &upload.filename, reader, content_length)
63 .await
64 }
65 };
66
67 match result {
68 Ok(file) => uploaded.push(file),
69 Err(error) => {
70 self.cleanup_uploaded_files(article.id, &uploaded).await;
71 return Err(error);
72 }
73 }
74 }
75
76 match policy {
77 FileReplacePolicy::ReplaceAll => {
78 for file in &existing {
79 self.delete_file(article.id, file.id).await?;
80 }
81 }
82 FileReplacePolicy::UpsertByFilename => {
83 for file in existing
84 .iter()
85 .filter(|file| upload_filenames.contains(&file.name))
86 {
87 self.delete_file(article.id, file.id).await?;
88 }
89 }
90 FileReplacePolicy::KeepExistingAndAdd => {}
91 }
92
93 self.list_files(article.id).await
94 }
95
96 pub(crate) async fn create_and_publish_article(
104 &self,
105 metadata: &ArticleMetadata,
106 uploads: Vec<UploadSpec>,
107 ) -> Result<PublishedArticle, FigshareError> {
108 let article = self.create_article(metadata).await?;
109 if let Err(error) = self
110 .reconcile_files(&article, FileReplacePolicy::ReplaceAll, uploads)
111 .await
112 {
113 let _ = self.delete_article(article.id).await;
114 return Err(error);
115 }
116
117 let public_article = match self.publish_article(article.id).await {
118 Ok(public_article) => public_article,
119 Err(error) => {
120 let _ = self.delete_article(article.id).await;
121 return Err(error);
122 }
123 };
124 let article = self.wait_for_own_article_public(article.id).await?;
125
126 Ok(PublishedArticle {
127 article,
128 public_article,
129 })
130 }
131
132 pub(crate) async fn publish_existing_article_with_policy(
140 &self,
141 article_id: ArticleId,
142 metadata: &ArticleMetadata,
143 policy: FileReplacePolicy,
144 uploads: Vec<UploadSpec>,
145 ) -> Result<PublishedArticle, FigshareError> {
146 let article = self.update_article(article_id, metadata).await?;
147 self.reconcile_files(&article, policy, uploads).await?;
148 let public_article = self.publish_article(article_id).await?;
149 let article = self.wait_for_own_article_public(article_id).await?;
150
151 Ok(PublishedArticle {
152 article,
153 public_article,
154 })
155 }
156}
157
158impl FigshareClient {
159 async fn cleanup_uploaded_files(&self, article_id: ArticleId, uploaded: &[ArticleFile]) {
160 for file in uploaded {
161 let _ = self.delete_file(article_id, file.id).await;
162 }
163 }
164}
165
166fn validate_upload_filenames(uploads: &[UploadSpec]) -> Result<BTreeSet<String>, FigshareError> {
167 client_uploader_traits::collect_upload_filenames(uploads.iter()).map_err(FigshareError::from)
168}
169
170#[cfg(test)]
171mod tests {
172 use super::validate_upload_filenames;
173 use crate::{upload::UploadSpec, FigshareError};
174
175 #[test]
176 fn duplicate_filenames_are_rejected() {
177 let uploads = vec![
178 UploadSpec::from_reader("artifact.bin", std::io::Cursor::new(vec![1]), 1),
179 UploadSpec::from_reader("artifact.bin", std::io::Cursor::new(vec![2]), 1),
180 ];
181 assert!(matches!(
182 validate_upload_filenames(&uploads),
183 Err(FigshareError::DuplicateUploadFilename { .. })
184 ));
185 }
186
187 #[test]
188 fn empty_filenames_are_rejected() {
189 let uploads = vec![UploadSpec::from_reader(
190 "",
191 std::io::Cursor::new(vec![1]),
192 1,
193 )];
194
195 assert!(matches!(
196 validate_upload_filenames(&uploads),
197 Err(FigshareError::InvalidState(message)) if message == "upload filename cannot be empty"
198 ));
199 }
200}