| author | |
| committer | |
| log | ee54ecdfad5603988e09516439764799c94202e3 |
| tree | 56fd8dd70ec818c9fd3dd5cc9acffdca5d1a5aab |
| parent | c830e2971fbbcfeb96cfb5286d43d88e0edff1d5 |
| signature | Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU (~clover) |
A new replica was built under a name of its own and hard-linked into place, which Android
refuses apps, so no section could open there. It now moves into place with a rename that
refuses to replace a file, as tempfile persists without clobbering.
Refs #24
Assisted-by: claude-opus-5.53 files changed, 24 insertions(+), 10 deletions(-)
crates/notebook/src/fs.rs+15| ... | @@ -6,6 +6,21 @@ pub use onestore::{confirm_file, place_file, read_file, read_file_limited, super | ... | @@ -6,6 +6,21 @@ pub use onestore::{confirm_file, place_file, read_file, read_file_limited, super |
| 6 | #[cfg(not(target_arch = "wasm32"))] | 6 | #[cfg(not(target_arch = "wasm32"))] |
| 7 | pub use std::{fs::*, path::absolute, process::id as process_id}; | 7 | pub use std::{fs::*, path::absolute, process::id as process_id}; |
| 8 | 8 | ||
| 9 | /// Moves the file `from` to `to`, refusing to replace a file there. Android forbids apps hard | ||
| 10 | /// links, the usual way to do this. | ||
| 11 | #[cfg(not(target_arch = "wasm32"))] | ||
| 12 | pub fn rename_new( | ||
| 13 | from: impl AsRef<std::path::Path>, | ||
| 14 | to: impl AsRef<std::path::Path>, | ||
| 15 | ) -> std::io::Result<()> { | ||
| 16 | tempfile::TempPath::try_from_path(from.as_ref())? | ||
| 17 | .persist_noclobber(to) | ||
| 18 | .map_err(|error| { | ||
| 19 | let _ = error.path.keep(); | ||
| 20 | error.error | ||
| 21 | }) | ||
| 22 | } | ||
| 23 | |||
| 9 | /// `Transaction::commit_file` through this file system. | 24 | /// `Transaction::commit_file` through this file system. |
| 10 | #[cfg(not(target_arch = "wasm32"))] | 25 | #[cfg(not(target_arch = "wasm32"))] |
| 11 | pub fn commit_file( | 26 | pub fn commit_file( |
crates/notebook/src/fs/web.rs+6-7| ... | @@ -414,14 +414,13 @@ pub fn copy(from: impl AsRef<Path>, to: impl AsRef<Path>) -> io::Result<u64> { | ... | @@ -414,14 +414,13 @@ pub fn copy(from: impl AsRef<Path>, to: impl AsRef<Path>) -> io::Result<u64> { |
| 414 | Ok(bytes.len() as u64) | 414 | Ok(bytes.len() as u64) |
| 415 | } | 415 | } |
| 416 | 416 | ||
| 417 | /// A copy, as nothing here writes a file under two names at once: a link is made to put a | 417 | /// Moves the file `from` to `to`, refusing to replace a file there. |
| 418 | /// finished file in place, then the first name removed. | 418 | pub fn rename_new(from: impl AsRef<Path>, to: impl AsRef<Path>) -> io::Result<()> { |
| 419 | pub fn hard_link(original: impl AsRef<Path>, link: impl AsRef<Path>) -> io::Result<()> { | 419 | let bytes = read(&from)?; |
| 420 | let bytes = read(original)?; | 420 | let to = normal(to.as_ref()); |
| 421 | let link = normal(link.as_ref()); | 421 | let data = with(|files| files.create(to))?; |
| 422 | let data = with(|files| files.create(link))?; | ||
| 423 | data.borrow_mut().bytes = bytes; | 422 | data.borrow_mut().bytes = bytes; |
| 424 | Ok(()) | 423 | remove_file(from) |
| 425 | } | 424 | } |
| 426 | 425 | ||
| 427 | pub fn create_dir(path: impl AsRef<Path>) -> io::Result<()> { | 426 | pub fn create_dir(path: impl AsRef<Path>) -> io::Result<()> { |
crates/notebook/src/lib.rs+3-3| ... | @@ -171,7 +171,7 @@ impl Replica { | ... | @@ -171,7 +171,7 @@ impl Replica { |
| 171 | /// `create` without opening the cache it made, as for an offline copy. | 171 | /// `create` without opening the cache it made, as for an offline copy. |
| 172 | pub(crate) fn seed(path: &Path, source: &[u8], key: Option<&Key>) -> Result<()> { | 172 | pub(crate) fn seed(path: &Path, source: &[u8], key: Option<&Key>) -> Result<()> { |
| 173 | validate(source, key)?; | 173 | validate(source, key)?; |
| 174 | // Built under a name of its own and linked into place whole, so that a cache that | 174 | // Built under a name of its own and moved into place whole, so that a cache that |
| 175 | // exists is complete, and two threads making the same one never share a build. | 175 | // exists is complete, and two threads making the same one never share a build. |
| 176 | static BUILDS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); | 176 | static BUILDS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); |
| 177 | let build = BUILDS.fetch_add(1, std::sync::atomic::Ordering::Relaxed); | 177 | let build = BUILDS.fetch_add(1, std::sync::atomic::Ordering::Relaxed); |
| ... | @@ -179,13 +179,13 @@ impl Replica { | ... | @@ -179,13 +179,13 @@ impl Replica { |
| 179 | building.push(format!(".creating-{}-{build}", fs::process_id())); | 179 | building.push(format!(".creating-{}-{build}", fs::process_id())); |
| 180 | let building = PathBuf::from(building); | 180 | let building = PathBuf::from(building); |
| 181 | let built = Self::build(&building, source); | 181 | let built = Self::build(&building, source); |
| 182 | let linked = built.and_then(|()| Ok(fs::hard_link(&building, path)?)); | 182 | let placed = built.and_then(|()| Ok(fs::rename_new(&building, path)?)); |
| 183 | for suffix in ["", "-wal", "-shm"] { | 183 | for suffix in ["", "-wal", "-shm"] { |
| 184 | let mut file = building.as_os_str().to_owned(); | 184 | let mut file = building.as_os_str().to_owned(); |
| 185 | file.push(suffix); | 185 | file.push(suffix); |
| 186 | let _ = fs::remove_file(file); | 186 | let _ = fs::remove_file(file); |
| 187 | } | 187 | } |
| 188 | linked | 188 | placed |
| 189 | } | 189 | } |
| 190 | 190 | ||
| 191 | fn build(path: &Path, source: &[u8]) -> Result<()> { | 191 | fn build(path: &Path, source: &[u8]) -> Result<()> { |