nuts_archive/
lib.rs

1// MIT License
2//
3// Copyright (c) 2023,2024 Robin Doer
4//
5// Permission is hereby granted, free of charge, to any person obtaining a copy
6// of this software and associated documentation files (the "Software"), to
7// deal in the Software without restriction, including without limitation the
8// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
9// sell copies of the Software, and to permit persons to whom the Software is
10// furnished to do so, subject to the following conditions:
11//
12// The above copyright notice and this permission notice shall be included in
13// all copies or substantial portions of the Software.
14//
15// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21// IN THE SOFTWARE.
22
23//! A storage application inspired by the `tar` tool.
24//!
25//! The archive is an application based on the [nuts container]. Inspired by
26//! the `tar` tool you can store files, directories and symlinks in a
27//! [nuts container].
28//!
29//! * Entries can be appended at the end of the archive.
30//! * They cannot be removed from the archive.
31//! * You can travere the archive from the first to the last entry in the
32//!   archive.
33//!
34//! ## Create a new archive
35//!
36//! ```rust
37//! use nuts_archive::ArchiveFactory;
38//! use nuts_container::{Cipher, Container, CreateOptionsBuilder};
39//! use nuts_directory::{CreateOptions, DirectoryBackend};
40//! use tempfile::{Builder, TempDir};
41//!
42//! // Let's create an archive service (with a directory backend) in a temporary directory
43//! let tmp_dir = Builder::new().prefix("nuts-archive").tempdir().unwrap();
44//! let backend_options = CreateOptions::for_path(tmp_dir);
45//! let container_options = CreateOptionsBuilder::new(Cipher::Aes128Gcm)
46//!     .with_password_callback(|| Ok(b"123".to_vec()))
47//!     .build::<DirectoryBackend<TempDir>>()
48//!     .unwrap();
49//! let container = Container::create(backend_options, container_options).unwrap();
50//! let archive = Container::create_service::<ArchiveFactory>(container).unwrap();
51//!
52//! // Fetch some information
53//! let info = archive.info();
54//! assert_eq!(info.blocks, 0);
55//! assert_eq!(info.files, 0);
56//! ```
57//!
58//! ## Open an existing archive
59//! ```rust
60//! use nuts_archive::ArchiveFactory;
61//! use nuts_container::{Cipher, Container, CreateOptionsBuilder, OpenOptionsBuilder};
62//! use nuts_directory::{CreateOptions, DirectoryBackend, OpenOptions};
63//! use tempfile::{Builder, TempDir};
64//!
65//! let dir = Builder::new().prefix("nuts-archive").tempdir().unwrap();
66//!
67//! {
68//!     // This will create an empty archive in a temporary directory.
69//!
70//!     let backend_options = CreateOptions::for_path(dir.path().to_owned());
71//!     let container_options = CreateOptionsBuilder::new(Cipher::Aes128Gcm)
72//!         .with_password_callback(|| Ok(b"123".to_vec()))
73//!         .build::<DirectoryBackend<&TempDir>>()
74//!         .unwrap();
75//!     let container = Container::create(backend_options, container_options).unwrap();
76//!
77//!     Container::create_service::<ArchiveFactory>(container).unwrap();
78//! }
79//!
80//! // Open the archive service (with a directory backend) from the temporary directory.
81//! let backend_options = OpenOptions::for_path(dir);
82//! let container_options = OpenOptionsBuilder::new()
83//!     .with_password_callback(|| Ok(b"123".to_vec()))
84//!     .build::<DirectoryBackend<TempDir>>()
85//!     .unwrap();
86//! let container = Container::open(backend_options, container_options).unwrap();
87//!
88//! let archive = Container::open_service::<ArchiveFactory>(container, false).unwrap();
89//!
90//! // Fetch some information
91//! let info = archive.info();
92//! assert_eq!(info.blocks, 0);
93//! assert_eq!(info.files, 0);
94//! ```
95//!
96//! ## Append an entry at the end of the archive
97//!
98//! ```rust
99//! use nuts_archive::ArchiveFactory;
100//! use nuts_container::{Cipher, Container, CreateOptionsBuilder, OpenOptionsBuilder};
101//! use nuts_directory::{CreateOptions, DirectoryBackend, OpenOptions};
102//! use tempfile::{Builder, TempDir};
103//!
104//! let dir = Builder::new().prefix("nuts-archive").tempdir().unwrap();
105//!
106//! {
107//!     // This will create an empty archive in a temporary directory.
108//!
109//!     let backend_options = CreateOptions::for_path(dir.path().to_owned());
110//!     let container_options = CreateOptionsBuilder::new(Cipher::Aes128Gcm)
111//!         .with_password_callback(|| Ok(b"123".to_vec()))
112//!         .build::<DirectoryBackend<&TempDir>>()
113//!         .unwrap();
114//!     let container = Container::create(backend_options, container_options).unwrap();
115//!
116//!     Container::create_service::<ArchiveFactory>(container).unwrap();
117//! }
118//!
119//! // Open the archive (with a directory backend) from the temporary directory.
120//! let backend_options = OpenOptions::for_path(dir);
121//! let container_options = OpenOptionsBuilder::new()
122//!     .with_password_callback(|| Ok(b"123".to_vec()))
123//!     .build::<DirectoryBackend<TempDir>>()
124//!     .unwrap();
125//! let container = Container::open(backend_options, container_options).unwrap();
126//!
127//! let mut archive = Container::open_service::<ArchiveFactory>(container, false).unwrap();
128//!
129//! // Append a new file entry
130//! let mut entry = archive.append_file("sample file").build().unwrap();
131//! entry.write_all("some sample data".as_bytes()).unwrap();
132//!
133//! // Append a new directory entry
134//! archive
135//!     .append_directory("sample directory")
136//!     .build()
137//!     .unwrap();
138//!
139//! // Append a new symlink entry
140//! archive
141//!     .append_symlink("sample symlink", "target")
142//!     .build()
143//!     .unwrap();
144//! ```
145//!
146//! ## Loop through all entries in the archive
147//!
148//! ```rust
149//! use nuts_archive::ArchiveFactory;
150//! use nuts_container::{Cipher, Container, CreateOptionsBuilder, OpenOptionsBuilder};
151//! use nuts_directory::{CreateOptions, DirectoryBackend, OpenOptions};
152//! use tempfile::{Builder, TempDir};
153//!
154//! let dir = Builder::new().prefix("nuts-archive").tempdir().unwrap();
155//!
156//! {
157//!     // This will create an empty archive in a temporary directory.
158//!
159//!     let backend_options = CreateOptions::for_path(dir.path().to_owned());
160//!     let container_options = CreateOptionsBuilder::new(Cipher::Aes128Gcm)
161//!         .with_password_callback(|| Ok(b"123".to_vec()))
162//!         .build::<DirectoryBackend<&TempDir>>()
163//!         .unwrap();
164//!     let container = Container::create(backend_options, container_options).unwrap();
165//!
166//!     Container::create_service::<ArchiveFactory>(container).unwrap();
167//! }
168//!
169//! // Open the archive (with a directory backend) from the temporary directory.
170//! let backend_options = OpenOptions::for_path(dir);
171//! let container_options = OpenOptionsBuilder::new()
172//!     .with_password_callback(|| Ok(b"123".to_vec()))
173//!     .build::<DirectoryBackend<TempDir>>()
174//!     .unwrap();
175//! let container = Container::open(backend_options, container_options).unwrap();
176//!
177//! // Open the archive and append some entries
178//! let mut archive = Container::open_service::<ArchiveFactory>(container, false).unwrap();
179//!
180//! archive.append_file("f1").build().unwrap();
181//! archive.append_directory("f2").build().unwrap();
182//! archive.append_symlink("f3", "target").build().unwrap();
183//!
184//! // Go through the archive
185//! let entry = archive.first().unwrap().unwrap();
186//! assert!(entry.is_file());
187//! assert_eq!(entry.name(), "f1");
188//!
189//! let entry = entry.next().unwrap().unwrap();
190//! assert!(entry.is_directory());
191//! assert_eq!(entry.name(), "f2");
192//!
193//! let entry = entry.next().unwrap().unwrap();
194//! assert!(entry.is_symlink());
195//! assert_eq!(entry.name(), "f3");
196//!
197//! assert!(entry.next().is_none());
198//! ```
199//!
200//! [nuts container]: nuts_container
201
202mod datetime;
203mod entry;
204mod error;
205mod header;
206mod id;
207mod magic;
208mod migration;
209mod pager;
210#[cfg(test)]
211mod tests;
212mod tree;
213
214use chrono::{DateTime, Utc};
215use id::Id;
216use log::debug;
217use nuts_backend::Backend;
218use nuts_bytes::PutBytesError;
219use nuts_container::{Container, Service, ServiceFactory};
220use std::convert::TryInto;
221
222pub use entry::immut::{DirectoryEntry, Entry, FileEntry, SymlinkEntry};
223pub use entry::mode::Group;
224pub use entry::r#mut::{DirectoryBuilder, EntryMut, FileBuilder, SymlinkBuilder};
225pub use error::{ArchiveResult, Error};
226
227use crate::entry::immut::InnerEntry;
228use crate::header::Header;
229use crate::migration::Migration;
230use crate::pager::Pager;
231use crate::tree::Tree;
232
233const SID: u32 = 0x61 << 24 | 0x72 << 16 | 0x63 << 8 | 0x68; // "arch"
234
235fn flush_header<B: Backend>(
236    pager: &mut Pager<B>,
237    id: &Id<B>,
238    header: &Header,
239    tree: &Tree<B>,
240) -> ArchiveResult<(), B> {
241    fn inner<B: Backend>(
242        pager: &mut Pager<B>,
243        header: &Header,
244        tree: &Tree<B>,
245    ) -> Result<usize, nuts_bytes::Error> {
246        let mut writer = pager.create_writer();
247        let mut n = 0;
248
249        n += writer.write(header)?;
250        n += writer.write(tree)?;
251
252        Ok(n)
253    }
254
255    match inner(pager, header, tree) {
256        Ok(n) => {
257            pager.write_buf(id)?;
258
259            debug!("{} bytes written into header at {}", n, id);
260
261            Ok(())
262        }
263        Err(err) => {
264            let err: Error<B> = match err {
265                nuts_bytes::Error::PutBytes(PutBytesError::NoSpace) => Error::InvalidBlockSize,
266                _ => err.into(),
267            };
268            Err(err)
269        }
270    }
271}
272
273/// Information/statistics from the archive.
274#[derive(Debug)]
275pub struct Info {
276    /// Time when the archive was created
277    pub created: DateTime<Utc>,
278
279    /// Time when the last entry was appended
280    pub modified: DateTime<Utc>,
281
282    /// Number of blocks allocated for the archive
283    pub blocks: u64,
284
285    /// Number of files stored in the archive
286    pub files: u64,
287}
288
289/// The archive.
290pub struct Archive<B: Backend> {
291    pager: Pager<B>,
292    header_id: Id<B>,
293    header: Header,
294    tree: Tree<B>,
295}
296
297impl<B: Backend> Archive<B> {
298    /// Fetches statistics/information from the archive.
299    pub fn info(&self) -> Info {
300        Info {
301            created: self.header.created,
302            modified: self.header.modified,
303            blocks: self.tree.nblocks(),
304            files: self.header.nfiles,
305        }
306    }
307
308    /// Returns the first entry in the archive.
309    ///
310    /// Next, you can use [`Entry::next()`] to traverse through the archive.
311    ///
312    /// If the archive is empty, [`None`] is returned.
313    pub fn first(&mut self) -> Option<ArchiveResult<Entry<B>, B>> {
314        match InnerEntry::first(&mut self.pager, &mut self.tree) {
315            Some(Ok(inner)) => Some(inner.try_into()),
316            Some(Err(err)) => Some(Err(err)),
317            None => None,
318        }
319    }
320
321    /// Searches for an entry with the given `name`.
322    ///
323    /// It scans the whole archive and returns the first entry which has the
324    /// given name wrapped into a [`Some`]. If no such entry exists, [`None`]
325    /// is returned.
326    pub fn lookup<N: AsRef<str>>(&mut self, name: N) -> Option<ArchiveResult<Entry<B>, B>> {
327        let mut entry_opt = self.first();
328
329        loop {
330            match entry_opt {
331                Some(Ok(entry)) => {
332                    if entry.name() == name.as_ref() {
333                        return Some(Ok(entry));
334                    }
335
336                    entry_opt = entry.next();
337                }
338                Some(Err(err)) => return Some(Err(err)),
339                None => break,
340            }
341        }
342
343        None
344    }
345
346    /// Appends a new file entry with the given `name` at the end of the
347    /// archive.
348    ///
349    /// The method returns a [`FileBuilder`] instance, where you are able to
350    /// set some more properties for the new entry. Calling
351    /// [`FileBuilder::build()`] will finally create the entry.
352    pub fn append_file<N: AsRef<str>>(&mut self, name: N) -> FileBuilder<B> {
353        FileBuilder::new(
354            &mut self.pager,
355            &self.header_id,
356            &mut self.header,
357            &mut self.tree,
358            name.as_ref().to_string(),
359        )
360    }
361
362    /// Appends a new directory entry with the given `name` at the end of the
363    /// archive.
364    ///
365    /// The method returns a [`DirectoryBuilder`] instance, where you are able
366    /// to set some more properties for the new entry. Calling
367    /// [`DirectoryBuilder::build()`] will finally create the entry.
368    pub fn append_directory<N: AsRef<str>>(&mut self, name: N) -> DirectoryBuilder<B> {
369        DirectoryBuilder::new(
370            &mut self.pager,
371            &self.header_id,
372            &mut self.header,
373            &mut self.tree,
374            name.as_ref().to_string(),
375        )
376    }
377
378    /// Appends a new symlink entry with the given `name` at the end of the
379    /// archive.
380    ///
381    /// The symlink points to the given `target` name.
382    ///
383    /// The method returns a [`SymlinkBuilder`] instance, where you are able to
384    /// set some more properties for the new entry. Calling
385    /// [`SymlinkBuilder::build()`] will finally create the entry.
386    pub fn append_symlink<N: AsRef<str>, T: AsRef<str>>(
387        &mut self,
388        name: N,
389        target: T,
390    ) -> SymlinkBuilder<B> {
391        SymlinkBuilder::new(
392            &mut self.pager,
393            &self.header_id,
394            &mut self.header,
395            &mut self.tree,
396            name.as_ref().to_string(),
397            target.as_ref().to_string(),
398        )
399    }
400
401    /// Consumes this `Archive`, returning the underlying [`Container`].
402    pub fn into_container(self) -> Container<B> {
403        self.pager.into_container()
404    }
405}
406
407impl<B: Backend + 'static> Service<B> for Archive<B> {
408    type Migration = Migration<B>;
409
410    fn sid() -> u32 {
411        SID
412    }
413
414    fn need_top_id() -> bool {
415        true
416    }
417
418    fn migration() -> Migration<B> {
419        Migration::default()
420    }
421}
422
423#[derive(Default)]
424pub struct ArchiveFactory;
425
426impl<B: Backend + 'static> ServiceFactory<B> for ArchiveFactory {
427    type Service = Archive<B>;
428    type Err = Error<B>;
429
430    fn create(container: Container<B>) -> Result<Self::Service, Self::Err> {
431        let mut pager = Pager::new(container);
432        let top_id = pager.top_id_or_err()?;
433
434        let header = Header::create();
435        let tree = Tree::<B>::new();
436
437        flush_header(&mut pager, &top_id, &header, &tree)?;
438
439        let archive = Archive {
440            pager,
441            header_id: top_id,
442            header,
443            tree,
444        };
445
446        debug!("archive created, header: {}", archive.header_id);
447
448        Ok(archive)
449    }
450
451    fn open(container: Container<B>) -> Result<Self::Service, Self::Err> {
452        let mut pager = Pager::new(container);
453        let top_id = pager.top_id_or_err()?;
454
455        let mut reader = pager.read_buf(&top_id)?;
456        let header = reader.read::<Header>()?;
457
458        header.validate_revision()?;
459
460        let tree = reader.read::<Tree<B>>()?;
461
462        let archive = Archive {
463            pager,
464            header_id: top_id,
465            header,
466            tree,
467        };
468
469        debug!("archive opened, header: {}", archive.header_id);
470
471        Ok(archive)
472    }
473}
474
475impl<B: Backend> AsRef<Container<B>> for Archive<B> {
476    fn as_ref(&self) -> &Container<B> {
477        &self.pager
478    }
479}