Skip to main content

nydus_storage/backend/
localfs.rs

1// Copyright (C) 2020-2021 Alibaba Cloud. All rights reserved.
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Storage backend driver to access blobs on local filesystems.
6
7use std::collections::HashMap;
8use std::fmt;
9use std::fs::{File, OpenOptions};
10use std::io::Result;
11use std::os::unix::io::AsRawFd;
12use std::path::{Path, PathBuf};
13use std::sync::{Arc, RwLock};
14
15use fuse_backend_rs::file_buf::FileVolatileSlice;
16use nix::sys::uio;
17
18use nydus_api::LocalFsConfig;
19use nydus_utils::metrics::BackendMetrics;
20
21use crate::backend::{BackendError, BackendResult, BlobBackend, BlobReader};
22use crate::utils::{readv, MemSliceCursor};
23
24type LocalFsResult<T> = std::result::Result<T, LocalFsError>;
25
26/// Error codes related to localfs storage backend.
27#[derive(Debug)]
28pub enum LocalFsError {
29    BlobFile(String),
30    ReadBlob(String),
31}
32
33impl fmt::Display for LocalFsError {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        match self {
36            LocalFsError::BlobFile(s) => write!(f, "{}", s),
37            LocalFsError::ReadBlob(s) => write!(f, "{}", s),
38        }
39    }
40}
41
42impl From<LocalFsError> for BackendError {
43    fn from(error: LocalFsError) -> Self {
44        BackendError::LocalFs(error)
45    }
46}
47
48struct LocalFsEntry {
49    id: String,
50    file: File,
51    metrics: Arc<BackendMetrics>,
52}
53
54impl BlobReader for LocalFsEntry {
55    fn blob_size(&self) -> BackendResult<u64> {
56        self.file.metadata().map(|v| v.len()).map_err(|e| {
57            let msg = format!("failed to get size of localfs blob {}, {}", self.id, e);
58            LocalFsError::BlobFile(msg).into()
59        })
60    }
61
62    fn try_read(&self, buf: &mut [u8], offset: u64) -> BackendResult<usize> {
63        uio::pread(self.file.as_raw_fd(), buf, offset as i64).map_err(|e| {
64            let msg = format!("failed to read data from blob {}, {}", self.id, e);
65            LocalFsError::ReadBlob(msg).into()
66        })
67    }
68
69    fn readv(
70        &self,
71        bufs: &[FileVolatileSlice],
72        offset: u64,
73        max_size: usize,
74    ) -> BackendResult<usize> {
75        let mut c = MemSliceCursor::new(bufs);
76        let mut iovec = c.consume(max_size);
77
78        readv(self.file.as_raw_fd(), &mut iovec, offset).map_err(|e| {
79            let msg = format!("failed to read data from blob {}, {}", self.id, e);
80            LocalFsError::ReadBlob(msg).into()
81        })
82    }
83
84    fn metrics(&self) -> &BackendMetrics {
85        &self.metrics
86    }
87}
88
89/// Storage backend based on local filesystem.
90#[derive(Default)]
91pub struct LocalFs {
92    // The blob file specified by the user.
93    blob_file: String,
94    // Directory to store blob files. If `blob_file` is not specified, `dir`/`blob_id` will be used
95    // as the blob file name.
96    dir: String,
97    // Alternative directories to store blob files
98    alt_dirs: Vec<String>,
99    // Metrics collector.
100    metrics: Arc<BackendMetrics>,
101    // Hashmap to map blob id to blob file.
102    entries: RwLock<HashMap<String, Arc<LocalFsEntry>>>,
103}
104
105impl LocalFs {
106    pub fn new(config: &LocalFsConfig, id: Option<&str>) -> Result<LocalFs> {
107        let id = id.ok_or_else(|| einval!("LocalFs requires blob_id"))?;
108
109        if config.blob_file.is_empty() && config.dir.is_empty() {
110            return Err(einval!("blob file or dir is required"));
111        }
112
113        Ok(LocalFs {
114            blob_file: config.blob_file.clone(),
115            dir: config.dir.clone(),
116            alt_dirs: config.alt_dirs.clone(),
117            metrics: BackendMetrics::new(id, "localfs"),
118            entries: RwLock::new(HashMap::new()),
119        })
120    }
121
122    // Use the user specified blob file name if available, otherwise generate the file name by
123    // concatenating `dir` and `blob_id`.
124    fn get_blob_path(&self, blob_id: &str) -> LocalFsResult<PathBuf> {
125        let path = if !self.blob_file.is_empty() {
126            Path::new(&self.blob_file).to_path_buf()
127        } else {
128            // Search blob file in dir and additionally in alt_dirs
129            let is_valid = |dir: &PathBuf| -> bool {
130                let blob = Path::new(&dir).join(blob_id);
131                if let Ok(meta) = std::fs::metadata(blob) {
132                    meta.len() != 0
133                } else {
134                    false
135                }
136            };
137
138            let blob = Path::new(&self.dir).join(blob_id);
139            if is_valid(&blob) || self.alt_dirs.is_empty() {
140                blob
141            } else {
142                let mut file = PathBuf::new();
143                for dir in &self.alt_dirs {
144                    file = Path::new(dir).join(blob_id);
145                    if is_valid(&file) {
146                        break;
147                    }
148                }
149                file
150            }
151        };
152
153        path.canonicalize().map_err(|e| {
154            LocalFsError::BlobFile(format!("invalid file path {}, {}", path.display(), e))
155        })
156    }
157
158    #[allow(clippy::mutex_atomic)]
159    fn get_blob(&self, blob_id: &str) -> LocalFsResult<Arc<dyn BlobReader>> {
160        // Don't expect poisoned lock here.
161        if let Some(entry) = self.entries.read().unwrap().get(blob_id) {
162            return Ok(entry.clone());
163        }
164
165        let blob_file_path = self.get_blob_path(blob_id)?;
166        let file = OpenOptions::new()
167            .read(true)
168            .open(&blob_file_path)
169            .map_err(|e| {
170                let msg = format!(
171                    "failed to open blob file {}, {}",
172                    blob_file_path.display(),
173                    e
174                );
175                LocalFsError::BlobFile(msg)
176            })?;
177        // Don't expect poisoned lock here.
178        let mut table_guard = self.entries.write().unwrap();
179        if let Some(entry) = table_guard.get(blob_id) {
180            Ok(entry.clone())
181        } else {
182            let entry = Arc::new(LocalFsEntry {
183                id: blob_id.to_owned(),
184                file,
185                metrics: self.metrics.clone(),
186            });
187            table_guard.insert(blob_id.to_string(), entry.clone());
188            Ok(entry)
189        }
190    }
191}
192
193impl BlobBackend for LocalFs {
194    fn shutdown(&self) {}
195
196    fn metrics(&self) -> &BackendMetrics {
197        &self.metrics
198    }
199
200    fn get_reader(&self, blob_id: &str) -> BackendResult<Arc<dyn BlobReader>> {
201        self.get_blob(blob_id).map_err(|e| e.into())
202    }
203}
204
205impl Drop for LocalFs {
206    fn drop(&mut self) {
207        self.metrics.release().unwrap_or_else(|e| error!("{:?}", e));
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214    use std::io::Write;
215    use std::os::unix::io::{FromRawFd, IntoRawFd};
216    use vmm_sys_util::tempfile::TempFile;
217
218    #[test]
219    fn test_invalid_localfs_new() {
220        let config = LocalFsConfig {
221            blob_file: "".to_string(),
222            dir: "".to_string(),
223            alt_dirs: Vec::new(),
224        };
225        assert!(LocalFs::new(&config, Some("test")).is_err());
226
227        let config = LocalFsConfig {
228            blob_file: "/a/b/c".to_string(),
229            dir: "/a/b".to_string(),
230            alt_dirs: Vec::new(),
231        };
232        assert!(LocalFs::new(&config, None).is_err());
233    }
234
235    #[test]
236    fn test_localfs_get_blob_path() {
237        let config = LocalFsConfig {
238            blob_file: "/a/b/cxxxxxxxxxxxxxxxxxxxxxxx".to_string(),
239            dir: "/a/b".to_string(),
240            alt_dirs: Vec::new(),
241        };
242        let fs = LocalFs::new(&config, Some("test")).unwrap();
243        assert!(fs.get_blob_path("test").is_err());
244
245        let tempfile = TempFile::new().unwrap();
246        let path = tempfile.as_path();
247        let filename = path.file_name().unwrap().to_str().unwrap();
248
249        let config = LocalFsConfig {
250            blob_file: path.to_str().unwrap().to_owned(),
251            dir: path.parent().unwrap().to_str().unwrap().to_owned(),
252            alt_dirs: Vec::new(),
253        };
254        let fs = LocalFs::new(&config, Some("test")).unwrap();
255        assert_eq!(fs.get_blob_path("test").unwrap().to_str(), path.to_str());
256
257        let config = LocalFsConfig {
258            blob_file: "".to_string(),
259            dir: path.parent().unwrap().to_str().unwrap().to_owned(),
260            alt_dirs: Vec::new(),
261        };
262        let fs = LocalFs::new(&config, Some(filename)).unwrap();
263        assert_eq!(fs.get_blob_path(filename).unwrap().to_str(), path.to_str());
264
265        let config = LocalFsConfig {
266            blob_file: "".to_string(),
267            dir: "/a/b".to_string(),
268            alt_dirs: vec![
269                "/test".to_string(),
270                path.parent().unwrap().to_str().unwrap().to_owned(),
271            ],
272        };
273        let fs = LocalFs::new(&config, Some(filename)).unwrap();
274        assert_eq!(fs.get_blob_path(filename).unwrap().to_str(), path.to_str());
275    }
276
277    #[test]
278    fn test_localfs_get_blob() {
279        let tempfile = TempFile::new().unwrap();
280        let path = tempfile.as_path();
281        let filename = path.file_name().unwrap().to_str().unwrap();
282        let config = LocalFsConfig {
283            blob_file: "".to_string(),
284            dir: path.parent().unwrap().to_str().unwrap().to_owned(),
285            alt_dirs: Vec::new(),
286        };
287        let fs = LocalFs::new(&config, Some(filename)).unwrap();
288        let blob1 = fs.get_blob(filename).unwrap();
289        let blob2 = fs.get_blob(filename).unwrap();
290        assert_eq!(Arc::strong_count(&blob1), 3);
291        assert_eq!(Arc::strong_count(&blob2), 3);
292    }
293
294    #[test]
295    fn test_localfs_get_reader() {
296        let tempfile = TempFile::new().unwrap();
297        let path = tempfile.as_path();
298        let filename = path.file_name().unwrap().to_str().unwrap();
299
300        {
301            let mut file = unsafe { File::from_raw_fd(tempfile.as_file().as_raw_fd()) };
302            file.write_all(&[0x1u8, 0x2, 0x3, 0x4]).unwrap();
303            let _ = file.into_raw_fd();
304        }
305
306        let config = LocalFsConfig {
307            blob_file: "".to_string(),
308            dir: path.parent().unwrap().to_str().unwrap().to_owned(),
309            alt_dirs: Vec::new(),
310        };
311        let fs = LocalFs::new(&config, Some(filename)).unwrap();
312        let blob1 = fs.get_reader(filename).unwrap();
313        let blob2 = fs.get_reader(filename).unwrap();
314        assert_eq!(Arc::strong_count(&blob1), 3);
315
316        let mut buf1 = [0x0u8];
317        blob1.read(&mut buf1, 0x0).unwrap();
318        assert_eq!(buf1[0], 0x1);
319
320        let mut buf2 = [0x0u8];
321        let mut buf3 = [0x0u8];
322        let bufs = [
323            unsafe { FileVolatileSlice::from_raw_ptr(buf2.as_mut_ptr(), buf2.len()) },
324            unsafe { FileVolatileSlice::from_raw_ptr(buf3.as_mut_ptr(), buf3.len()) },
325        ];
326
327        assert_eq!(blob2.readv(&bufs, 0x1, 2).unwrap(), 2);
328        assert_eq!(buf2[0], 0x2);
329        assert_eq!(buf3[0], 0x3);
330
331        assert_eq!(blob2.readv(&bufs, 0x3, 3).unwrap(), 1);
332        assert_eq!(buf2[0], 0x4);
333        assert_eq!(buf3[0], 0x3);
334
335        assert_eq!(blob2.blob_size().unwrap(), 4);
336        let blob4 = fs.get_blob(filename).unwrap();
337        assert_eq!(blob4.blob_size().unwrap(), 4);
338    }
339}