|
2 | 2 | //! ordered list of *writes* and a raw terminal-output byte log. |
3 | 3 | //! |
4 | 4 | //! A **write** is one open→…→close lifecycle of a descriptor opened for |
5 | | -//! writing. Its *before* is the file's content snapshotted in the open |
6 | | -//! callback; its *after* is the content snapshotted in the close callback of |
7 | | -//! the same descriptor. Both are stored as points in the Loro history (per-path |
| 5 | +//! writing. Its *after* is the content snapshotted in the close callback of |
| 6 | +//! that descriptor; its *before* is the file's content just prior to the write |
| 7 | +//! — the open-callback snapshot for a fresh or non-truncating open, or, when a |
| 8 | +//! truncating open (`O_TRUNC`, as `writeFileSync` uses) has already emptied the |
| 9 | +//! file before the snapshot could run, the content last recorded for that path. |
| 10 | +//! Both are stored as points in the Loro history (per-path |
8 | 11 | //! `LoroText`/binary, so repeated near-identical writes are delta-stored), and |
9 | 12 | //! each write records the before/after [`Frontiers`](loro::Frontiers) the |
10 | 13 | //! server later `checkout`s to render them. |
@@ -34,7 +37,9 @@ pub struct Write { |
34 | 37 | pub seq: u64, |
35 | 38 | /// Absolute path of the written file. |
36 | 39 | pub path: Str, |
37 | | - /// Frontier capturing the file's content at the matching open. |
| 40 | + /// Frontier capturing the file's content just before this write (the open |
| 41 | + /// snapshot, or the last-recorded content when a truncating open emptied it |
| 42 | + /// first). |
38 | 43 | pub before: Vec<OpId>, |
39 | 44 | /// Frontier capturing the file's content at the close. |
40 | 45 | pub after: Vec<OpId>, |
@@ -135,14 +140,34 @@ impl Snapshotter { |
135 | 140 | /// descriptor `raw_fd` of process `pid`. Pairs with the matching |
136 | 141 | /// [`Self::record_close`]. |
137 | 142 | /// |
| 143 | + /// The open-time `content` is read in the supervisor *after* the open |
| 144 | + /// syscall, so a truncating open (`O_TRUNC` — what `writeFileSync` and most |
| 145 | + /// whole-file writes use) has already emptied the file by the time we read |
| 146 | + /// it. To avoid losing the real pre-write content, the open read is only |
| 147 | + /// adopted as the `before` when it is non-empty (a non-truncating open, or a |
| 148 | + /// pre-existing file); an empty read for a path we've already recorded is |
| 149 | + /// treated as a truncating open and the last-recorded content is kept. |
| 150 | + /// |
138 | 151 | /// # Panics |
139 | 152 | /// |
140 | 153 | /// Panics if a Loro container operation fails (a corrupted invariant). |
141 | 154 | pub fn record_open(&self, pid: u32, raw_fd: i64, path: &str, content: &[u8]) { |
142 | 155 | let mut guard = self.lock(); |
143 | | - let before = set_content(&mut guard, path, content); |
144 | | - let key = fd_key(pid, raw_fd, &Str::from(path)); |
145 | | - guard.open.insert(key, before); |
| 156 | + let key = Str::from(path); |
| 157 | + let before = if !content.is_empty() { |
| 158 | + // Non-truncating open or a pre-existing file: the read reflects the |
| 159 | + // real current content (including any external modification). |
| 160 | + set_content(&mut guard, path, content) |
| 161 | + } else if guard.flavor.contains_key(&key) { |
| 162 | + // Empty read but we already track this path: a truncating open |
| 163 | + // emptied the file before we could read it. Keep what we last |
| 164 | + // recorded (the previous write's `after`) as the `before`. |
| 165 | + serialize_frontiers(&guard.doc.state_frontiers()) |
| 166 | + } else { |
| 167 | + // Empty read, never seen before: a genuinely new or empty file. |
| 168 | + set_content(&mut guard, path, content) |
| 169 | + }; |
| 170 | + guard.open.insert(fd_key(pid, raw_fd, &key), before); |
146 | 171 | } |
147 | 172 |
|
148 | 173 | /// Record the post-write snapshot taken just before `path` is closed on |
|
0 commit comments