box_format/file/
writer.rs

1use std::collections::HashMap;
2use std::default::Default;
3use std::fs::File;
4use std::fs::OpenOptions;
5use std::io::{prelude::*, BufReader, BufWriter, Result, SeekFrom};
6use std::num::NonZeroU64;
7use std::path::{Path, PathBuf};
8
9use memmap2::{Mmap, MmapOptions};
10
11use crate::{
12    compression::Compression,
13    header::BoxHeader,
14    path::BoxPath,
15    record::{DirectoryRecord, FileRecord, LinkRecord, Record},
16    ser::Serialize,
17};
18
19use super::{
20    reader::{read_header, read_trailer},
21    BoxMetadata,
22};
23
24pub struct BoxFileWriter {
25    pub(crate) file: BufWriter<File>,
26    pub(crate) path: PathBuf,
27    pub(crate) header: BoxHeader,
28    pub(crate) meta: BoxMetadata,
29}
30
31impl Drop for BoxFileWriter {
32    fn drop(&mut self) {
33        let _ = self.finish_inner();
34    }
35}
36
37impl BoxFileWriter {
38    #[inline(always)]
39    fn write_header(&mut self) -> std::io::Result<()> {
40        self.file.seek(SeekFrom::Start(0))?;
41        self.header.write(&mut self.file)
42    }
43
44    #[inline(always)]
45    fn finish_inner(&mut self) -> std::io::Result<u64> {
46        let pos = self.next_write_addr().get();
47        self.header.trailer = NonZeroU64::new(pos);
48        self.write_header()?;
49        self.file.seek(SeekFrom::Start(pos))?;
50        self.meta.write(&mut self.file)?;
51
52        let new_pos = self.file.seek(SeekFrom::Current(0))?;
53        let file = self.file.get_mut();
54        file.set_len(new_pos)?;
55        Ok(new_pos)
56    }
57
58    pub fn finish(mut self) -> std::io::Result<u64> {
59        self.finish_inner()
60    }
61
62    #[inline(always)]
63    fn next_write_addr(&self) -> NonZeroU64 {
64        // TODO: this is probably slow as hell
65        let offset = self
66            .meta
67            .inodes
68            .iter()
69            .rev()
70            .find_map(|r| r.as_file())
71            .map(|r| r.data.get() + r.length)
72            .unwrap_or(std::mem::size_of::<BoxHeader>() as u64);
73
74        let v = match self.header.alignment {
75            0 => offset,
76            alignment => {
77                let diff = offset % alignment;
78                if diff == 0 {
79                    offset
80                } else {
81                    offset + (alignment - diff)
82                }
83            }
84        };
85
86        NonZeroU64::new(v).unwrap()
87    }
88
89    /// This will open an existing `.box` file for writing, and error if the file is not valid.
90    pub fn open<P: AsRef<Path>>(path: P) -> std::io::Result<BoxFileWriter> {
91        OpenOptions::new()
92            .read(true)
93            .write(true)
94            .open(path.as_ref())
95            .map(|mut file| {
96                // Try to load the header so we can easily rewrite it when saving.
97                // If header is invalid, we're not even loading a .box file.
98                let (header, meta) = {
99                    let mut reader = BufReader::new(&mut file);
100                    let header = read_header(&mut reader, 0)?;
101                    let ptr = header.trailer.ok_or_else(|| {
102                        std::io::Error::new(std::io::ErrorKind::Other, "no trailer found")
103                    })?;
104                    let meta = read_trailer(&mut reader, ptr, path.as_ref(), 0)?;
105                    (header, meta)
106                };
107
108                let f = BoxFileWriter {
109                    file: BufWriter::new(file),
110                    path: path.as_ref().to_path_buf().canonicalize()?,
111                    header,
112                    meta,
113                };
114
115                Ok(f)
116            })?
117    }
118
119    /// This will create a new `.box` file for writing, and error if the file already exists.
120    pub fn create<P: AsRef<Path>>(path: P) -> std::io::Result<BoxFileWriter> {
121        let mut boxfile = OpenOptions::new()
122            .write(true)
123            .read(true)
124            .create_new(true)
125            .open(path.as_ref())
126            .and_then(|file| {
127                Ok(BoxFileWriter {
128                    file: BufWriter::new(file),
129                    path: path.as_ref().to_path_buf().canonicalize()?,
130                    header: BoxHeader::default(),
131                    meta: BoxMetadata::default(),
132                })
133            })?;
134
135        boxfile.write_header()?;
136
137        Ok(boxfile)
138    }
139
140    /// This will create a new `.box` file for reading and writing, and error if the file already exists.
141    /// Will insert byte-aligned values based on provided `alignment` value. For best results, consider a power of 2.
142    pub fn create_with_alignment<P: AsRef<Path>>(
143        path: P,
144        alignment: u64,
145    ) -> std::io::Result<BoxFileWriter> {
146        let mut boxfile = OpenOptions::new()
147            .write(true)
148            .read(true)
149            .create_new(true)
150            .open(path.as_ref())
151            .and_then(|file| {
152                Ok(BoxFileWriter {
153                    file: BufWriter::new(file),
154                    path: path.as_ref().to_path_buf().canonicalize()?,
155                    header: BoxHeader::with_alignment(alignment),
156                    meta: BoxMetadata::default(),
157                })
158            })?;
159
160        boxfile.write_header()?;
161
162        Ok(boxfile)
163    }
164
165    pub fn path(&self) -> &Path {
166        &self.path
167    }
168
169    pub fn alignment(&self) -> u64 {
170        self.header.alignment
171    }
172
173    pub fn version(&self) -> u32 {
174        self.header.version
175    }
176
177    /// Will return the metadata for the `.box` if it has been provided.
178    pub fn metadata(&self) -> &BoxMetadata {
179        &self.meta
180    }
181
182    #[inline(always)]
183    fn iter(&self) -> super::meta::Records {
184        super::meta::Records::new(self.metadata(), &*self.metadata().root, None)
185    }
186
187    #[inline(always)]
188    fn insert_inner<F>(&mut self, path: BoxPath, create_record: F) -> std::io::Result<()>
189    where
190        F: FnOnce(&mut Self, &BoxPath) -> std::io::Result<Record>,
191    {
192        log::debug!("insert_inner path: {:?}", path);
193        match path.parent() {
194            Some(parent) => {
195                log::debug!("insert_inner parent: {:?}", parent);
196
197                match self.meta.inode(&parent) {
198                    None => {
199                        let err = std::io::Error::new(
200                            std::io::ErrorKind::Other,
201                            format!("No inode found for path: {:?}", parent),
202                        );
203                        Err(err)
204                    }
205                    Some(parent) => {
206                        let record = create_record(self, &path)?;
207                        log::debug!("Inserting record into parent {:?}: {:?}", &parent, &record);
208                        let new_inode = self.meta.insert_record(record);
209                        log::debug!("Inserted with inode: {:?}", &new_inode);
210                        let parent = self
211                            .meta
212                            .record_mut(parent)
213                            .unwrap()
214                            .as_directory_mut()
215                            .unwrap();
216                        parent.inodes.push(new_inode);
217                        Ok(())
218                    }
219                }
220            }
221            None => {
222                let record = create_record(self, &path)?;
223                log::debug!("Inserting record into root: {:?}", &record);
224                let new_inode = self.meta.insert_record(record);
225                self.meta.root.push(new_inode);
226                Ok(())
227            }
228        }
229    }
230
231    pub fn mkdir(&mut self, path: BoxPath, attrs: HashMap<String, Vec<u8>>) -> std::io::Result<()> {
232        log::debug!("mkdir: {}", path);
233
234        self.insert_inner(path, move |this, path| {
235            let attrs = attrs
236                .into_iter()
237                .map(|(k, v)| {
238                    let k = this.meta.attr_key_or_create(&k);
239                    (k, v)
240                })
241                .collect::<HashMap<_, _>>();
242
243            let dir_record = DirectoryRecord {
244                name: path.filename(),
245                inodes: vec![],
246                attrs,
247            };
248
249            Ok(dir_record.upcast())
250        })
251    }
252
253    pub fn link(
254        &mut self,
255        path: BoxPath,
256        target: BoxPath,
257        attrs: HashMap<String, Vec<u8>>,
258    ) -> std::io::Result<()> {
259        self.insert_inner(path, move |this, path| {
260            let attrs = attrs
261                .into_iter()
262                .map(|(k, v)| {
263                    let k = this.meta.attr_key_or_create(&k);
264                    (k, v)
265                })
266                .collect::<HashMap<_, _>>();
267
268            let link_record = LinkRecord {
269                name: path.filename(),
270                target,
271                attrs,
272            };
273
274            Ok(link_record.upcast())
275        })
276    }
277
278    pub fn insert<R: Read>(
279        &mut self,
280        compression: Compression,
281        path: BoxPath,
282        value: &mut R,
283        attrs: HashMap<String, Vec<u8>>,
284    ) -> std::io::Result<&FileRecord> {
285        self.insert_inner(path, move |this, path| {
286            let next_addr = this.next_write_addr();
287            let byte_count = this.write_data::<R>(compression, next_addr.get(), value)?;
288            let attrs = attrs
289                .into_iter()
290                .map(|(k, v)| {
291                    let k = this.meta.attr_key_or_create(&k);
292                    (k, v)
293                })
294                .collect::<HashMap<_, _>>();
295
296            let record = FileRecord {
297                compression,
298                length: byte_count.write,
299                decompressed_length: byte_count.read,
300                name: path.filename(),
301                data: next_addr,
302                attrs,
303            };
304
305            Ok(record.upcast())
306        })?;
307
308        Ok(self.meta.inodes.last().unwrap().as_file().unwrap())
309    }
310
311    /// # Safety
312    ///
313    /// Use of memory maps is unsafe as modifications to the file could affect the operation
314    /// of the application. Ensure that the Box being operated on is not mutated while a memory
315    /// map is in use.
316    pub unsafe fn data(&self, record: &FileRecord) -> std::io::Result<Mmap> {
317        self.read_data(record)
318    }
319
320    #[inline(always)]
321    fn write_data<R: Read>(
322        &mut self,
323        compression: Compression,
324        pos: u64,
325        reader: &mut R,
326    ) -> std::io::Result<comde::com::ByteCount> {
327        self.file.seek(SeekFrom::Start(pos))?;
328        compression.compress(&mut self.file, reader)
329    }
330
331    pub fn set_attr<S: AsRef<str>>(
332        &mut self,
333        path: &BoxPath,
334        key: S,
335        value: Vec<u8>,
336    ) -> Result<()> {
337        let inode = match self.iter().find(|r| &r.path == path) {
338            Some(v) => v.inode,
339            None => todo!(),
340        };
341
342        let key = self.meta.attr_key_or_create(key.as_ref());
343        let record = self.meta.record_mut(inode).unwrap();
344        record.attrs_mut().insert(key, value);
345
346        Ok(())
347    }
348
349    pub fn set_file_attr<S: AsRef<str>>(&mut self, key: S, value: Vec<u8>) -> Result<()> {
350        let key = self.meta.attr_key_or_create(key.as_ref());
351
352        self.meta.attrs.insert(key, value);
353
354        Ok(())
355    }
356
357    #[inline(always)]
358    unsafe fn read_data(&self, header: &FileRecord) -> std::io::Result<Mmap> {
359        MmapOptions::new()
360            .offset(header.data.get())
361            .len(header.length as usize)
362            .map(self.file.get_ref())
363    }
364}