Skip to main content

coding_agent_search/pages/
mod.rs

1use anyhow::{Context, Result, bail};
2use frankensqlite::Connection;
3use frankensqlite::compat::OpenFlags;
4use std::fs::Metadata;
5#[cfg(not(windows))]
6use std::fs::OpenOptions;
7#[cfg(not(windows))]
8use std::io::Write;
9use std::path::{Path, PathBuf};
10
11pub mod analytics;
12pub mod archive_config;
13pub mod attachments;
14pub mod bundle;
15pub mod config_input;
16pub mod confirmation;
17pub mod deploy_cloudflare;
18pub mod deploy_github;
19pub mod docs;
20pub mod encrypt;
21pub mod errors;
22pub mod export;
23pub mod fts;
24pub mod key_management;
25pub mod password;
26pub mod patterns;
27pub mod preview;
28pub mod profiles;
29pub mod qr;
30pub mod redact;
31pub mod secret_scan;
32pub mod size;
33pub mod summary;
34pub mod verify;
35pub mod wizard;
36
37fn ensure_real_directory(path: &Path, metadata: &Metadata, label: &str) -> Result<()> {
38    let file_type = metadata.file_type();
39    if file_type.is_symlink() {
40        bail!("{label} must not be a symlink: {}", path.display());
41    }
42    if !file_type.is_dir() {
43        bail!("{label} must be a directory: {}", path.display());
44    }
45    Ok(())
46}
47
48pub(crate) fn resolve_site_dir(path: &Path) -> Result<PathBuf> {
49    let path_metadata = match std::fs::symlink_metadata(path) {
50        Ok(metadata) => metadata,
51        Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
52            bail!("path does not exist: {}", path.display());
53        }
54        Err(err) => {
55            return Err(err).with_context(|| format!("Failed to inspect path {}", path.display()));
56        }
57    };
58
59    if path.file_name().map(|name| name == "site").unwrap_or(false) {
60        ensure_real_directory(path, &path_metadata, "site directory")?;
61        return Ok(path.to_path_buf());
62    }
63
64    let site_subdir = path.join("site");
65    match std::fs::symlink_metadata(&site_subdir) {
66        Ok(metadata) => {
67            ensure_real_directory(&site_subdir, &metadata, "site directory")?;
68            return Ok(site_subdir);
69        }
70        Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
71        Err(err) => {
72            return Err(err).with_context(|| {
73                format!("Failed to inspect site directory {}", site_subdir.display())
74            });
75        }
76    }
77
78    ensure_real_directory(path, &path_metadata, "site directory")?;
79    Ok(path.to_path_buf())
80}
81
82pub(crate) fn open_existing_sqlite_db(path: &Path) -> Result<Connection> {
83    if !path.exists() {
84        bail!("database does not exist: {}", path.display());
85    }
86
87    // Open read-only to prevent accidental writes to the source database
88    // during export/scan operations.
89    frankensqlite::compat::open_with_flags(
90        path.to_string_lossy().as_ref(),
91        OpenFlags::SQLITE_OPEN_READ_ONLY,
92    )
93    .with_context(|| format!("opening sqlite database at {}", path.display()))
94}
95
96/// Write `data` to `path` and fsync both the file contents and the parent
97/// directory so the name-entry pointing at `path` survives a crash.
98///
99/// Why: a bare `std::fs::write` only flushes the page cache when the OS
100/// decides to. If power is lost between the write and the next sync, the
101/// file can appear empty or missing after reboot. This helper mirrors the
102/// fix landed for `pages/encrypt.rs::sync_tree` under bead
103/// coding_agent_session_search-92o31.
104#[cfg(not(windows))]
105pub(crate) fn write_file_durably(path: &Path, data: &[u8]) -> Result<()> {
106    let mut f = OpenOptions::new()
107        .create(true)
108        .write(true)
109        .truncate(true)
110        .open(path)
111        .with_context(|| format!("creating {} for durable write", path.display()))?;
112    f.write_all(data)
113        .with_context(|| format!("writing {} durably", path.display()))?;
114    f.sync_all()
115        .with_context(|| format!("fsyncing {} after durable write", path.display()))?;
116    drop(f);
117    let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) else {
118        return Ok(());
119    };
120    std::fs::File::open(parent)
121        .with_context(|| format!("opening parent {} for fsync", parent.display()))?
122        .sync_all()
123        .with_context(|| {
124            format!(
125                "fsyncing parent {} after durable write to {}",
126                parent.display(),
127                path.display()
128            )
129        })
130}
131
132/// Windows has no portable directory-fsync; NTFS journals dirent updates
133/// synchronously, so plain `fs::write` is sufficient for crash safety.
134#[cfg(windows)]
135pub(crate) fn write_file_durably(path: &Path, data: &[u8]) -> Result<()> {
136    std::fs::write(path, data).with_context(|| format!("writing {}", path.display()))
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn write_file_durably_writes_bytes_and_fsyncs() {
145        let tmp = tempfile::tempdir().expect("tempdir");
146        let path = tmp.path().join("out.json");
147        write_file_durably(&path, b"hello").expect("durable write");
148        let got = std::fs::read(&path).expect("read back");
149        assert_eq!(got, b"hello");
150    }
151
152    #[cfg(not(windows))]
153    #[test]
154    fn write_file_durably_surfaces_parent_fsync_error() {
155        // Negative-side guard mirroring the sync_tree regression test from
156        // bead coding_agent_session_search-92o31: if the parent directory
157        // disappears between write and fsync, the helper must surface the
158        // I/O error rather than silently succeeding.
159        let tmp = tempfile::tempdir().expect("tempdir");
160        let nested = tmp.path().join("subdir");
161        std::fs::create_dir(&nested).expect("mkdir");
162        let path = nested.join("out.json");
163
164        // A file path whose parent does not exist must fail at the open
165        // step; this proves the write is routed through our helper rather
166        // than any fire-and-forget path.
167        std::fs::remove_dir_all(&nested).expect("rm nested");
168        let err = write_file_durably(&path, b"data").unwrap_err();
169        let msg = format!("{err:#}");
170        assert!(
171            msg.contains("creating") || msg.contains("opening parent"),
172            "expected durable write to surface I/O error, got: {msg}"
173        );
174    }
175}