nuts_directory/
lib.rs

1// MIT License
2//
3// Copyright (c) 2022-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//! Nuts backend implementation where the blocks of the container are stored
24//! in a file hierarchy.
25//!
26//! # Introduction
27//!
28//! The _nuts-directory_ crate implements a [nuts] backend where the blocks of
29//! the container are stored in a file hierarchy. Each block is identified by
30//! an [id](Id), which is basically a 16 byte random number.
31//!
32//! When storing a block to disks the path to the file is derived from the id:
33//!
34//! 1. The id is converted into a hex string.
35//! 2. The path then would be:
36//!    `<first two chars>/<next two chars>/<remaining chars>`
37//!
38//! The header of the container is stored in the file
39//! `00/00/0000000000000000000000000000`.
40//!
41//! # Create a new backend instance
42//!
43//! The [`CreateOptions`] type is used to create a new backend instance, which
44//! is passed to the [`Container::create`] method. You need at least a
45//! directory where the backend put its blocks. See the [`CreateOptions`]
46//! documentation for further options.
47//!
48//! # Open an existing backend
49//!
50//! The [`OpenOptions`] type is used to open a backend instance, which is
51//! passed to the [`Container::open`] method. You need the directory where the
52//! backend put its blocks.
53//!
54//! [nuts]: https://crates.io/crates/nuts-container
55//! [`Container::create`]: https://docs.rs/nuts-container/latest/nuts_container/container/struct.Container.html#method.create
56//! [`Container::open`]: https://docs.rs/nuts-container/latest/nuts_container/container/struct.Container.html#method.open
57
58mod error;
59mod id;
60mod info;
61mod options;
62
63use log::{error, warn};
64use nuts_backend::{Backend, ReceiveHeader, HEADER_MAX_SIZE};
65use std::io::{self, ErrorKind, Read, Write};
66use std::path::Path;
67use std::{cmp, fs};
68
69pub use error::Error;
70pub use id::Id;
71pub use info::Info;
72pub use options::{CreateOptions, OpenOptions, Settings};
73
74use crate::error::Result;
75
76fn read_block(path: &Path, id: &Id, bsize: u32, buf: &mut [u8]) -> Result<usize> {
77    let path = id.to_pathbuf(path);
78    let mut fh = fs::OpenOptions::new().read(true).open(path)?;
79
80    let len = cmp::min(buf.len(), bsize as usize);
81    let target = &mut buf[..len];
82
83    fh.read_exact(target)?;
84
85    Ok(len)
86}
87
88fn write_block(
89    path: &Path,
90    id: &Id,
91    aquire: bool,
92    header: bool,
93    bsize: u32,
94    buf: &[u8],
95) -> Result<usize> {
96    let path = id.to_pathbuf(path);
97
98    if let Some(dir) = path.parent() {
99        fs::create_dir_all(dir)?;
100    }
101
102    if aquire {
103        // A block is aquired. Allow only to create non-existing files.
104        if path.exists() {
105            return Err(io::Error::new(
106                ErrorKind::Other,
107                format!("cannot aquire {}, already stored in {}", id, path.display()),
108            )
109            .into());
110        }
111    } else {
112        // * The header block can be created even if it does not exist.
113        // * Any other block must be aquired before, thus open should fail if the
114        //   file does not exist.
115        if !header && !path.is_file() {
116            return Err(io::Error::new(
117                ErrorKind::Other,
118                format!("cannot open {}, no related file {}", id, path.display()),
119            )
120            .into());
121        }
122    }
123
124    let tmp_path = path.with_extension("tmp");
125
126    let mut fh = fs::OpenOptions::new()
127        .write(true)
128        .create_new(true)
129        .open(&tmp_path)?;
130
131    let len = cmp::min(buf.len(), bsize as usize);
132    let pad_len = bsize as usize - len;
133
134    fh.write_all(&buf[..len])?;
135    fh.write_all(&vec![0; pad_len])?;
136    fh.flush()?;
137
138    fs::rename(tmp_path, path)?;
139
140    Ok(len)
141}
142
143fn read_header(path: &Path, buf: &mut [u8]) -> Result<()> {
144    read_block(path, &Id::min(), HEADER_MAX_SIZE as u32, buf).map(|_| ())
145}
146
147fn write_header(path: &Path, bsize: u32, buf: &[u8]) -> Result<()> {
148    write_block(path, &Id::min(), false, true, bsize, buf).map(|_| ())
149}
150
151#[derive(Debug)]
152pub struct DirectoryBackend<P: AsRef<Path>> {
153    bsize: u32,
154    path: P,
155}
156
157impl<P: AsRef<Path>> ReceiveHeader<Self> for DirectoryBackend<P> {
158    fn get_header_bytes(&mut self, bytes: &mut [u8; HEADER_MAX_SIZE]) -> Result<()> {
159        read_header(self.path.as_ref(), bytes)
160    }
161}
162
163impl<P: AsRef<Path>> Backend for DirectoryBackend<P> {
164    type Settings = Settings;
165    type Err = Error;
166    type Id = Id;
167    type Info = Info;
168
169    fn info(&self) -> Result<Info> {
170        Ok(Info { bsize: self.bsize })
171    }
172
173    fn block_size(&self) -> u32 {
174        self.bsize
175    }
176
177    fn aquire(&mut self, buf: &[u8]) -> Result<Self::Id> {
178        const MAX: u8 = 3;
179
180        for n in 0..MAX {
181            let id = Id::generate()?;
182
183            match write_block(self.path.as_ref(), &id, true, false, self.bsize, buf) {
184                Ok(_) => return Ok(id),
185                Err(Error::Io(err)) => {
186                    if err.kind() == ErrorKind::AlreadyExists {
187                        warn!("Id {} already exists try again ({}/{})", id, n + 1, MAX);
188                    } else {
189                        return Err(err.into());
190                    }
191                }
192                Err(err) => return Err(err),
193            };
194        }
195
196        Err(Error::UniqueId)
197    }
198
199    fn release(&mut self, id: Self::Id) -> Result<()> {
200        let path = id.to_pathbuf(self.path.as_ref());
201
202        Ok(fs::remove_file(path)?)
203    }
204
205    fn read(&mut self, id: &Id, buf: &mut [u8]) -> Result<usize> {
206        read_block(self.path.as_ref(), id, self.bsize, buf)
207    }
208
209    fn write(&mut self, id: &Id, buf: &[u8]) -> Result<usize> {
210        write_block(self.path.as_ref(), id, false, false, self.bsize, buf)
211    }
212
213    fn write_header(&mut self, buf: &[u8; HEADER_MAX_SIZE]) -> Result<()> {
214        write_header(self.path.as_ref(), self.bsize, buf)
215    }
216
217    fn delete(self) {
218        if let Err(err) = fs::remove_dir_all(self.path) {
219            error!("failed to delete backend instance: {}", err);
220        }
221    }
222}