mithril_aggregator/file_uploaders/
local_uploader.rs

1use anyhow::Context;
2use async_trait::async_trait;
3use slog::{debug, Logger};
4use std::path::{Path, PathBuf};
5
6use mithril_common::StdResult;
7use mithril_common::{entities::FileUri, logging::LoggerExtensions};
8
9use crate::file_uploaders::{FileUploadRetryPolicy, FileUploader};
10use crate::tools::url_sanitizer::SanitizedUrlWithTrailingSlash;
11
12/// LocalUploader is a file uploader working using local files
13pub struct LocalUploader {
14    /// File server URL prefix
15    server_url_prefix: SanitizedUrlWithTrailingSlash,
16
17    /// Target folder where to store files archives
18    target_location: Option<PathBuf>,
19
20    retry_policy: FileUploadRetryPolicy,
21    logger: Logger,
22}
23
24impl LocalUploader {
25    /// Instantiates a new LocalUploader that copies 'uploaded' files to a target location
26    pub(crate) fn new(
27        server_url_prefix: SanitizedUrlWithTrailingSlash,
28        target_location: &Path,
29        retry_policy: FileUploadRetryPolicy,
30        logger: Logger,
31    ) -> Self {
32        let logger = logger.new_with_component_name::<Self>();
33        debug!(logger, "New LocalUploader created"; "server_url_prefix" => &server_url_prefix.as_str());
34
35        Self {
36            server_url_prefix,
37            target_location: Some(target_location.to_path_buf()),
38            logger,
39            retry_policy,
40        }
41    }
42
43    /// Instantiates a new LocalUploader that does not copy files and only returns the location
44    pub(crate) fn new_without_copy(
45        server_url_prefix: SanitizedUrlWithTrailingSlash,
46        retry_policy: FileUploadRetryPolicy,
47        logger: Logger,
48    ) -> Self {
49        let logger = logger.new_with_component_name::<Self>();
50        debug!(logger, "New LocalUploader created"; "server_url_prefix" => &server_url_prefix.as_str());
51
52        Self {
53            server_url_prefix,
54            target_location: None,
55            logger,
56            retry_policy,
57        }
58    }
59}
60
61#[async_trait]
62impl FileUploader for LocalUploader {
63    async fn upload_without_retry(&self, filepath: &Path) -> StdResult<FileUri> {
64        let archive_name = filepath.file_name().unwrap().to_str().unwrap();
65
66        let disk_path = if let Some(target_location) = &self.target_location {
67            let target_path = target_location.join(archive_name);
68            tokio::fs::copy(filepath, &target_path)
69                .await
70                .with_context(|| "File copy failure")?;
71            target_path
72        } else {
73            filepath.to_path_buf()
74        };
75
76        let uri = self.server_url_prefix.join(archive_name)?.to_string();
77
78        debug!(self.logger, "File 'uploaded' to local storage"; "uri" => &uri, "disk_path" => disk_path.display());
79
80        Ok(FileUri(uri))
81    }
82
83    fn retry_policy(&self) -> FileUploadRetryPolicy {
84        self.retry_policy.clone()
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use std::fs::File;
91    use std::io::Write;
92    use std::path::{Path, PathBuf};
93    use std::time::Duration;
94
95    use mithril_common::test_utils::TempDir;
96
97    use crate::test_tools::TestLogger;
98
99    use super::*;
100
101    fn create_fake_archive(dir: &Path, name: &str) -> PathBuf {
102        let file_path = dir.join(format!("{name}.tar.gz"));
103        let mut file = File::create(&file_path).unwrap();
104        writeln!(
105            file,
106            "I swear, this is an archive, not a temporary test file."
107        )
108        .unwrap();
109
110        file_path
111    }
112
113    #[tokio::test]
114    async fn should_extract_archive_name_to_deduce_location() {
115        let source_dir = TempDir::create(
116            "local_uploader",
117            "should_extract_archive_name_to_deduce_location_source",
118        );
119        let target_dir = TempDir::create(
120            "local_uploader",
121            "should_extract_archive_name_to_deduce_location_target",
122        );
123        let archive_name = "an_archive";
124        let archive = create_fake_archive(&source_dir, archive_name);
125        let expected_location = format!(
126            "http://test.com:8080/base-root/{}",
127            &archive.file_name().unwrap().to_string_lossy()
128        );
129
130        let url_prefix =
131            SanitizedUrlWithTrailingSlash::parse("http://test.com:8080/base-root").unwrap();
132        let uploader = LocalUploader::new(
133            url_prefix,
134            &target_dir,
135            FileUploadRetryPolicy::never(),
136            TestLogger::stdout(),
137        );
138        let location = FileUploader::upload(&uploader, &archive)
139            .await
140            .expect("local upload should not fail");
141
142        assert_eq!(FileUri(expected_location), location);
143    }
144
145    #[tokio::test]
146    async fn should_copy_file_to_target_location() {
147        let source_dir = TempDir::create(
148            "local_uploader",
149            "should_copy_file_to_target_location_source",
150        );
151        let target_dir = TempDir::create(
152            "local_uploader",
153            "should_copy_file_to_target_location_target",
154        );
155        let archive = create_fake_archive(&source_dir, "an_archive");
156        let uploader = LocalUploader::new(
157            SanitizedUrlWithTrailingSlash::parse("http://test.com:8080/base-root/").unwrap(),
158            &target_dir,
159            FileUploadRetryPolicy::never(),
160            TestLogger::stdout(),
161        );
162        FileUploader::upload(&uploader, &archive).await.unwrap();
163
164        assert!(target_dir.join(archive.file_name().unwrap()).exists());
165    }
166
167    #[tokio::test]
168    async fn should_error_if_path_is_a_directory() {
169        let source_dir = TempDir::create(
170            "local_uploader",
171            "should_error_if_path_is_a_directory_source",
172        );
173        let target_dir = TempDir::create(
174            "local_uploader",
175            "should_error_if_path_is_a_directory_target",
176        );
177        let uploader = LocalUploader::new(
178            SanitizedUrlWithTrailingSlash::parse("http://test.com:8080/base-root/").unwrap(),
179            &target_dir,
180            FileUploadRetryPolicy::never(),
181            TestLogger::stdout(),
182        );
183        FileUploader::upload(&uploader, &source_dir)
184            .await
185            .expect_err("Uploading a directory should fail");
186    }
187
188    #[tokio::test]
189    async fn retry_policy_from_file_uploader_trait_should_be_implemented() {
190        let target_dir = TempDir::create("local_uploader", "test_retry_policy");
191        let expected_policy = FileUploadRetryPolicy {
192            attempts: 10,
193            delay_between_attempts: Duration::from_millis(123),
194        };
195
196        let uploader: Box<dyn FileUploader> = Box::new(LocalUploader::new(
197            SanitizedUrlWithTrailingSlash::parse("http://test.com:8080/base-root/").unwrap(),
198            &target_dir,
199            expected_policy.clone(),
200            TestLogger::stdout(),
201        ));
202
203        assert_eq!(expected_policy, uploader.retry_policy());
204    }
205
206    #[tokio::test]
207    async fn should_only_return_location_if_copy_disabled() {
208        let source_dir = TempDir::create(
209            "local_uploader",
210            "should_only_return_location_and_not_copy_file_if_copy_disabled",
211        );
212        let archive = create_fake_archive(&source_dir, "an_archive");
213        let uploader = LocalUploader::new_without_copy(
214            SanitizedUrlWithTrailingSlash::parse("http://test.com:8080/base-root/").unwrap(),
215            FileUploadRetryPolicy::never(),
216            TestLogger::stdout(),
217        );
218        let location = FileUploader::upload(&uploader, &archive).await.unwrap();
219
220        let expected_location = format!(
221            "http://test.com:8080/base-root/{}",
222            &archive.file_name().unwrap().to_string_lossy()
223        );
224        assert_eq!(FileUri(expected_location), location);
225    }
226}