2020-07-30 10:19:22 +00:00
|
|
|
use anyhow::{bail, format_err, Error};
|
2019-05-10 08:25:40 +00:00
|
|
|
use std::sync::{Arc, Mutex};
|
2020-10-20 08:08:25 +00:00
|
|
|
use std::collections::{HashMap, HashSet};
|
|
|
|
use nix::dir::Dir;
|
2019-05-08 10:41:58 +00:00
|
|
|
|
2020-07-31 05:27:57 +00:00
|
|
|
use ::serde::{Serialize};
|
2020-05-18 07:57:35 +00:00
|
|
|
use serde_json::{json, Value};
|
2019-05-08 10:41:58 +00:00
|
|
|
|
2019-12-18 10:05:30 +00:00
|
|
|
use proxmox::tools::digest_to_hex;
|
|
|
|
use proxmox::tools::fs::{replace_file, CreateOptions};
|
2019-11-21 13:36:28 +00:00
|
|
|
use proxmox::api::{RpcEnvironment, RpcEnvironmentType};
|
2019-06-14 08:36:20 +00:00
|
|
|
|
assume correct backup, avoid verifying chunk existance
This can slow things down by a lot on setups with (relatively) high
seek time, in the order of doubling the backup times if cache isn't
populated with the last backups chunk inode info.
Effectively there's nothing known this protects us from in the
codebase. The only thing which was theorized about was the case
where a really long running backup job (over 24 hours) is still
running and writing new chunks, not indexed yet anywhere, then an
update (or manual action) triggers a reload of the proxy. There was
some theory that then a GC in the new daemon would not know about the
oldest writer in the old one, and thus use a less strict atime limit
for chunk sweeping - opening up a window for deleting chunks from the
long running backup.
But, this simply cannot happen as we have a per datastore process
wide flock, which is acquired shared by backup jobs and exclusive by
GC. In the same process GC and backup can both get it, as it has a
process locking granularity. If there's an old daemon with a writer,
that also has the lock open shared, and so no GC in the new process
can get exclusive access to it.
So, with that confirmed we have no need for a "half-assed"
verification in the backup finish step. Rather, we plan to add an
opt-in "full verify each backup on finish" option (see #2988)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-10-01 09:59:18 +00:00
|
|
|
use crate::api2::types::Userid;
|
2019-05-09 11:06:09 +00:00
|
|
|
use crate::backup::*;
|
2020-08-06 13:46:01 +00:00
|
|
|
use crate::server::WorkerTask;
|
2019-05-09 16:01:24 +00:00
|
|
|
use crate::server::formatter::*;
|
|
|
|
use hyper::{Body, Response};
|
2019-05-08 10:41:58 +00:00
|
|
|
|
2020-07-31 05:27:57 +00:00
|
|
|
#[derive(Copy, Clone, Serialize)]
|
2019-05-30 07:20:32 +00:00
|
|
|
struct UploadStatistic {
|
|
|
|
count: u64,
|
|
|
|
size: u64,
|
|
|
|
compressed_size: u64,
|
|
|
|
duplicates: u64,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl UploadStatistic {
|
|
|
|
fn new() -> Self {
|
|
|
|
Self {
|
|
|
|
count: 0,
|
|
|
|
size: 0,
|
|
|
|
compressed_size: 0,
|
|
|
|
duplicates: 0,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-05-23 06:50:36 +00:00
|
|
|
|
2020-07-31 05:27:57 +00:00
|
|
|
impl std::ops::Add for UploadStatistic {
|
|
|
|
type Output = Self;
|
|
|
|
|
|
|
|
fn add(self, other: Self) -> Self {
|
|
|
|
Self {
|
|
|
|
count: self.count + other.count,
|
|
|
|
size: self.size + other.size,
|
|
|
|
compressed_size: self.compressed_size + other.compressed_size,
|
|
|
|
duplicates: self.duplicates + other.duplicates,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-23 06:50:36 +00:00
|
|
|
struct DynamicWriterState {
|
|
|
|
name: String,
|
|
|
|
index: DynamicIndexWriter,
|
|
|
|
offset: u64,
|
|
|
|
chunk_count: u64,
|
2019-05-30 07:20:32 +00:00
|
|
|
upload_stat: UploadStatistic,
|
2019-05-23 06:50:36 +00:00
|
|
|
}
|
|
|
|
|
2019-05-28 04:18:55 +00:00
|
|
|
struct FixedWriterState {
|
|
|
|
name: String,
|
|
|
|
index: FixedIndexWriter,
|
|
|
|
size: usize,
|
|
|
|
chunk_size: u32,
|
|
|
|
chunk_count: u64,
|
2019-07-04 11:40:43 +00:00
|
|
|
small_chunk_count: usize, // allow 0..1 small chunks (last chunk may be smaller)
|
2019-05-30 07:20:32 +00:00
|
|
|
upload_stat: UploadStatistic,
|
2020-06-23 12:43:10 +00:00
|
|
|
incremental: bool,
|
2019-05-28 04:18:55 +00:00
|
|
|
}
|
|
|
|
|
assume correct backup, avoid verifying chunk existance
This can slow things down by a lot on setups with (relatively) high
seek time, in the order of doubling the backup times if cache isn't
populated with the last backups chunk inode info.
Effectively there's nothing known this protects us from in the
codebase. The only thing which was theorized about was the case
where a really long running backup job (over 24 hours) is still
running and writing new chunks, not indexed yet anywhere, then an
update (or manual action) triggers a reload of the proxy. There was
some theory that then a GC in the new daemon would not know about the
oldest writer in the old one, and thus use a less strict atime limit
for chunk sweeping - opening up a window for deleting chunks from the
long running backup.
But, this simply cannot happen as we have a per datastore process
wide flock, which is acquired shared by backup jobs and exclusive by
GC. In the same process GC and backup can both get it, as it has a
process locking granularity. If there's an old daemon with a writer,
that also has the lock open shared, and so no GC in the new process
can get exclusive access to it.
So, with that confirmed we have no need for a "half-assed"
verification in the backup finish step. Rather, we plan to add an
opt-in "full verify each backup on finish" option (see #2988)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-10-01 09:59:18 +00:00
|
|
|
// key=digest, value=length
|
|
|
|
type KnownChunksMap = HashMap<[u8;32], u32>;
|
2020-09-14 08:50:19 +00:00
|
|
|
|
2019-05-10 08:25:40 +00:00
|
|
|
struct SharedBackupState {
|
2019-05-15 10:58:55 +00:00
|
|
|
finished: bool,
|
2019-05-10 08:25:40 +00:00
|
|
|
uid_counter: usize,
|
2020-05-30 14:37:33 +00:00
|
|
|
file_counter: usize, // successfully uploaded files
|
2019-05-23 06:50:36 +00:00
|
|
|
dynamic_writers: HashMap<usize, DynamicWriterState>,
|
2019-05-28 04:18:55 +00:00
|
|
|
fixed_writers: HashMap<usize, FixedWriterState>,
|
2020-09-14 08:50:19 +00:00
|
|
|
known_chunks: KnownChunksMap,
|
2020-07-31 05:27:57 +00:00
|
|
|
backup_size: u64, // sums up size of all files
|
|
|
|
backup_stat: UploadStatistic,
|
2019-05-10 08:25:40 +00:00
|
|
|
}
|
|
|
|
|
2019-05-15 10:58:55 +00:00
|
|
|
impl SharedBackupState {
|
|
|
|
|
|
|
|
// Raise error if finished flag is set
|
|
|
|
fn ensure_unfinished(&self) -> Result<(), Error> {
|
|
|
|
if self.finished {
|
|
|
|
bail!("backup already marked as finished.");
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get an unique integer ID
|
|
|
|
pub fn next_uid(&mut self) -> usize {
|
|
|
|
self.uid_counter += 1;
|
|
|
|
self.uid_counter
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2019-05-08 10:41:58 +00:00
|
|
|
/// `RpcEnvironmet` implementation for backup service
|
|
|
|
#[derive(Clone)]
|
|
|
|
pub struct BackupEnvironment {
|
|
|
|
env_type: RpcEnvironmentType,
|
2020-05-18 07:57:35 +00:00
|
|
|
result_attributes: Value,
|
2020-08-06 13:46:01 +00:00
|
|
|
user: Userid,
|
2019-05-29 07:35:21 +00:00
|
|
|
pub debug: bool,
|
2019-05-09 16:01:24 +00:00
|
|
|
pub formatter: &'static OutputFormatter,
|
2019-05-09 11:06:09 +00:00
|
|
|
pub worker: Arc<WorkerTask>,
|
|
|
|
pub datastore: Arc<DataStore>,
|
2019-05-10 08:25:40 +00:00
|
|
|
pub backup_dir: BackupDir,
|
2019-05-11 09:21:13 +00:00
|
|
|
pub last_backup: Option<BackupInfo>,
|
2019-05-10 08:25:40 +00:00
|
|
|
state: Arc<Mutex<SharedBackupState>>
|
2019-05-08 10:41:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl BackupEnvironment {
|
2019-05-10 08:25:40 +00:00
|
|
|
pub fn new(
|
|
|
|
env_type: RpcEnvironmentType,
|
2020-08-06 13:46:01 +00:00
|
|
|
user: Userid,
|
2019-05-10 08:25:40 +00:00
|
|
|
worker: Arc<WorkerTask>,
|
|
|
|
datastore: Arc<DataStore>,
|
|
|
|
backup_dir: BackupDir,
|
|
|
|
) -> Self {
|
|
|
|
|
|
|
|
let state = SharedBackupState {
|
2019-05-15 10:58:55 +00:00
|
|
|
finished: false,
|
2019-05-10 08:25:40 +00:00
|
|
|
uid_counter: 0,
|
2019-05-29 08:38:57 +00:00
|
|
|
file_counter: 0,
|
2019-05-10 08:25:40 +00:00
|
|
|
dynamic_writers: HashMap::new(),
|
2019-05-28 04:18:55 +00:00
|
|
|
fixed_writers: HashMap::new(),
|
2019-05-20 16:05:10 +00:00
|
|
|
known_chunks: HashMap::new(),
|
2020-07-31 05:27:57 +00:00
|
|
|
backup_size: 0,
|
|
|
|
backup_stat: UploadStatistic::new(),
|
2019-05-10 08:25:40 +00:00
|
|
|
};
|
|
|
|
|
2019-05-08 10:41:58 +00:00
|
|
|
Self {
|
2020-05-18 07:57:35 +00:00
|
|
|
result_attributes: json!({}),
|
2019-05-08 10:41:58 +00:00
|
|
|
env_type,
|
|
|
|
user,
|
|
|
|
worker,
|
2019-05-09 11:06:09 +00:00
|
|
|
datastore,
|
2019-05-29 07:35:21 +00:00
|
|
|
debug: false,
|
2019-05-09 16:01:24 +00:00
|
|
|
formatter: &JSON_FORMATTER,
|
2019-05-10 08:25:40 +00:00
|
|
|
backup_dir,
|
2019-05-11 09:21:13 +00:00
|
|
|
last_backup: None,
|
2019-05-10 08:25:40 +00:00
|
|
|
state: Arc::new(Mutex::new(state)),
|
2019-05-08 10:41:58 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-30 07:20:32 +00:00
|
|
|
/// Register a Chunk with associated length.
|
|
|
|
///
|
|
|
|
/// We do not fully trust clients, so a client may only use registered
|
|
|
|
/// chunks. Please use this method to register chunks from previous backups.
|
2019-05-20 16:05:10 +00:00
|
|
|
pub fn register_chunk(&self, digest: [u8; 32], length: u32) -> Result<(), Error> {
|
|
|
|
let mut state = self.state.lock().unwrap();
|
|
|
|
|
|
|
|
state.ensure_unfinished()?;
|
|
|
|
|
assume correct backup, avoid verifying chunk existance
This can slow things down by a lot on setups with (relatively) high
seek time, in the order of doubling the backup times if cache isn't
populated with the last backups chunk inode info.
Effectively there's nothing known this protects us from in the
codebase. The only thing which was theorized about was the case
where a really long running backup job (over 24 hours) is still
running and writing new chunks, not indexed yet anywhere, then an
update (or manual action) triggers a reload of the proxy. There was
some theory that then a GC in the new daemon would not know about the
oldest writer in the old one, and thus use a less strict atime limit
for chunk sweeping - opening up a window for deleting chunks from the
long running backup.
But, this simply cannot happen as we have a per datastore process
wide flock, which is acquired shared by backup jobs and exclusive by
GC. In the same process GC and backup can both get it, as it has a
process locking granularity. If there's an old daemon with a writer,
that also has the lock open shared, and so no GC in the new process
can get exclusive access to it.
So, with that confirmed we have no need for a "half-assed"
verification in the backup finish step. Rather, we plan to add an
opt-in "full verify each backup on finish" option (see #2988)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-10-01 09:59:18 +00:00
|
|
|
state.known_chunks.insert(digest, length);
|
2019-05-20 16:05:10 +00:00
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2019-05-30 07:20:32 +00:00
|
|
|
/// Register fixed length chunks after upload.
|
|
|
|
///
|
|
|
|
/// Like `register_chunk()`, but additionally record statistics for
|
|
|
|
/// the fixed index writer.
|
2019-05-30 06:10:06 +00:00
|
|
|
pub fn register_fixed_chunk(
|
|
|
|
&self,
|
|
|
|
wid: usize,
|
|
|
|
digest: [u8; 32],
|
|
|
|
size: u32,
|
|
|
|
compressed_size: u32,
|
|
|
|
is_duplicate: bool,
|
|
|
|
) -> Result<(), Error> {
|
|
|
|
let mut state = self.state.lock().unwrap();
|
|
|
|
|
|
|
|
state.ensure_unfinished()?;
|
|
|
|
|
|
|
|
let mut data = match state.fixed_writers.get_mut(&wid) {
|
|
|
|
Some(data) => data,
|
|
|
|
None => bail!("fixed writer '{}' not registered", wid),
|
|
|
|
};
|
|
|
|
|
2019-07-04 11:40:43 +00:00
|
|
|
if size > data.chunk_size {
|
|
|
|
bail!("fixed writer '{}' - got large chunk ({} > {}", data.name, size, data.chunk_size);
|
|
|
|
} else if size < data.chunk_size {
|
|
|
|
data.small_chunk_count += 1;
|
|
|
|
if data.small_chunk_count > 1 {
|
|
|
|
bail!("fixed writer '{}' - detected multiple end chunks (chunk size too small)");
|
|
|
|
}
|
2019-05-30 06:10:06 +00:00
|
|
|
}
|
|
|
|
|
2019-05-30 07:20:32 +00:00
|
|
|
// record statistics
|
|
|
|
data.upload_stat.count += 1;
|
|
|
|
data.upload_stat.size += size as u64;
|
|
|
|
data.upload_stat.compressed_size += compressed_size as u64;
|
|
|
|
if is_duplicate { data.upload_stat.duplicates += 1; }
|
|
|
|
|
|
|
|
// register chunk
|
assume correct backup, avoid verifying chunk existance
This can slow things down by a lot on setups with (relatively) high
seek time, in the order of doubling the backup times if cache isn't
populated with the last backups chunk inode info.
Effectively there's nothing known this protects us from in the
codebase. The only thing which was theorized about was the case
where a really long running backup job (over 24 hours) is still
running and writing new chunks, not indexed yet anywhere, then an
update (or manual action) triggers a reload of the proxy. There was
some theory that then a GC in the new daemon would not know about the
oldest writer in the old one, and thus use a less strict atime limit
for chunk sweeping - opening up a window for deleting chunks from the
long running backup.
But, this simply cannot happen as we have a per datastore process
wide flock, which is acquired shared by backup jobs and exclusive by
GC. In the same process GC and backup can both get it, as it has a
process locking granularity. If there's an old daemon with a writer,
that also has the lock open shared, and so no GC in the new process
can get exclusive access to it.
So, with that confirmed we have no need for a "half-assed"
verification in the backup finish step. Rather, we plan to add an
opt-in "full verify each backup on finish" option (see #2988)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-10-01 09:59:18 +00:00
|
|
|
state.known_chunks.insert(digest, size);
|
2019-05-30 06:10:06 +00:00
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2019-05-30 07:20:32 +00:00
|
|
|
/// Register dynamic length chunks after upload.
|
|
|
|
///
|
|
|
|
/// Like `register_chunk()`, but additionally record statistics for
|
|
|
|
/// the dynamic index writer.
|
2019-05-30 06:10:06 +00:00
|
|
|
pub fn register_dynamic_chunk(
|
|
|
|
&self,
|
|
|
|
wid: usize,
|
|
|
|
digest: [u8; 32],
|
|
|
|
size: u32,
|
|
|
|
compressed_size: u32,
|
|
|
|
is_duplicate: bool,
|
|
|
|
) -> Result<(), Error> {
|
|
|
|
let mut state = self.state.lock().unwrap();
|
|
|
|
|
|
|
|
state.ensure_unfinished()?;
|
|
|
|
|
|
|
|
let mut data = match state.dynamic_writers.get_mut(&wid) {
|
|
|
|
Some(data) => data,
|
|
|
|
None => bail!("dynamic writer '{}' not registered", wid),
|
|
|
|
};
|
|
|
|
|
2019-05-30 07:20:32 +00:00
|
|
|
// record statistics
|
|
|
|
data.upload_stat.count += 1;
|
|
|
|
data.upload_stat.size += size as u64;
|
|
|
|
data.upload_stat.compressed_size += compressed_size as u64;
|
|
|
|
if is_duplicate { data.upload_stat.duplicates += 1; }
|
|
|
|
|
|
|
|
// register chunk
|
assume correct backup, avoid verifying chunk existance
This can slow things down by a lot on setups with (relatively) high
seek time, in the order of doubling the backup times if cache isn't
populated with the last backups chunk inode info.
Effectively there's nothing known this protects us from in the
codebase. The only thing which was theorized about was the case
where a really long running backup job (over 24 hours) is still
running and writing new chunks, not indexed yet anywhere, then an
update (or manual action) triggers a reload of the proxy. There was
some theory that then a GC in the new daemon would not know about the
oldest writer in the old one, and thus use a less strict atime limit
for chunk sweeping - opening up a window for deleting chunks from the
long running backup.
But, this simply cannot happen as we have a per datastore process
wide flock, which is acquired shared by backup jobs and exclusive by
GC. In the same process GC and backup can both get it, as it has a
process locking granularity. If there's an old daemon with a writer,
that also has the lock open shared, and so no GC in the new process
can get exclusive access to it.
So, with that confirmed we have no need for a "half-assed"
verification in the backup finish step. Rather, we plan to add an
opt-in "full verify each backup on finish" option (see #2988)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-10-01 09:59:18 +00:00
|
|
|
state.known_chunks.insert(digest, size);
|
2019-05-30 06:10:06 +00:00
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2019-05-20 16:05:10 +00:00
|
|
|
pub fn lookup_chunk(&self, digest: &[u8; 32]) -> Option<u32> {
|
|
|
|
let state = self.state.lock().unwrap();
|
|
|
|
|
|
|
|
match state.known_chunks.get(digest) {
|
assume correct backup, avoid verifying chunk existance
This can slow things down by a lot on setups with (relatively) high
seek time, in the order of doubling the backup times if cache isn't
populated with the last backups chunk inode info.
Effectively there's nothing known this protects us from in the
codebase. The only thing which was theorized about was the case
where a really long running backup job (over 24 hours) is still
running and writing new chunks, not indexed yet anywhere, then an
update (or manual action) triggers a reload of the proxy. There was
some theory that then a GC in the new daemon would not know about the
oldest writer in the old one, and thus use a less strict atime limit
for chunk sweeping - opening up a window for deleting chunks from the
long running backup.
But, this simply cannot happen as we have a per datastore process
wide flock, which is acquired shared by backup jobs and exclusive by
GC. In the same process GC and backup can both get it, as it has a
process locking granularity. If there's an old daemon with a writer,
that also has the lock open shared, and so no GC in the new process
can get exclusive access to it.
So, with that confirmed we have no need for a "half-assed"
verification in the backup finish step. Rather, we plan to add an
opt-in "full verify each backup on finish" option (see #2988)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-10-01 09:59:18 +00:00
|
|
|
Some(len) => Some(*len),
|
2019-05-20 16:05:10 +00:00
|
|
|
None => None,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-15 10:58:55 +00:00
|
|
|
/// Store the writer with an unique ID
|
2019-05-23 06:50:36 +00:00
|
|
|
pub fn register_dynamic_writer(&self, index: DynamicIndexWriter, name: String) -> Result<usize, Error> {
|
2019-05-10 08:25:40 +00:00
|
|
|
let mut state = self.state.lock().unwrap();
|
|
|
|
|
2019-05-15 10:58:55 +00:00
|
|
|
state.ensure_unfinished()?;
|
|
|
|
|
|
|
|
let uid = state.next_uid();
|
2019-05-10 08:25:40 +00:00
|
|
|
|
2019-05-23 06:50:36 +00:00
|
|
|
state.dynamic_writers.insert(uid, DynamicWriterState {
|
2019-05-30 07:20:32 +00:00
|
|
|
index, name, offset: 0, chunk_count: 0, upload_stat: UploadStatistic::new(),
|
2019-05-23 06:50:36 +00:00
|
|
|
});
|
2019-05-15 10:58:55 +00:00
|
|
|
|
|
|
|
Ok(uid)
|
2019-05-10 08:25:40 +00:00
|
|
|
}
|
|
|
|
|
2019-05-28 04:18:55 +00:00
|
|
|
/// Store the writer with an unique ID
|
2020-06-23 12:43:10 +00:00
|
|
|
pub fn register_fixed_writer(&self, index: FixedIndexWriter, name: String, size: usize, chunk_size: u32, incremental: bool) -> Result<usize, Error> {
|
2019-05-28 04:18:55 +00:00
|
|
|
let mut state = self.state.lock().unwrap();
|
|
|
|
|
|
|
|
state.ensure_unfinished()?;
|
|
|
|
|
|
|
|
let uid = state.next_uid();
|
|
|
|
|
|
|
|
state.fixed_writers.insert(uid, FixedWriterState {
|
2020-06-23 12:43:10 +00:00
|
|
|
index, name, chunk_count: 0, size, chunk_size, small_chunk_count: 0, upload_stat: UploadStatistic::new(), incremental,
|
2019-05-28 04:18:55 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
Ok(uid)
|
|
|
|
}
|
|
|
|
|
2019-05-10 08:25:40 +00:00
|
|
|
/// Append chunk to dynamic writer
|
2019-05-24 08:05:22 +00:00
|
|
|
pub fn dynamic_writer_append_chunk(&self, wid: usize, offset: u64, size: u32, digest: &[u8; 32]) -> Result<(), Error> {
|
2019-05-10 08:25:40 +00:00
|
|
|
let mut state = self.state.lock().unwrap();
|
|
|
|
|
2019-05-15 10:58:55 +00:00
|
|
|
state.ensure_unfinished()?;
|
|
|
|
|
2019-05-10 08:25:40 +00:00
|
|
|
let mut data = match state.dynamic_writers.get_mut(&wid) {
|
|
|
|
Some(data) => data,
|
|
|
|
None => bail!("dynamic writer '{}' not registered", wid),
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2019-05-24 08:05:22 +00:00
|
|
|
if data.offset != offset {
|
|
|
|
bail!("dynamic writer '{}' append chunk failed - got strange chunk offset ({} != {})",
|
|
|
|
data.name, data.offset, offset);
|
|
|
|
}
|
|
|
|
|
2019-05-28 07:01:01 +00:00
|
|
|
data.offset += size as u64;
|
|
|
|
data.chunk_count += 1;
|
|
|
|
|
2019-05-23 06:50:36 +00:00
|
|
|
data.index.add_chunk(data.offset, digest)?;
|
2019-05-10 08:25:40 +00:00
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2019-05-28 04:18:55 +00:00
|
|
|
/// Append chunk to fixed writer
|
|
|
|
pub fn fixed_writer_append_chunk(&self, wid: usize, offset: u64, size: u32, digest: &[u8; 32]) -> Result<(), Error> {
|
|
|
|
let mut state = self.state.lock().unwrap();
|
|
|
|
|
|
|
|
state.ensure_unfinished()?;
|
|
|
|
|
|
|
|
let mut data = match state.fixed_writers.get_mut(&wid) {
|
|
|
|
Some(data) => data,
|
|
|
|
None => bail!("fixed writer '{}' not registered", wid),
|
|
|
|
};
|
|
|
|
|
2019-07-04 11:40:43 +00:00
|
|
|
let end = (offset as usize) + (size as usize);
|
|
|
|
let idx = data.index.check_chunk_alignment(end, size as usize)?;
|
2019-05-28 04:18:55 +00:00
|
|
|
|
2019-07-04 11:40:43 +00:00
|
|
|
data.chunk_count += 1;
|
2019-05-28 04:18:55 +00:00
|
|
|
|
2019-07-04 11:40:43 +00:00
|
|
|
data.index.add_digest(idx, digest)?;
|
2019-05-28 04:18:55 +00:00
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2019-06-14 08:36:20 +00:00
|
|
|
fn log_upload_stat(&self, archive_name: &str, csum: &[u8; 32], uuid: &[u8; 16], size: u64, chunk_count: u64, upload_stat: &UploadStatistic) {
|
2019-05-30 07:20:32 +00:00
|
|
|
self.log(format!("Upload statistics for '{}'", archive_name));
|
2019-08-03 11:05:38 +00:00
|
|
|
self.log(format!("UUID: {}", digest_to_hex(uuid)));
|
|
|
|
self.log(format!("Checksum: {}", digest_to_hex(csum)));
|
2019-05-30 07:20:32 +00:00
|
|
|
self.log(format!("Size: {}", size));
|
|
|
|
self.log(format!("Chunk count: {}", chunk_count));
|
2019-06-14 05:12:30 +00:00
|
|
|
|
|
|
|
if size == 0 || chunk_count == 0 {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-05-30 07:20:32 +00:00
|
|
|
self.log(format!("Upload size: {} ({}%)", upload_stat.size, (upload_stat.size*100)/size));
|
2019-06-14 05:12:30 +00:00
|
|
|
|
2020-06-23 12:43:09 +00:00
|
|
|
// account for zero chunk, which might be uploaded but never used
|
|
|
|
let client_side_duplicates = if chunk_count < upload_stat.count {
|
|
|
|
0
|
|
|
|
} else {
|
|
|
|
chunk_count - upload_stat.count
|
|
|
|
};
|
|
|
|
|
2019-06-14 05:12:30 +00:00
|
|
|
let server_side_duplicates = upload_stat.duplicates;
|
|
|
|
|
|
|
|
if (client_side_duplicates + server_side_duplicates) > 0 {
|
|
|
|
let per = (client_side_duplicates + server_side_duplicates)*100/chunk_count;
|
|
|
|
self.log(format!("Duplicates: {}+{} ({}%)", client_side_duplicates, server_side_duplicates, per));
|
|
|
|
}
|
|
|
|
|
2019-05-30 07:20:32 +00:00
|
|
|
if upload_stat.size > 0 {
|
2019-06-14 05:12:30 +00:00
|
|
|
self.log(format!("Compression: {}%", (upload_stat.compressed_size*100)/upload_stat.size));
|
2019-05-30 07:20:32 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-15 05:58:05 +00:00
|
|
|
/// Close dynamic writer
|
2019-09-23 08:56:53 +00:00
|
|
|
pub fn dynamic_writer_close(&self, wid: usize, chunk_count: u64, size: u64, csum: [u8; 32]) -> Result<(), Error> {
|
2019-05-15 05:58:05 +00:00
|
|
|
let mut state = self.state.lock().unwrap();
|
|
|
|
|
2019-05-15 10:58:55 +00:00
|
|
|
state.ensure_unfinished()?;
|
|
|
|
|
2019-05-15 05:58:05 +00:00
|
|
|
let mut data = match state.dynamic_writers.remove(&wid) {
|
|
|
|
Some(data) => data,
|
|
|
|
None => bail!("dynamic writer '{}' not registered", wid),
|
|
|
|
};
|
|
|
|
|
2019-05-23 06:50:36 +00:00
|
|
|
if data.chunk_count != chunk_count {
|
|
|
|
bail!("dynamic writer '{}' close failed - unexpected chunk count ({} != {})", data.name, data.chunk_count, chunk_count);
|
|
|
|
}
|
|
|
|
|
|
|
|
if data.offset != size {
|
|
|
|
bail!("dynamic writer '{}' close failed - unexpected file size ({} != {})", data.name, data.offset, size);
|
|
|
|
}
|
|
|
|
|
2019-06-14 08:36:20 +00:00
|
|
|
let uuid = data.index.uuid;
|
|
|
|
|
2019-09-23 08:56:53 +00:00
|
|
|
let expected_csum = data.index.close()?;
|
|
|
|
|
|
|
|
if csum != expected_csum {
|
|
|
|
bail!("dynamic writer '{}' close failed - got unexpected checksum", data.name);
|
|
|
|
}
|
2019-05-15 05:58:05 +00:00
|
|
|
|
2019-06-14 08:36:20 +00:00
|
|
|
self.log_upload_stat(&data.name, &csum, &uuid, size, chunk_count, &data.upload_stat);
|
2019-05-30 07:20:32 +00:00
|
|
|
|
2019-05-29 08:38:57 +00:00
|
|
|
state.file_counter += 1;
|
2020-07-31 05:27:57 +00:00
|
|
|
state.backup_size += size;
|
|
|
|
state.backup_stat = state.backup_stat + data.upload_stat;
|
2019-05-29 08:38:57 +00:00
|
|
|
|
2019-05-15 05:58:05 +00:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2019-05-28 04:18:55 +00:00
|
|
|
/// Close fixed writer
|
2019-09-23 08:56:53 +00:00
|
|
|
pub fn fixed_writer_close(&self, wid: usize, chunk_count: u64, size: u64, csum: [u8; 32]) -> Result<(), Error> {
|
2019-05-28 04:18:55 +00:00
|
|
|
let mut state = self.state.lock().unwrap();
|
|
|
|
|
|
|
|
state.ensure_unfinished()?;
|
|
|
|
|
|
|
|
let mut data = match state.fixed_writers.remove(&wid) {
|
|
|
|
Some(data) => data,
|
|
|
|
None => bail!("fixed writer '{}' not registered", wid),
|
|
|
|
};
|
|
|
|
|
|
|
|
if data.chunk_count != chunk_count {
|
2019-05-28 07:12:38 +00:00
|
|
|
bail!("fixed writer '{}' close failed - received wrong number of chunk ({} != {})", data.name, data.chunk_count, chunk_count);
|
2019-05-28 04:18:55 +00:00
|
|
|
}
|
|
|
|
|
2020-06-23 12:43:10 +00:00
|
|
|
if !data.incremental {
|
|
|
|
let expected_count = data.index.index_length();
|
2019-05-28 07:12:38 +00:00
|
|
|
|
2020-06-23 12:43:10 +00:00
|
|
|
if chunk_count != (expected_count as u64) {
|
|
|
|
bail!("fixed writer '{}' close failed - unexpected chunk count ({} != {})", data.name, expected_count, chunk_count);
|
|
|
|
}
|
2019-05-28 07:12:38 +00:00
|
|
|
|
2020-06-23 12:43:10 +00:00
|
|
|
if size != (data.size as u64) {
|
|
|
|
bail!("fixed writer '{}' close failed - unexpected file size ({} != {})", data.name, data.size, size);
|
|
|
|
}
|
2019-05-28 07:12:38 +00:00
|
|
|
}
|
2019-05-28 04:18:55 +00:00
|
|
|
|
2019-06-14 08:36:20 +00:00
|
|
|
let uuid = data.index.uuid;
|
2019-09-23 08:56:53 +00:00
|
|
|
let expected_csum = data.index.close()?;
|
2019-05-28 04:18:55 +00:00
|
|
|
|
2019-09-23 08:56:53 +00:00
|
|
|
if csum != expected_csum {
|
|
|
|
bail!("fixed writer '{}' close failed - got unexpected checksum", data.name);
|
|
|
|
}
|
|
|
|
|
|
|
|
self.log_upload_stat(&data.name, &expected_csum, &uuid, size, chunk_count, &data.upload_stat);
|
2019-05-30 07:20:32 +00:00
|
|
|
|
2019-05-29 08:38:57 +00:00
|
|
|
state.file_counter += 1;
|
2020-07-31 05:27:57 +00:00
|
|
|
state.backup_size += size;
|
|
|
|
state.backup_stat = state.backup_stat + data.upload_stat;
|
2019-05-29 08:38:57 +00:00
|
|
|
|
2019-05-28 04:18:55 +00:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2019-06-24 07:35:37 +00:00
|
|
|
pub fn add_blob(&self, file_name: &str, data: Vec<u8>) -> Result<(), Error> {
|
|
|
|
|
|
|
|
let mut path = self.datastore.base_path();
|
|
|
|
path.push(self.backup_dir.relative_path());
|
|
|
|
path.push(file_name);
|
|
|
|
|
|
|
|
let blob_len = data.len();
|
|
|
|
let orig_len = data.len(); // fixme:
|
|
|
|
|
2020-07-28 08:23:16 +00:00
|
|
|
// always verify blob/CRC at server side
|
|
|
|
let blob = DataBlob::load_from_reader(&mut &data[..])?;
|
2019-06-24 07:35:37 +00:00
|
|
|
|
|
|
|
let raw_data = blob.raw_data();
|
2019-12-18 10:05:30 +00:00
|
|
|
replace_file(&path, raw_data, CreateOptions::new())?;
|
2019-06-24 07:35:37 +00:00
|
|
|
|
|
|
|
self.log(format!("add blob {:?} ({} bytes, comp: {})", path, orig_len, blob_len));
|
|
|
|
|
|
|
|
let mut state = self.state.lock().unwrap();
|
|
|
|
state.file_counter += 1;
|
2020-07-31 05:27:57 +00:00
|
|
|
state.backup_size += orig_len as u64;
|
|
|
|
state.backup_stat.size += blob_len as u64;
|
2019-06-24 07:35:37 +00:00
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2019-05-15 10:58:55 +00:00
|
|
|
/// Mark backup as finished
|
|
|
|
pub fn finish_backup(&self) -> Result<(), Error> {
|
|
|
|
let mut state = self.state.lock().unwrap();
|
|
|
|
|
|
|
|
state.ensure_unfinished()?;
|
|
|
|
|
2020-09-08 13:29:42 +00:00
|
|
|
// test if all writer are correctly closed
|
|
|
|
if state.dynamic_writers.len() != 0 || state.fixed_writers.len() != 0 {
|
2019-05-15 10:58:55 +00:00
|
|
|
bail!("found open index writer - unable to finish backup");
|
|
|
|
}
|
|
|
|
|
2019-05-29 08:38:57 +00:00
|
|
|
if state.file_counter == 0 {
|
|
|
|
bail!("backup does not contain valid files (file count == 0)");
|
|
|
|
}
|
|
|
|
|
2020-10-16 07:31:12 +00:00
|
|
|
// check for valid manifest and store stats
|
2020-07-31 05:27:57 +00:00
|
|
|
let stats = serde_json::to_value(state.backup_stat)?;
|
2020-10-16 07:31:12 +00:00
|
|
|
self.datastore.update_manifest(&self.backup_dir, |manifest| {
|
|
|
|
manifest.unprotected["chunk_upload_stats"] = stats;
|
|
|
|
}).map_err(|err| format_err!("unable to update manifest blob - {}", err))?;
|
2020-07-30 10:19:22 +00:00
|
|
|
|
2020-08-11 08:50:41 +00:00
|
|
|
if let Some(base) = &self.last_backup {
|
|
|
|
let path = self.datastore.snapshot_path(&base.backup_dir);
|
|
|
|
if !path.exists() {
|
|
|
|
bail!(
|
|
|
|
"base snapshot {} was removed during backup, cannot finish as chunks might be missing",
|
|
|
|
base.backup_dir
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-04 10:41:59 +00:00
|
|
|
// marks the backup as successful
|
|
|
|
state.finished = true;
|
2020-06-10 12:57:39 +00:00
|
|
|
|
2019-05-15 10:58:55 +00:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2020-10-20 08:08:25 +00:00
|
|
|
/// If verify-new is set on the datastore, this will run a new verify task
|
|
|
|
/// for the backup. If not, this will return and also drop the passed lock
|
|
|
|
/// immediately.
|
|
|
|
pub fn verify_after_complete(&self, snap_lock: Dir) -> Result<(), Error> {
|
|
|
|
self.ensure_finished()?;
|
|
|
|
|
|
|
|
if !self.datastore.verify_new() {
|
|
|
|
// no verify requested, do nothing
|
|
|
|
return Ok(());
|
|
|
|
}
|
|
|
|
|
2020-10-22 06:24:37 +00:00
|
|
|
let worker_id = format!("{}:{}/{}/{:08X}",
|
2020-10-20 08:08:25 +00:00
|
|
|
self.datastore.name(),
|
|
|
|
self.backup_dir.group().backup_type(),
|
|
|
|
self.backup_dir.group().backup_id(),
|
|
|
|
self.backup_dir.backup_time());
|
|
|
|
|
|
|
|
let datastore = self.datastore.clone();
|
|
|
|
let backup_dir = self.backup_dir.clone();
|
|
|
|
|
|
|
|
WorkerTask::new_thread(
|
|
|
|
"verify",
|
|
|
|
Some(worker_id),
|
|
|
|
self.user.clone(),
|
|
|
|
false,
|
|
|
|
move |worker| {
|
|
|
|
worker.log("Automatically verifying newly added snapshot");
|
|
|
|
|
|
|
|
let verified_chunks = Arc::new(Mutex::new(HashSet::with_capacity(1024*16)));
|
|
|
|
let corrupt_chunks = Arc::new(Mutex::new(HashSet::with_capacity(64)));
|
|
|
|
|
|
|
|
if !verify_backup_dir_with_lock(
|
|
|
|
datastore,
|
|
|
|
&backup_dir,
|
|
|
|
verified_chunks,
|
|
|
|
corrupt_chunks,
|
|
|
|
worker.clone(),
|
|
|
|
worker.upid().clone(),
|
2020-10-29 06:59:19 +00:00
|
|
|
None,
|
2020-10-20 08:08:25 +00:00
|
|
|
snap_lock,
|
|
|
|
)? {
|
|
|
|
bail!("verification failed - please check the log for details");
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
},
|
|
|
|
).map(|_| ())
|
|
|
|
}
|
|
|
|
|
2019-05-08 10:41:58 +00:00
|
|
|
pub fn log<S: AsRef<str>>(&self, msg: S) {
|
|
|
|
self.worker.log(msg);
|
|
|
|
}
|
2019-05-09 16:01:24 +00:00
|
|
|
|
2019-05-29 07:35:21 +00:00
|
|
|
pub fn debug<S: AsRef<str>>(&self, msg: S) {
|
|
|
|
if self.debug { self.worker.log(msg); }
|
|
|
|
}
|
|
|
|
|
2019-05-09 16:01:24 +00:00
|
|
|
pub fn format_response(&self, result: Result<Value, Error>) -> Response<Body> {
|
|
|
|
match result {
|
|
|
|
Ok(data) => (self.formatter.format_data)(data, self),
|
|
|
|
Err(err) => (self.formatter.format_error)(err),
|
|
|
|
}
|
|
|
|
}
|
2019-05-15 10:58:55 +00:00
|
|
|
|
|
|
|
/// Raise error if finished flag is not set
|
|
|
|
pub fn ensure_finished(&self) -> Result<(), Error> {
|
|
|
|
let state = self.state.lock().unwrap();
|
|
|
|
if !state.finished {
|
|
|
|
bail!("backup ended but finished flag is not set.");
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2020-10-20 12:18:14 +00:00
|
|
|
/// Return true if the finished flag is set
|
|
|
|
pub fn finished(&self) -> bool {
|
|
|
|
let state = self.state.lock().unwrap();
|
|
|
|
state.finished
|
|
|
|
}
|
|
|
|
|
2019-05-15 10:58:55 +00:00
|
|
|
/// Remove complete backup
|
|
|
|
pub fn remove_backup(&self) -> Result<(), Error> {
|
|
|
|
let mut state = self.state.lock().unwrap();
|
|
|
|
state.finished = true;
|
|
|
|
|
2020-07-29 12:33:11 +00:00
|
|
|
self.datastore.remove_backup_dir(&self.backup_dir, true)?;
|
2019-05-15 10:58:55 +00:00
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
2019-05-08 10:41:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl RpcEnvironment for BackupEnvironment {
|
|
|
|
|
2020-05-18 07:57:35 +00:00
|
|
|
fn result_attrib_mut(&mut self) -> &mut Value {
|
|
|
|
&mut self.result_attributes
|
2019-05-08 10:41:58 +00:00
|
|
|
}
|
|
|
|
|
2020-05-18 07:57:35 +00:00
|
|
|
fn result_attrib(&self) -> &Value {
|
|
|
|
&self.result_attributes
|
2019-05-08 10:41:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn env_type(&self) -> RpcEnvironmentType {
|
|
|
|
self.env_type
|
|
|
|
}
|
|
|
|
|
|
|
|
fn set_user(&mut self, _user: Option<String>) {
|
|
|
|
panic!("unable to change user");
|
|
|
|
}
|
|
|
|
|
|
|
|
fn get_user(&self) -> Option<String> {
|
2020-08-06 13:46:01 +00:00
|
|
|
Some(self.user.to_string())
|
2019-05-08 10:41:58 +00:00
|
|
|
}
|
|
|
|
}
|
2019-05-09 11:06:09 +00:00
|
|
|
|
2019-06-07 11:10:56 +00:00
|
|
|
impl AsRef<BackupEnvironment> for dyn RpcEnvironment {
|
2019-05-09 11:06:09 +00:00
|
|
|
fn as_ref(&self) -> &BackupEnvironment {
|
|
|
|
self.as_any().downcast_ref::<BackupEnvironment>().unwrap()
|
|
|
|
}
|
|
|
|
}
|
2019-06-07 11:10:56 +00:00
|
|
|
|
|
|
|
impl AsRef<BackupEnvironment> for Box<dyn RpcEnvironment> {
|
2019-05-09 16:01:24 +00:00
|
|
|
fn as_ref(&self) -> &BackupEnvironment {
|
|
|
|
self.as_any().downcast_ref::<BackupEnvironment>().unwrap()
|
|
|
|
}
|
|
|
|
}
|