use crate::{Error, Result, readable};
use fs2::FileExt;
use rusqlite::{
Connection, OpenFlags,
backup::{Backup, StepResult},
};
use std::{
fs::{self, File, OpenOptions},
io::{BufWriter, Write},
path::{Path, PathBuf},
};
pub(crate) const SQLITE_HEADER: &[u8] = b"SQLite format 3\0";
pub(crate) const MAX_SNAPSHOT: usize = 256 * 1024 * 1024;
pub(crate) fn reader(path: &Path) -> Result<Connection> {
use std::io::Read;
let mut header = [0; 16];
let count = File::open(path)?.read(&mut header)?;
if count == 16 && header == SQLITE_HEADER {
let db = Connection::open_with_flags(path, OpenFlags::SQLITE_OPEN_READ_ONLY)?;
db.busy_timeout(std::time::Duration::from_secs(5))?;
Ok(db)
} else {
import(&fs::read(path)?)
}
}
pub(crate) struct ReadableFile {
pub path: PathBuf,
_lock: File,
cache: readable::SnapshotCache,
}
impl ReadableFile {
pub fn open(path: &Path) -> Result<Self> {
let parent = path
.parent()
.filter(|p| !p.as_os_str().is_empty())
.unwrap_or(Path::new("."));
let path = if path.exists() {
fs::canonicalize(path)?
} else {
fs::canonicalize(parent)?.join(
path.file_name()
.ok_or_else(|| Error::new("invalid path".into()))?,
)
};
let mut lock = path.as_os_str().to_os_string();
lock.push(".lock");
let lock = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(PathBuf::from(lock))?;
lock.try_lock_exclusive().map_err(|error| {
let locked = error.raw_os_error() == fs2::lock_contended_error().raw_os_error();
let error = Error::from(error);
if locked {
error
.with_code("BOXDB_LOCKED")
.with_context("readable database is locked")
} else {
error.with_context("lock readable database")
}
})?;
Ok(Self {
path,
_lock: lock,
cache: readable::SnapshotCache::default(),
})
}
pub fn watch(&self, hooks: &crate::history::Shared) {
*crate::history::lock(&hooks.readable) = Some(self.cache.tracker());
}
pub fn publish(&self, db: &Connection, types: &crate::schema::Types) -> Result<()> {
let mut replaced = false;
self.cache
.publish(db, types, |snapshot| {
let parent = self.path.parent().unwrap();
let mut temp = tempfile::NamedTempFile::new_in(parent)?;
{
let mut writer = BufWriter::with_capacity(64 * 1024, temp.as_file_mut());
snapshot.write(&mut writer)?;
writer.flush()?;
}
temp.as_file().sync_all()?;
temp.persist(&self.path)
.map_err(|e| Error::from(e.error).with_context("publish readable database"))?;
replaced = true;
File::open(parent)?.sync_all()?;
Ok(())
})
.map_err(|error| {
error.state(
crate::ErrorOperation::Publish,
if replaced {
crate::OperationOutcome::Unknown
} else {
crate::OperationOutcome::NotApplied
},
!db.is_autocommit(),
false,
)
})
}
}
pub(crate) fn copy(source: &Connection, destination: &mut Connection) -> Result<()> {
let backup = Backup::new(source, destination)?;
match backup.step(-1)? {
StepResult::Done => Ok(()),
StepResult::Busy => Err(Error::sqlite(
rusqlite::ffi::SQLITE_BUSY,
"BoxDB conversion is busy; retry after the transaction completes".into(),
)),
StepResult::Locked => Err(Error::sqlite(
rusqlite::ffi::SQLITE_LOCKED,
"BoxDB conversion is locked; retry after the transaction completes".into(),
)),
_ => Err(Error::internal("BoxDB conversion backup did not finish")),
}
}
pub(crate) fn import(bytes: &[u8]) -> Result<Connection> {
if bytes.len() > MAX_SNAPSHOT {
return Err(Error::new("readable database exceeds 256 MiB limit".into()));
}
readable::decode(bytes)
}
pub(crate) fn data_version(db: &Connection) -> Result<u32> {
let mut version = 0u32;
let code = unsafe {
rusqlite::ffi::sqlite3_file_control(
db.handle(),
c"main".as_ptr(),
rusqlite::ffi::SQLITE_FCNTL_DATA_VERSION,
(&mut version as *mut u32).cast(),
)
};
if code != rusqlite::ffi::SQLITE_OK {
return Err(Error::sqlite(
code,
format!("cannot inspect SQLite pager: {code}"),
));
}
Ok(version)
}
pub(crate) fn check_commit_constraints(db: &Connection) -> Result<()> {
let mut unresolved = 0;
let mut highwater = 0;
let code = unsafe {
rusqlite::ffi::sqlite3_db_status(
db.handle(),
rusqlite::ffi::SQLITE_DBSTATUS_DEFERRED_FKS,
&mut unresolved,
&mut highwater,
0,
)
};
if code != rusqlite::ffi::SQLITE_OK {
return Err(Error::sqlite(
code,
format!("cannot inspect pending SQLite constraints: {code}"),
));
}
if unresolved != 0 {
return Err(Error::sqlite(
rusqlite::ffi::SQLITE_CONSTRAINT_FOREIGNKEY,
"FOREIGN KEY constraint failed".into(),
));
}
Ok(())
}