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
use rusqlite::OptionalExtension;
use super::{SqlStore, exclusive::ExclusiveFileId};
use crate::{RootId, block::FileId as StoreFileId};
impl<'conn> SqlStore<'conn> {
/// Ensure the given file's `liteboxfs_files` row is private to `root_id`.
///
/// If the file is currently shared (i.e. paths in other roots also reference this file ID),
/// a new `liteboxfs_files` row is inserted (copying all metadata and associated
/// `liteboxfs_file_blocks`, `liteboxfs_merkle_nodes`, and `liteboxfs_xattrs` rows), and the
/// `liteboxfs_paths` rows for `root_id` are updated to point at the new row. The returned
/// [`ExclusiveFileId`] always refers to a row that is exclusively owned by `root_id` at the
/// time of the call.
pub fn materialize_file(
&self,
file_id: StoreFileId,
root_id: RootId,
) -> crate::Result<ExclusiveFileId> {
let root_row_id: i64 = self.db.query_row(
r#"
SELECT
id
FROM
liteboxfs_roots
WHERE
uuid = ?;
"#,
rusqlite::params![root_id.to_string()],
|row| row.get(0),
)?;
// A file is shared only if another root references it. Multiple paths within the same root
// are hard links: they're meant to share the underlying file row, so mutations should be
// visible through every link. COW'ing in that case would allocate a new file ID and break
// identity for callers that track files by ID.
let is_shared: bool = self
.db
.query_row(
r#"
SELECT
1
FROM
liteboxfs_paths
WHERE
file = ?
AND root != ?
LIMIT 1;
"#,
rusqlite::params![file_id, root_row_id],
|_| Ok(()),
)
.optional()?
.is_some();
if !is_shared {
return Ok(ExclusiveFileId::new(file_id));
}
// The file is shared. Insert a new liteboxfs_files row copying all metadata.
let new_file_id: StoreFileId = self.db.query_row(
r#"
INSERT INTO
liteboxfs_files (kind, mode, atime, mtime, ctime, btime, uid, gid, major, minor, target)
SELECT
kind, mode, atime, mtime, ctime, btime, uid, gid, major, minor, target
FROM
liteboxfs_files
WHERE
id = ?
RETURNING
id;
"#,
rusqlite::params![file_id],
|row| Ok(StoreFileId::from(row.get::<_, i64>(0)?)),
)?;
// Copy associated per-file tables.
self.copy_file_blocks(file_id, new_file_id)?; // defined in root.rs
self.copy_merkle_nodes(file_id, new_file_id)?;
self.copy_xattrs(file_id, new_file_id)?;
// Re-point all paths in this root to the new file ID.
self.db.execute(
r#"
UPDATE
liteboxfs_paths
SET
file = ?
WHERE
file = ?
AND root = ?;
"#,
rusqlite::params![new_file_id, file_id, root_row_id],
)?;
Ok(ExclusiveFileId::new(new_file_id))
}
/// Delete all `liteboxfs_files` rows that are no longer referenced by any `liteboxfs_paths`
/// row.
///
/// This is called after a root is deleted: the root deletion cascades through paths, but files
/// are not tied to roots directly, so orphaned files must be cleaned up separately.
pub fn delete_all_unlinked_files(&self) -> crate::Result<()> {
self.db.execute(
r#"
DELETE FROM
liteboxfs_files
WHERE
NOT EXISTS (
SELECT
1
FROM
liteboxfs_paths
WHERE
liteboxfs_paths.file = liteboxfs_files.id
);
"#,
[],
)?;
Ok(())
}
}