iroh_blobs/store/fs/
delete_set.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
use std::{
    collections::BTreeSet,
    sync::{Arc, Mutex},
};

use tracing::warn;

use super::options::PathOptions;
use crate::Hash;

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub(super) enum BaoFilePart {
    Outboard,
    Data,
    Sizes,
    Bitfield,
}

/// Creates a pair of a protect handle and a delete handle.
///
/// The protect handle can be used to protect files from deletion.
/// The delete handle can be used to create transactions in which files can be marked for deletion.
pub(super) fn pair(options: Arc<PathOptions>) -> (ProtectHandle, DeleteHandle) {
    let ds = Arc::new(Mutex::new(DeleteSet::default()));
    (ProtectHandle(ds.clone()), DeleteHandle::new(ds, options))
}

/// Helper to keep track of files to delete after a transaction is committed.
#[derive(Debug, Default)]
struct DeleteSet(BTreeSet<(Hash, BaoFilePart)>);

impl DeleteSet {
    /// Mark a file as to be deleted after the transaction is committed.
    fn delete(&mut self, hash: Hash, parts: impl IntoIterator<Item = BaoFilePart>) {
        for part in parts {
            self.0.insert((hash, part));
        }
    }

    /// Mark a file as to be kept after the transaction is committed.
    ///
    /// This will cancel any previous delete for the same file in the same transaction.
    fn protect(&mut self, hash: Hash, parts: impl IntoIterator<Item = BaoFilePart>) {
        for part in parts {
            self.0.remove(&(hash, part));
        }
    }

    /// Apply the delete set and clear it.
    ///
    /// This will delete all files marked for deletion and then clear the set.
    /// Errors will just be logged.
    fn commit(&mut self, options: &PathOptions) {
        for (hash, to_delete) in &self.0 {
            tracing::debug!("deleting {:?} for {hash}", to_delete);
            let path = match to_delete {
                BaoFilePart::Data => options.data_path(hash),
                BaoFilePart::Outboard => options.outboard_path(hash),
                BaoFilePart::Sizes => options.sizes_path(hash),
                BaoFilePart::Bitfield => options.bitfield_path(hash),
            };
            if let Err(cause) = std::fs::remove_file(&path) {
                // Ignore NotFound errors, if the file is already gone that's fine.
                if cause.kind() != std::io::ErrorKind::NotFound {
                    warn!(
                        "failed to delete {:?} {}: {}",
                        to_delete,
                        path.display(),
                        cause
                    );
                }
            }
        }
        self.0.clear();
    }

    fn clear(&mut self) {
        self.0.clear();
    }

    fn is_empty(&self) -> bool {
        self.0.is_empty()
    }
}

#[derive(Debug, Clone)]
pub(super) struct ProtectHandle(Arc<Mutex<DeleteSet>>);

/// Protect handle, to be used concurrently with transactions to mark files for keeping.
impl ProtectHandle {
    /// Inside or outside a transaction, mark files as to be kept
    ///
    /// If we are not inside a transaction, this will do nothing.
    pub(super) fn protect(&self, hash: Hash, parts: impl IntoIterator<Item = BaoFilePart>) {
        let mut guard = self.0.lock().unwrap();
        guard.protect(hash, parts);
    }
}

/// A delete handle. The only thing you can do with this is to open transactions that keep track of files to delete.
#[derive(Debug)]
pub(super) struct DeleteHandle {
    ds: Arc<Mutex<DeleteSet>>,
    options: Arc<PathOptions>,
}

impl DeleteHandle {
    fn new(ds: Arc<Mutex<DeleteSet>>, options: Arc<PathOptions>) -> Self {
        Self { ds, options }
    }

    /// Open a file transaction. You can open only one transaction at a time.
    pub(super) fn begin_write(&mut self) -> FileTransaction<'_> {
        FileTransaction::new(self)
    }
}

/// A file transaction. Inside a transaction, you can mark files for deletion.
///
/// Dropping a transaction will clear the delete set. Committing a transaction will apply the delete set by actually deleting the files.
#[derive(Debug)]
pub(super) struct FileTransaction<'a>(&'a DeleteHandle);

impl<'a> FileTransaction<'a> {
    fn new(inner: &'a DeleteHandle) -> Self {
        let guard = inner.ds.lock().unwrap();
        debug_assert!(guard.is_empty());
        drop(guard);
        Self(inner)
    }

    /// Mark files as to be deleted
    pub fn delete(&self, hash: Hash, parts: impl IntoIterator<Item = BaoFilePart>) {
        let mut guard = self.0.ds.lock().unwrap();
        guard.delete(hash, parts);
    }

    /// Apply the delete set and clear it.
    pub fn commit(self) {
        let mut guard = self.0.ds.lock().unwrap();
        guard.commit(&self.0.options);
    }
}

impl Drop for FileTransaction<'_> {
    fn drop(&mut self) {
        self.0.ds.lock().unwrap().clear();
    }
}