Skip to main content

harmos_storage/
frozen_file.rs

1/* ***
2 *
3 * Harmos
4 *
5 * SPDX-License-Identifier: Apache-2.0
6 *
7 * ***
8 */
9
10// Freeze An Opened Source Into One Private Immutable File.
11
12/* === Modules === */
13
14/* === Re-Exports === */
15
16/* === Imports === */
17
18// Standard Library Imports
19use std::{
20    fs::File,
21    io::Write,
22    sync::{Arc, Mutex},
23};
24
25// Third Party Imports
26use sha2::{Digest, Sha256};
27
28// Local Imports
29use crate::local::io_error;
30use crate::{ByteRange, Result, StorageManager};
31
32/* === Constants === */
33
34/* === Macros === */
35
36/* === Type Aliases === */
37
38/* === Structs === */
39
40/// A private immutable copy of an opened file range, retained independently of its source.
41#[derive(Clone)]
42pub struct FrozenFile {
43    backing: Arc<Backing>,
44    length: u64,
45    sha256: [u8; 32],
46    pub(crate) session: StorageManager<crate::ErasedCatalog>,
47}
48
49struct Backing {
50    file: Mutex<File>,
51}
52
53/* === Enums === */
54
55/* === Traits === */
56
57/* === Implementations === */
58
59impl FrozenFile {
60    pub fn length(&self) -> u64 {
61        self.length
62    }
63
64    pub fn sha256(&self) -> &[u8; 32] {
65        &self.sha256
66    }
67
68    /// Reads the immutable snapshot through its originating storage manager.
69    /// The reader owns its file handle and remains valid after this snapshot is dropped.
70    pub async fn read(&self, range: ByteRange) -> Result<crate::FileReader> {
71        let file = self.opened_file()?;
72        self.session.read_opened_file(file, range).await
73    }
74
75    pub(crate) fn opened_file(&self) -> Result<File> {
76        self.backing.file.lock().unwrap().try_clone().map_err(io_error)
77    }
78}
79
80impl<C> StorageManager<C> {
81    /// Copies a stable opened range in native bounded batches. Cancellation drops
82    /// the partial copy only after any blocking writer releases its actual owners.
83    /// On Windows, `source` must allow a separate read open; exclusive handles are refused.
84    pub async fn freeze_opened_file(&self, source: File, range: ByteRange) -> Result<FrozenFile> {
85        let backing = tokio::task::spawn_blocking(move || {
86            let file = tempfile::NamedTempFile::new().map_err(io_error)?.into_file();
87            Ok::<_, crate::Error>(Arc::new(Backing { file: Mutex::new(file) }))
88        })
89        .await
90        .map_err(task_error)??;
91        let mut reader = self.read_opened_file(source, range).await?;
92        let mut digest = Sha256::new();
93        while let Some(batch) = reader.next().await? {
94            digest.update(batch.bytes());
95            let target = Arc::clone(&backing);
96            tokio::task::spawn_blocking(move || target.file.lock().unwrap().write_all(batch.bytes()).map_err(io_error))
97                .await
98                .map_err(task_error)??;
99        }
100        let completion = reader.completion()?;
101        if range.limit.is_some_and(|length| length != completion.bytes) {
102            return Err(crate::Error::InvalidInput {
103                field: "frozen file",
104                reason: "source ended before the selected prefix",
105            });
106        }
107        reader.check_unchanged().await?;
108        let session = self.erased();
109        Ok(FrozenFile {
110            backing,
111            length: completion.bytes,
112            sha256: digest.finalize().into(),
113            session,
114        })
115    }
116}
117
118/* === Public Functions === */
119
120/* === Private Functions === */
121
122fn task_error(error: tokio::task::JoinError) -> crate::Error {
123    crate::Error::backend("freeze opened file", crate::BackendFailure::Permanent, error)
124}
125
126/* === Tests === */