use anyhow::{Context, Result, bail};
use frankensqlite::Connection;
use frankensqlite::compat::OpenFlags;
use std::fs::Metadata;
#[cfg(not(windows))]
use std::fs::OpenOptions;
#[cfg(not(windows))]
use std::io::Write;
use std::path::{Path, PathBuf};
pub mod analytics;
pub mod archive_config;
pub mod attachments;
pub mod bundle;
pub mod config_input;
pub mod confirmation;
pub mod deploy_cloudflare;
pub mod deploy_github;
pub mod docs;
pub mod encrypt;
pub mod errors;
pub mod export;
pub mod fts;
pub mod key_management;
pub mod password;
pub mod patterns;
pub mod preview;
pub mod profiles;
pub mod qr;
pub mod redact;
pub mod secret_scan;
pub mod size;
pub mod summary;
pub mod verify;
pub mod wizard;
fn ensure_real_directory(path: &Path, metadata: &Metadata, label: &str) -> Result<()> {
let file_type = metadata.file_type();
if file_type.is_symlink() {
bail!("{label} must not be a symlink: {}", path.display());
}
if !file_type.is_dir() {
bail!("{label} must be a directory: {}", path.display());
}
Ok(())
}
pub(crate) fn resolve_site_dir(path: &Path) -> Result<PathBuf> {
let path_metadata = match std::fs::symlink_metadata(path) {
Ok(metadata) => metadata,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
bail!("path does not exist: {}", path.display());
}
Err(err) => {
return Err(err).with_context(|| format!("Failed to inspect path {}", path.display()));
}
};
if path.file_name().map(|name| name == "site").unwrap_or(false) {
ensure_real_directory(path, &path_metadata, "site directory")?;
return Ok(path.to_path_buf());
}
let site_subdir = path.join("site");
match std::fs::symlink_metadata(&site_subdir) {
Ok(metadata) => {
ensure_real_directory(&site_subdir, &metadata, "site directory")?;
return Ok(site_subdir);
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => {
return Err(err).with_context(|| {
format!("Failed to inspect site directory {}", site_subdir.display())
});
}
}
ensure_real_directory(path, &path_metadata, "site directory")?;
Ok(path.to_path_buf())
}
pub(crate) fn open_existing_sqlite_db(path: &Path) -> Result<Connection> {
if !path.exists() {
bail!("database does not exist: {}", path.display());
}
frankensqlite::compat::open_with_flags(
path.to_string_lossy().as_ref(),
OpenFlags::SQLITE_OPEN_READ_ONLY,
)
.with_context(|| format!("opening sqlite database at {}", path.display()))
}
#[cfg(not(windows))]
pub(crate) fn write_file_durably(path: &Path, data: &[u8]) -> Result<()> {
let mut f = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(path)
.with_context(|| format!("creating {} for durable write", path.display()))?;
f.write_all(data)
.with_context(|| format!("writing {} durably", path.display()))?;
f.sync_all()
.with_context(|| format!("fsyncing {} after durable write", path.display()))?;
drop(f);
let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) else {
return Ok(());
};
std::fs::File::open(parent)
.with_context(|| format!("opening parent {} for fsync", parent.display()))?
.sync_all()
.with_context(|| {
format!(
"fsyncing parent {} after durable write to {}",
parent.display(),
path.display()
)
})
}
#[cfg(windows)]
pub(crate) fn write_file_durably(path: &Path, data: &[u8]) -> Result<()> {
std::fs::write(path, data).with_context(|| format!("writing {}", path.display()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn write_file_durably_writes_bytes_and_fsyncs() {
let tmp = tempfile::tempdir().expect("tempdir");
let path = tmp.path().join("out.json");
write_file_durably(&path, b"hello").expect("durable write");
let got = std::fs::read(&path).expect("read back");
assert_eq!(got, b"hello");
}
#[cfg(not(windows))]
#[test]
fn write_file_durably_surfaces_parent_fsync_error() {
let tmp = tempfile::tempdir().expect("tempdir");
let nested = tmp.path().join("subdir");
std::fs::create_dir(&nested).expect("mkdir");
let path = nested.join("out.json");
std::fs::remove_dir_all(&nested).expect("rm nested");
let err = write_file_durably(&path, b"data").unwrap_err();
let msg = format!("{err:#}");
assert!(
msg.contains("creating") || msg.contains("opening parent"),
"expected durable write to surface I/O error, got: {msg}"
);
}
}