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}