use crate::fsentry::{FilesystemEntry, FilesystemKind};
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;
//use crate::fsiter::FsIterator;
use crate::chunkid::ChunkId;
use rusqlite::{params, Connection, OpenFlags, Row, Transaction};
use std::os::unix::ffi::OsStringExt;
use std::path::{Path, PathBuf};
/// A backup generation.
pub struct Generation {
conn: Connection,
fileno: u64,
}
impl Generation {
pub fn create
(filename: P) -> anyhow::Result
where
P: AsRef,
{
let flags = OpenFlags::SQLITE_OPEN_CREATE | OpenFlags::SQLITE_OPEN_READ_WRITE;
let conn = Connection::open_with_flags(filename, flags)?;
conn.execute(
"CREATE TABLE files (fileno INTEGER PRIMARY KEY, path BLOB, kind INTEGER, len INTEGER)",
params![],
)?;
conn.execute(
"CREATE TABLE chunks (fileno INTEGER, chunkid TEXT)",
params![],
)?;
conn.pragma_update(None, "journal_mode", &"WAL")?;
Ok(Self { conn, fileno: 0 })
}
pub fn open(filename: P) -> anyhow::Result
where
P: AsRef,
{
let flags = OpenFlags::SQLITE_OPEN_READ_WRITE;
let conn = Connection::open_with_flags(filename, flags)?;
conn.pragma_update(None, "journal_mode", &"WAL")?;
let fileno = find_max_fileno(&conn)?;
Ok(Self { conn, fileno })
}
pub fn insert(&mut self, e: FilesystemEntry, ids: &[ChunkId]) -> anyhow::Result<()> {
let t = self.conn.transaction()?;
self.fileno += 1;
insert_one(&t, e, self.fileno, ids)?;
t.commit()?;
Ok(())
}
pub fn insert_iter(
&mut self,
entries: impl Iterator- )>>,
) -> anyhow::Result<()> {
let t = self.conn.transaction()?;
for r in entries {
let (e, ids) = r?;
self.fileno += 1;
insert_one(&t, e, self.fileno, &ids[..])?;
}
t.commit()?;
Ok(())
}
pub fn file_count(&self) -> u64 {
self.fileno
}
pub fn files(&self) -> anyhow::Result> {
let mut stmt = self.conn.prepare("SELECT * FROM files")?;
let iter = stmt.query_map(params![], |row| row_to_entry(row))?;
let mut files: Vec<(u64, FilesystemEntry)> = vec![];
for x in iter {
let (fileno, entry) = x?;
files.push((fileno, entry));
}
Ok(files)
}
pub fn chunkids(&self, fileno: u64) -> anyhow::Result> {
let fileno = fileno as i64;
let mut stmt = self
.conn
.prepare("SELECT chunkid FROM chunks WHERE fileno = ?1")?;
let iter = stmt.query_map(params![fileno], |row| Ok(row.get(0)?))?;
let mut ids: Vec = vec![];
for x in iter {
let fileno: String = x?;
ids.push(ChunkId::from(&fileno));
}
Ok(ids)
}
}
fn row_to_entry(row: &Row) -> rusqlite::Result<(u64, FilesystemEntry)> {
let fileno: i64 = row.get(row.column_index("fileno")?)?;
let fileno = fileno as u64;
let path: Vec = row.get(row.column_index("path")?)?;
let path: &OsStr = OsStrExt::from_bytes(&path);
let path: PathBuf = PathBuf::from(path);
let kind = row.get(row.column_index("kind")?)?;
let kind = FilesystemKind::from_code(kind).unwrap();
let entry = match kind {
FilesystemKind::Regular => FilesystemEntry::regular(path, 0),
FilesystemKind::Directory => FilesystemEntry::directory(path),
};
Ok((fileno, entry))
}
fn insert_one(
t: &Transaction,
e: FilesystemEntry,
fileno: u64,
ids: &[ChunkId],
) -> anyhow::Result<()> {
let path = e.path().as_os_str().to_os_string().into_vec();
let kind = e.kind().as_code();
let len = e.len() as i64;
let fileno = fileno as i64;
t.execute(
"INSERT INTO files (fileno, path, kind, len) VALUES (?1, ?2, ?3, ?4)",
params![fileno, path, kind, len],
)?;
for id in ids {
t.execute(
"INSERT INTO chunks (fileno, chunkid) VALUES (?1, ?2)",
params![fileno, id],
)?;
}
Ok(())
}
fn find_max_fileno(conn: &Connection) -> anyhow::Result {
let mut stmt = conn.prepare("SELECT fileno FROM files ORDER BY fileno")?;
let mut rows = stmt.query(params![])?;
let mut fileno: i64 = 0;
while let Some(row) = rows.next()? {
let x = row.get(0)?;
if x > fileno {
fileno = x;
}
}
Ok(fileno as u64)
}
#[cfg(test)]
mod test {
use super::Generation;
use tempfile::NamedTempFile;
#[test]
fn empty() {
let filename = NamedTempFile::new().unwrap().path().to_path_buf();
{
let mut _gen = Generation::create(&filename).unwrap();
// _gen is dropped here; the connection is close; the file
// should not be removed.
}
assert!(filename.exists());
}
}