summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2021-10-24 09:59:45 +0300
committerLars Wirzenius <liw@liw.fi>2021-10-24 11:37:20 +0300
commit574b8e2c74f3174b15d3d80a21d86cbb10268b15 (patch)
tree2eb1f525826e0dba2bfe67e964c07792d20dd52c
parent60cb5be04ecd313f4ea508f66951957d9644fa01 (diff)
downloadobnam2-574b8e2c74f3174b15d3d80a21d86cbb10268b15.tar.gz
feat! store schema version of generation database in the db
Add a new mandatory database table "meta" to the SQLite database the stores information about the files in a backup generation. The idea is for future versions of the Obnam client to be able to be able to restore from backups made by older -- or newer -- versions of Obnam, as far as is reasonable. Add the `obnam gen-info` command to show information about the generation metadata. Sponsored-by: author
-rw-r--r--obnam.md28
-rw-r--r--src/bin/obnam.rs3
-rw-r--r--src/cmd/gen_info.rs36
-rw-r--r--src/cmd/mod.rs1
-rw-r--r--src/generation.rs115
5 files changed, 183 insertions, 0 deletions
diff --git a/obnam.md b/obnam.md
index 02634f0..d363069 100644
--- a/obnam.md
+++ b/obnam.md
@@ -1768,6 +1768,34 @@ exclude_cache_tag_directories: false
~~~
+## Generation information
+
+This scenario verifies that the Obnam client can show metadata about a
+backup generation.
+
+~~~scenario
+given an installed obnam
+given a running chunk server
+given a client config based on smoke.yaml
+given a file live/data.dat containing some random data
+given a manifest of the directory live in live.yaml
+given file geninfo.json
+when I run obnam backup
+when I run obnam gen-info latest
+then stdout, as JSON, has all the values in file geninfo.json
+~~~
+
+~~~{#geninfo.json .file .json}
+{
+ "schema_version": {
+ "major": 0,
+ "minor": 0
+ },
+ "extras": {}
+}
+~~~
+
+
# Acceptance criteria for backup encryption
This chapter outlines scenarios, to be implemented later, for
diff --git a/src/bin/obnam.rs b/src/bin/obnam.rs
index b2d5683..21add9c 100644
--- a/src/bin/obnam.rs
+++ b/src/bin/obnam.rs
@@ -5,6 +5,7 @@ use log4rs::config::{Appender, Logger, Root};
use obnam::cmd::backup::Backup;
use obnam::cmd::chunk::{DecryptChunk, EncryptChunk};
use obnam::cmd::chunkify::Chunkify;
+use obnam::cmd::gen_info::GenInfo;
use obnam::cmd::get_chunk::GetChunk;
use obnam::cmd::init::Init;
use obnam::cmd::list::List;
@@ -47,6 +48,7 @@ fn main_program() -> anyhow::Result<()> {
Command::ListFiles(x) => x.run(&config),
Command::Resolve(x) => x.run(&config),
Command::Restore(x) => x.run(&config),
+ Command::GenInfo(x) => x.run(&config),
Command::GetChunk(x) => x.run(&config),
Command::Config(x) => x.run(&config),
Command::EncryptChunk(x) => x.run(&config),
@@ -103,6 +105,7 @@ enum Command {
List(List),
ListFiles(ListFiles),
Restore(Restore),
+ GenInfo(GenInfo),
ShowGeneration(ShowGeneration),
Resolve(Resolve),
GetChunk(GetChunk),
diff --git a/src/cmd/gen_info.rs b/src/cmd/gen_info.rs
new file mode 100644
index 0000000..6d12bd8
--- /dev/null
+++ b/src/cmd/gen_info.rs
@@ -0,0 +1,36 @@
+use crate::client::AsyncBackupClient;
+use crate::config::ClientConfig;
+use crate::error::ObnamError;
+use log::info;
+use structopt::StructOpt;
+use tempfile::NamedTempFile;
+use tokio::runtime::Runtime;
+
+#[derive(Debug, StructOpt)]
+pub struct GenInfo {
+ #[structopt()]
+ gen_ref: String,
+}
+
+impl GenInfo {
+ pub fn run(&self, config: &ClientConfig) -> Result<(), ObnamError> {
+ let rt = Runtime::new()?;
+ rt.block_on(self.run_async(config))
+ }
+
+ async fn run_async(&self, config: &ClientConfig) -> Result<(), ObnamError> {
+ let temp = NamedTempFile::new()?;
+
+ let client = AsyncBackupClient::new(config)?;
+
+ let genlist = client.list_generations().await?;
+ let gen_id = genlist.resolve(&self.gen_ref)?;
+ info!("generation id is {}", gen_id.as_chunk_id());
+
+ let gen = client.fetch_generation(&gen_id, temp.path()).await?;
+ let meta = gen.meta()?;
+ println!("{}", serde_json::to_string_pretty(&meta)?);
+
+ Ok(())
+ }
+}
diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs
index 502ec5d..ee5efd9 100644
--- a/src/cmd/mod.rs
+++ b/src/cmd/mod.rs
@@ -1,6 +1,7 @@
pub mod backup;
pub mod chunk;
pub mod chunkify;
+pub mod gen_info;
pub mod get_chunk;
pub mod init;
pub mod list;
diff --git a/src/generation.rs b/src/generation.rs
index bd36a19..de2ea10 100644
--- a/src/generation.rs
+++ b/src/generation.rs
@@ -2,9 +2,17 @@ use crate::backup_reason::Reason;
use crate::chunkid::ChunkId;
use crate::fsentry::FilesystemEntry;
use rusqlite::Connection;
+use serde::Serialize;
+use std::collections::HashMap;
use std::fmt;
use std::path::{Path, PathBuf};
+/// Current generation database schema major version.
+const SCHEMA_MAJOR: u32 = 0;
+
+/// Current generation database schema minor version.
+const SCHEMA_MINOR: u32 = 0;
+
/// An identifier for a file in a generation.
type FileId = i64;
@@ -126,6 +134,15 @@ 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("Generation 'meta' table does not have a row {0}")]
+ NoMetaKey(String),
+
+ #[error("Generation 'meta' row {0} has badly formed integer: {1}")]
+ BadMetaInteger(String, std::num::ParseIntError),
+
#[error(transparent)]
RusqliteError(#[from] rusqlite::Error),
@@ -174,6 +191,11 @@ impl LocalGeneration {
Ok(Self { conn })
}
+ pub fn meta(&self) -> Result<GenMeta, LocalGenerationError> {
+ let map = sql::meta(&self.conn)?;
+ GenMeta::from(map)
+ }
+
pub fn file_count(&self) -> Result<i64, LocalGenerationError> {
sql::file_count(&self.conn)
}
@@ -205,6 +227,65 @@ impl LocalGeneration {
}
}
+/// Metadata about the generation.
+#[derive(Debug, Serialize)]
+pub struct GenMeta {
+ schema_version: SchemaVersion,
+ extras: HashMap<String, String>,
+}
+
+impl GenMeta {
+ fn from(mut map: HashMap<String, String>) -> Result<Self, LocalGenerationError> {
+ let major: u32 = metaint(&mut map, "schema_version_major")?;
+ let minor: u32 = metaint(&mut map, "schema_version_minor")?;
+ Ok(Self {
+ schema_version: SchemaVersion::new(major, minor),
+ extras: map,
+ })
+ }
+
+ pub fn schema_version(&self) -> SchemaVersion {
+ self.schema_version
+ }
+}
+
+fn metastr(map: &mut HashMap<String, String>, key: &str) -> Result<String, LocalGenerationError> {
+ if let Some(v) = map.remove(key) {
+ Ok(v)
+ } else {
+ Err(LocalGenerationError::NoMetaKey(key.to_string()))
+ }
+}
+
+fn metaint(map: &mut HashMap<String, String>, key: &str) -> Result<u32, LocalGenerationError> {
+ let v = metastr(map, key)?;
+ let v = v
+ .parse()
+ .map_err(|err| LocalGenerationError::BadMetaInteger(key.to_string(), err))?;
+ Ok(v)
+}
+
+/// Schema version of the database storing the generation.
+///
+/// An Obnam client can restore a generation using schema version
+/// (x,y), if the client supports a schema version (x,z). If z < y,
+/// the client knows it may not be able to the generation faithfully,
+/// and should warn the user about this. If z >= y, the client knows
+/// it can restore the generation faithfully. If the client does not
+/// support any schema version x, it knows it can't restore the backup
+/// at all.
+#[derive(Debug, Clone, Copy, Serialize)]
+pub struct SchemaVersion {
+ pub major: u32,
+ pub minor: u32,
+}
+
+impl SchemaVersion {
+ fn new(major: u32, minor: u32) -> Self {
+ Self { major, minor }
+ }
+}
+
mod sql {
use super::BackedUpFile;
use super::FileId;
@@ -212,14 +293,19 @@ mod sql {
use crate::backup_reason::Reason;
use crate::chunkid::ChunkId;
use crate::fsentry::FilesystemEntry;
+ use crate::generation::SCHEMA_MAJOR;
+ use crate::generation::SCHEMA_MINOR;
use log::debug;
use rusqlite::{params, Connection, OpenFlags, Row, Statement, Transaction};
+ use std::collections::HashMap;
use std::os::unix::ffi::OsStrExt;
use std::path::Path;
pub fn create_db(filename: &Path) -> Result<Connection, LocalGenerationError> {
let flags = OpenFlags::SQLITE_OPEN_CREATE | OpenFlags::SQLITE_OPEN_READ_WRITE;
let conn = Connection::open_with_flags(filename, flags)?;
+ conn.execute("CREATE TABLE meta (key TEXT, value TEXT)", params![])?;
+ init_meta(&conn)?;
conn.execute(
"CREATE TABLE files (fileno INTEGER PRIMARY KEY, filename BLOB, json TEXT, reason TEXT, is_cachedir_tag BOOLEAN)",
params![],
@@ -234,6 +320,18 @@ mod sql {
Ok(conn)
}
+ fn init_meta(conn: &Connection) -> Result<(), LocalGenerationError> {
+ conn.execute(
+ "INSERT INTO meta (key, value) VALUES (?1, ?2)",
+ params!["schema_version_major", SCHEMA_MAJOR],
+ )?;
+ conn.execute(
+ "INSERT INTO meta (key, value) VALUES (?1, ?2)",
+ params!["schema_version_minor", SCHEMA_MINOR],
+ )?;
+ Ok(())
+ }
+
pub fn open_db(filename: &Path) -> Result<Connection, LocalGenerationError> {
let flags = OpenFlags::SQLITE_OPEN_READ_WRITE;
let conn = Connection::open_with_flags(filename, flags)?;
@@ -241,6 +339,23 @@ mod sql {
Ok(conn)
}
+ pub fn meta(conn: &Connection) -> Result<HashMap<String, String>, LocalGenerationError> {
+ let mut stmt = conn.prepare("SELECT key, value FROM meta")?;
+ let iter = stmt.query_map(params![], |row| row_to_key_value(row))?;
+ let mut map = HashMap::new();
+ for r in iter {
+ let (key, value) = r?;
+ map.insert(key, value);
+ }
+ Ok(map)
+ }
+
+ fn row_to_key_value(row: &Row) -> rusqlite::Result<(String, String)> {
+ let key: String = row.get(row.column_index("key")?)?;
+ let value: String = row.get(row.column_index("value")?)?;
+ Ok((key, value))
+ }
+
pub fn insert_one(
t: &Transaction,
e: FilesystemEntry,