use crate::backup_reason::Reason;
use crate::chunkid::ChunkId;
use crate::db::{DatabaseError, SqlResults};
use crate::dbgen::{FileId, GenerationDb, GenerationDbError};
use crate::fsentry::FilesystemEntry;
use crate::genmeta::{GenerationMeta, GenerationMetaError};
use crate::label::LabelChecksumKind;
use crate::schema::{SchemaVersion, VersionComponent};
use serde::Serialize;
use std::fmt;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize)]
pub struct GenId {
id: ChunkId,
}
impl GenId {
pub fn from_chunk_id(id: ChunkId) -> Self {
Self { id }
}
pub fn as_chunk_id(&self) -> &ChunkId {
&self.id
}
}
impl fmt::Display for GenId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.id)
}
}
pub struct NascentGeneration {
db: GenerationDb,
fileno: FileId,
}
#[derive(Debug, thiserror::Error)]
pub enum NascentError {
#[error("Could not back up a backup root directory: {0}: {1}")]
BackupRootFailed(PathBuf, crate::fsiter::FsIterError),
#[error(transparent)]
LocalGenerationError(#[from] LocalGenerationError),
#[error(transparent)]
GenerationDb(#[from] GenerationDbError),
#[error("SQL transaction error: {0}")]
Transaction(rusqlite::Error),
#[error("SQL commit error: {0}")]
Commit(rusqlite::Error),
#[error("Failed to create temporary file: {0}")]
TempFile(#[from] std::io::Error),
}
impl NascentGeneration {
pub fn create<P>(
filename: P,
schema: SchemaVersion,
checksum_kind: LabelChecksumKind,
) -> Result<Self, NascentError>
where
P: AsRef<Path>,
{
let db = GenerationDb::create(filename.as_ref(), schema, checksum_kind)?;
Ok(Self { db, fileno: 0 })
}
pub fn close(self) -> Result<(), NascentError> {
self.db.close().map_err(NascentError::GenerationDb)
}
pub fn file_count(&self) -> FileId {
self.fileno
}
pub fn insert(
&mut self,
e: FilesystemEntry,
ids: &[ChunkId],
reason: Reason,
is_cachedir_tag: bool,
) -> Result<(), NascentError> {
self.fileno += 1;
self.db
.insert(e, self.fileno, ids, reason, is_cachedir_tag)?;
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct FinishedGeneration {
id: GenId,
ended: String,
}
impl FinishedGeneration {
pub fn new(id: &str, ended: &str) -> Self {
let id = GenId::from_chunk_id(id.parse().unwrap()); Self {
id,
ended: ended.to_string(),
}
}
pub fn id(&self) -> &GenId {
&self.id
}
pub fn ended(&self) -> &str {
&self.ended
}
}
pub struct LocalGeneration {
db: GenerationDb,
}
#[derive(Debug, thiserror::Error)]
pub enum LocalGenerationError {
#[error("Generation has more than one file with the name {0}")]
TooManyFiles(PathBuf),
#[error("Generation does not have a 'meta' table")]
NoMeta,
#[error("Backup is not compatible with this version of Obnam: {0}.{1}")]
Incompatible(VersionComponent, VersionComponent),
#[error(transparent)]
GenerationMeta(#[from] GenerationMetaError),
#[error(transparent)]
RusqliteError(#[from] rusqlite::Error),
#[error(transparent)]
GenerationDb(#[from] GenerationDbError),
#[error(transparent)]
Database(#[from] DatabaseError),
#[error(transparent)]
SerdeJsonError(#[from] serde_json::Error),
#[error(transparent)]
IoError(#[from] std::io::Error),
}
pub struct BackedUpFile {
fileno: FileId,
entry: FilesystemEntry,
reason: Reason,
}
impl BackedUpFile {
pub fn new(fileno: FileId, entry: FilesystemEntry, reason: Reason) -> Self {
Self {
fileno,
entry,
reason,
}
}
pub fn fileno(&self) -> FileId {
self.fileno
}
pub fn entry(&self) -> &FilesystemEntry {
&self.entry
}
pub fn reason(&self) -> Reason {
self.reason
}
}
impl LocalGeneration {
fn new(db: GenerationDb) -> Self {
Self { db }
}
pub fn open<P>(filename: P) -> Result<Self, LocalGenerationError>
where
P: AsRef<Path>,
{
let db = GenerationDb::open(filename.as_ref())?;
let gen = Self::new(db);
Ok(gen)
}
pub fn meta(&self) -> Result<GenerationMeta, LocalGenerationError> {
let map = self.db.meta()?;
GenerationMeta::from(map).map_err(LocalGenerationError::GenerationMeta)
}
pub fn file_count(&self) -> Result<FileId, LocalGenerationError> {
Ok(self.db.file_count()?)
}
pub fn files(
&self,
) -> Result<SqlResults<(FileId, FilesystemEntry, Reason, bool)>, LocalGenerationError> {
self.db.files().map_err(LocalGenerationError::GenerationDb)
}
pub fn chunkids(&self, fileid: FileId) -> Result<SqlResults<ChunkId>, LocalGenerationError> {
self.db
.chunkids(fileid)
.map_err(LocalGenerationError::GenerationDb)
}
pub fn get_file(
&self,
filename: &Path,
) -> Result<Option<FilesystemEntry>, LocalGenerationError> {
self.db
.get_file(filename)
.map_err(LocalGenerationError::GenerationDb)
}
pub fn get_fileno(&self, filename: &Path) -> Result<Option<FileId>, LocalGenerationError> {
self.db
.get_fileno(filename)
.map_err(LocalGenerationError::GenerationDb)
}
pub fn is_cachedir_tag(&self, filename: &Path) -> Result<bool, LocalGenerationError> {
self.db
.is_cachedir_tag(filename)
.map_err(LocalGenerationError::GenerationDb)
}
}
#[cfg(test)]
mod test {
use super::{LabelChecksumKind, LocalGeneration, NascentGeneration, Reason, SchemaVersion};
use crate::fsentry::EntryBuilder;
use crate::fsentry::FilesystemKind;
use std::path::PathBuf;
use tempfile::{tempdir, NamedTempFile};
#[test]
fn round_trips_u64_max() {
let tmp = tempdir().unwrap();
let filename = tmp.path().join("test.db");
let path = PathBuf::from("/");
let schema = SchemaVersion::new(0, 0);
{
let e = EntryBuilder::new(FilesystemKind::Directory)
.path(path.clone())
.len(u64::MAX)
.build();
let mut gen =
NascentGeneration::create(&filename, schema, LabelChecksumKind::Sha256).unwrap();
gen.insert(e, &[], Reason::IsNew, false).unwrap();
gen.close().unwrap();
}
let db = LocalGeneration::open(&filename).unwrap();
let e = db.get_file(&path).unwrap().unwrap();
assert_eq!(e.len(), u64::MAX);
}
#[test]
fn empty() {
let filename = NamedTempFile::new().unwrap().path().to_path_buf();
let schema = SchemaVersion::new(0, 0);
{
let mut _gen =
NascentGeneration::create(&filename, schema, LabelChecksumKind::Sha256).unwrap();
}
assert!(filename.exists());
}
#[test]
fn remembers_cachedir_tags() {
use crate::{
backup_reason::Reason, backup_run::FsEntryBackupOutcome, fsentry::FilesystemEntry,
};
use std::{fs::metadata, path::Path};
let src_file = NamedTempFile::new().unwrap();
let metadata = metadata(src_file.path()).unwrap();
let dbfile = NamedTempFile::new().unwrap().path().to_path_buf();
let nontag_path1 = Path::new("/nontag1");
let nontag_path2 = Path::new("/dir/nontag2");
let tag_path1 = Path::new("/a_tag");
let tag_path2 = Path::new("/another_dir/a_tag");
let schema = SchemaVersion::new(0, 0);
let mut gen =
NascentGeneration::create(&dbfile, schema, LabelChecksumKind::Sha256).unwrap();
let mut cache = users::UsersCache::new();
gen.insert(
FilesystemEntry::from_metadata(nontag_path1, &metadata, &mut cache).unwrap(),
&[],
Reason::IsNew,
false,
)
.unwrap();
gen.insert(
FilesystemEntry::from_metadata(tag_path1, &metadata, &mut cache).unwrap(),
&[],
Reason::IsNew,
true,
)
.unwrap();
let entries = vec![
FsEntryBackupOutcome {
entry: FilesystemEntry::from_metadata(nontag_path2, &metadata, &mut cache).unwrap(),
ids: vec![],
reason: Reason::IsNew,
is_cachedir_tag: false,
},
FsEntryBackupOutcome {
entry: FilesystemEntry::from_metadata(tag_path2, &metadata, &mut cache).unwrap(),
ids: vec![],
reason: Reason::IsNew,
is_cachedir_tag: true,
},
];
for o in entries {
gen.insert(o.entry, &o.ids, o.reason, o.is_cachedir_tag)
.unwrap();
}
gen.close().unwrap();
let gen = LocalGeneration::open(dbfile).unwrap();
assert!(!gen.is_cachedir_tag(nontag_path1).unwrap());
assert!(!gen.is_cachedir_tag(nontag_path2).unwrap());
assert!(gen.is_cachedir_tag(tag_path1).unwrap());
assert!(gen.is_cachedir_tag(tag_path2).unwrap());
assert!(!gen.is_cachedir_tag(Path::new("/hello/world")).unwrap());
assert!(!gen
.is_cachedir_tag(Path::new("/different path/to/another file.txt"))
.unwrap());
}
}