1 //
2 //  Copyright 2023 Google, Inc.
3 //
4 //  Licensed under the Apache License, Version 2.0 (the "License");
5 //  you may not use this file except in compliance with the License.
6 //  You may obtain a copy of the License at:
7 //
8 //  http://www.apache.org/licenses/LICENSE-2.0
9 //
10 //  Unless required by applicable law or agreed to in writing, software
11 //  distributed under the License is distributed on an "AS IS" BASIS,
12 //  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 //  See the License for the specific language governing permissions and
14 //  limitations under the License.
15 
16 //! # Zip Artifact Class
17 
18 use std::{
19     fs::{read_dir, remove_file, File},
20     io::{Read, Result, Write},
21     path::PathBuf,
22 };
23 
24 use log::warn;
25 use zip::{result::ZipResult, write::FileOptions, ZipWriter};
26 
27 use crate::system::netsimd_temp_dir;
28 
29 use super::time_display::file_current_time;
30 
31 /// Recurse all files in root and put it in Vec<PathBuf>
recurse_files(root: &PathBuf) -> Result<Vec<(PathBuf, String)>>32 fn recurse_files(root: &PathBuf) -> Result<Vec<(PathBuf, String)>> {
33     let mut result = Vec::new();
34     // Read all entries in the given root directory
35     let entries = read_dir(root)?;
36     for entry in entries {
37         let entry = entry?;
38         let meta = entry.metadata()?;
39         // Perform recursion if it's a directory
40         if meta.is_dir() {
41             let mut subdir = recurse_files(&entry.path())?;
42             result.append(&mut subdir);
43         }
44         if meta.is_file() {
45             if let Some(filename) = entry
46                 .path()
47                 .file_name()
48                 .and_then(|os_name| os_name.to_str())
49                 .map(|str_name| str_name.to_string())
50             {
51                 result.push((entry.path(), filename));
52             } else {
53                 warn!("Unable to fetch filename for file: {:?}", entry.path());
54             }
55         }
56     }
57     Ok(result)
58 }
59 
60 /// Fetch all zip files in root and put it in sorted Vec<PathBuf>
fetch_zip_files(root: &PathBuf) -> Result<Vec<PathBuf>>61 fn fetch_zip_files(root: &PathBuf) -> Result<Vec<PathBuf>> {
62     // Read all entries in the given root directory
63     // Push path to result if name matches "netsim_artifacts_*.zip"
64     let mut result: Vec<PathBuf> = read_dir(root)?
65         .filter_map(|e| e.ok())
66         .map(|e| e.path())
67         .filter(|path| {
68             path.is_file()
69                 && path.file_name().and_then(|os_name| os_name.to_str()).map_or(false, |filename| {
70                     filename.starts_with("netsim_artifacts_") && filename.ends_with(".zip")
71                 })
72         })
73         .collect();
74     // Sort the zip files by timestamp from oldest to newest
75     result.sort();
76     Ok(result)
77 }
78 
79 /// Remove set number of zip files
remove_zip_files() -> Result<()>80 pub fn remove_zip_files() -> Result<()> {
81     // TODO(b/305012017): Add parameter for keeping some number of zip files
82     // Fetch all zip files in netsimd_temp_dir
83     let zip_files = fetch_zip_files(&netsimd_temp_dir())?;
84     for file in zip_files {
85         if let Err(err) = std::fs::remove_file(&file) {
86             warn!("Removing {file:?} error: {err:?}");
87         }
88     }
89     Ok(())
90 }
91 
92 /// Zip the whole netsimd temp directory and store it in temp directory.
zip_artifacts() -> ZipResult<()>93 pub fn zip_artifacts() -> ZipResult<()> {
94     // Fetch all files in netsimd_temp_dir
95     let root = netsimd_temp_dir();
96     let files = recurse_files(&root)?;
97 
98     // Define PathBuf for zip file
99     let zip_file = root.join(format!("netsim_artifacts_{}.zip", file_current_time()));
100 
101     // Create a new ZipWriter
102     let mut zip_writer = ZipWriter::new(File::create(zip_file)?);
103     let mut buffer = Vec::new();
104 
105     // Put each artifact files into zip file
106     for (file, filename) in files {
107         // Avoid zip files
108         if filename.starts_with("netsim_artifacts") {
109             continue;
110         }
111 
112         // Write to zip file
113         zip_writer.start_file(&filename, FileOptions::default())?;
114         let mut f = File::open(&file)?;
115         f.read_to_end(&mut buffer)?;
116         zip_writer.write_all(&buffer)?;
117         buffer.clear();
118 
119         // Remove the file once written except for netsim log and json files
120         if filename.starts_with("netsim_")
121             && (filename.ends_with(".log") || filename.ends_with(".json"))
122         {
123             continue;
124         }
125         remove_file(file)?;
126     }
127 
128     // Finish writing zip file
129     zip_writer.finish()?;
130     Ok(())
131 }
132 
133 #[cfg(test)]
134 mod tests {
135     use super::*;
136 
test_temp_dir() -> PathBuf137     fn test_temp_dir() -> PathBuf {
138         std::env::temp_dir().join("netsim-test").join(format!("{:?}", std::thread::current()))
139     }
140 
141     #[test]
test_recurse_files()142     fn test_recurse_files() {
143         let path = test_temp_dir();
144 
145         // Create the temporary directory
146         if std::fs::create_dir_all(&path).is_err() {
147             return; // return if test environment disallows file creation
148         }
149 
150         // Create a file
151         let file = path.join("hello.txt");
152         if File::create(&file).is_err() {
153             return; // return if test environment disallows file creation
154         }
155 
156         // Create a folder and a file inside
157         let folder = path.join("folder");
158         std::fs::create_dir_all(&folder).unwrap();
159         let nested_file = folder.join("world.txt");
160         File::create(&nested_file).unwrap();
161 
162         // Recurse Files and check the contents
163         let files_result = recurse_files(&path);
164         assert!(files_result.is_ok());
165         let files = files_result.unwrap();
166         assert_eq!(files.len(), 2);
167         assert!(files.contains(&(file, "hello.txt".to_string())));
168         assert!(files.contains(&(nested_file, "world.txt".to_string())));
169     }
170 
171     #[test]
test_fetch_zip_files()172     fn test_fetch_zip_files() {
173         let path = test_temp_dir();
174 
175         // Create the temporary directory
176         if std::fs::create_dir_all(&path).is_err() {
177             return; // return if test environment disallows file creation
178         }
179 
180         // Create multiple zip files in random order of timestamps
181         let zip_file_1 = path.join("netsim_artifacts_2024-01-01.zip");
182         let zip_file_2 = path.join("netsim_artifacts_2022-12-31.zip");
183         let zip_file_3 = path.join("netsim_artifacts_2023-06-01.zip");
184         let zip_file_faulty = path.join("netsim_arts_2000-01-01.zip");
185         if File::create(&zip_file_1).is_err() {
186             return; // return if test environment disallows file creation
187         }
188         File::create(&zip_file_2).unwrap();
189         File::create(&zip_file_3).unwrap();
190         File::create(zip_file_faulty).unwrap();
191 
192         // Fetch all zip files and check the contents if it is in order
193         let files_result = fetch_zip_files(&path);
194         assert!(files_result.is_ok());
195         let files = files_result.unwrap();
196         assert_eq!(files.len(), 3);
197         assert_eq!(files.first().unwrap(), &zip_file_2);
198         assert_eq!(files.get(1).unwrap(), &zip_file_3);
199         assert_eq!(files.get(2).unwrap(), &zip_file_1);
200     }
201 }
202