1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::process;
use walkdir::WalkDir;

use detect::Difference;
use error::{SyncError, DescribeIoError};
use archive::{Archive, ArchiveEntries};
use state::{ArchiveEntryPerReplica};
use config::*;

mod progress;
pub use propagate::progress::{ProgressCallback, EmptyProgressCallback, ToCheck};

/// Propagates a change from `master` to every other replica.
pub fn propagate<T, P, PL, AL>(
        difference: &Difference<PL, AL>,
        master: usize,
        archive: &Archive,
        options: &T,
        progress: &P) -> Result<(), SyncError> where T: PropagationOptions, P: ProgressCallback, PL: PathLen, AL: ArchiveLen {
    let ref master_entry = difference.current_state[master];
    let master_path = difference.absolute_path_for_root(master);

    //let ref archive_update = ArchiveUpdateInfo::<PL, AL>::new(difference.path, &difference.roots, archive);

    for (i, replica) in difference.current_state.iter().enumerate() {
        // skip the master
        if i == master { continue; }

        let absolute_path = difference.absolute_path_for_root(i);
        if replica != &ArchiveEntryPerReplica::from(absolute_path.as_ref()) {
            return Err(SyncError::PathModified(absolute_path))
        }

        match *master_entry {
            ArchiveEntryPerReplica::Empty => match *replica {
                ArchiveEntryPerReplica::Empty => { },
                ArchiveEntryPerReplica::File(_) => remove_file(&absolute_path, options)?,
                ArchiveEntryPerReplica::Directory(_) => remove_directory_recursive(&absolute_path, options)?,
                ArchiveEntryPerReplica::Symlink(_) => unimplemented!()
            },
            ArchiveEntryPerReplica::File(_) => match *replica {
                ArchiveEntryPerReplica::Empty => transfer_file(&master_path, &absolute_path, progress)?,
                ArchiveEntryPerReplica::File(_) => transfer_file(&master_path, &absolute_path, progress)?,
                ArchiveEntryPerReplica::Directory(_) => {
                    remove_directory_recursive(&absolute_path, options)?;
                    transfer_file(&master_path, &absolute_path, progress)?;
                },
                ArchiveEntryPerReplica::Symlink(_) => unimplemented!()
            },
            ArchiveEntryPerReplica::Directory(_) => match *replica {
                ArchiveEntryPerReplica::Empty => transfer_directory(&master_path, &absolute_path, progress)?,
                ArchiveEntryPerReplica::File(_) => {
                    remove_file(&absolute_path, options)?;
                    transfer_directory(&master_path, &absolute_path, progress)?;
                },
                ArchiveEntryPerReplica::Directory(_) => {
                    remove_directory_recursive(&absolute_path, options)?;
                    transfer_directory(&master_path, &absolute_path, progress)?;
                },
                ArchiveEntryPerReplica::Symlink(_) => unimplemented!()
            },
            ArchiveEntryPerReplica::Symlink(_) => unimplemented!()
        };
    }

    // Update the archives for this path and its children
    update_archive_for_path::<PL, AL>(&difference.path, archive, &difference.roots)?;

    Ok(())
}

fn remove_file<T>(path: &Path, options: &T) -> Result<(), SyncError> where T: PropagationOptions {
    if !options.should_remove(path) {
        return Err(SyncError::Cancelled);
    }

    info!("Removing file {:?}", path);
    // delegate the actual removal to a callback function
    options.remove_file(path)
}

fn remove_directory_recursive<T>(path: &Path, options: &T) -> Result<(), SyncError> where T: PropagationOptions {
    if !options.should_remove(path) {
        return Err(SyncError::Cancelled);
    }

    info!("Removing directory {:?}", path);
    // delegate the actual removal to a callback function
    options.remove_dir_all(path)
}

fn transfer_file<P>(source: &Path, dest: &Path, progress: &P) -> Result<(), SyncError> where P: ProgressCallback {
    let parent = dest.parent().unwrap();
    if !parent.exists() {
        info!("Creating parent directory {:?}", parent);
        fs::create_dir_all(parent)?;
    }
    info!("Transferring file {:?} to {:?}", source, dest);
    run_rsync(source, dest, progress)
        .describe(|| format!("while copying file from {:?} to {:?}", source, dest))?;

    Ok(())
}

fn transfer_directory<P>(source: &Path, dest: &Path, progress: &P) -> Result<(), SyncError> where P: ProgressCallback {
    fs::create_dir_all(dest)?;

    info!("Copying directory {:?} to {:?}", source, dest);
    run_rsync(source, dest, progress)
        .describe(|| format!("while copying directory from {:?} to {:?}", source, dest))?;

    Ok(())
}

fn run_rsync<P>(source: &Path, dest: &Path, progress: &P) -> io::Result<()> where P: ProgressCallback {
    let append_slash = source.metadata()?.is_dir();
    let mut source_str = source.to_string_lossy().into_owned();
    if append_slash {
        source_str.push_str("/");
    }
    let mut command = process::Command::new("/usr/local/bin/rsync");
    let command = command.arg("-a")
        .arg("--info=progress2")
        .arg(source_str)
        .stdout(process::Stdio::piped())
        .arg(dest.to_string_lossy().as_ref());
    let mut command = command.spawn()?;

    {
        let stdout = command.stdout.as_mut().unwrap();
        let reader = io::BufReader::new(stdout);

        progress::parse_from_stdout(reader, progress)?;
    }

    let status = command.wait()?;
    println!("{}", status);
    if !status.success() {
        panic!("Error in rsync");
    }

    Ok(())
}

/// Look at the archives in this path, and if it is a directory remove all descendants.
fn update_archive_for_path<PL: PathLen, AL: ArchiveLen>(relative_path: &Path, archive: &Archive, roots: &[PathBuf]) -> Result<(), SyncError> {
    let directory = relative_path.parent().unwrap();
    let mut archive_file = archive.for_directory(directory);
    let mut entries: ArchiveEntries<AL> = archive_file.read()?;

    // remove old archive information (only needed when `relative_path` is a directory)
    {
        let replicas = entries.get(relative_path);
        if let Some(replicas) = replicas {
            let is_dir = any_directories_in(&replicas);

            if is_dir {
                debug!("There are descendant directories inside {:?} that need to be cleared from the archive", relative_path);
                let mut stack = Vec::new();
                stack.push(Archive::hash(relative_path));
                loop {
                    let item = match stack.pop() { Some(v) => v, None => break };

                    trace!("Scanning archive file {:?} for descendant directories", item);
                    let mut archive_file = archive.for_hashed_directory(item);
                    let entries: ArchiveEntries<AL> = archive_file.read()?;

                    let dirs = entries.iter().filter(|&(_, replicas)| {
                        any_directories_in(&replicas)
                    }).map(|(hash, _)| *hash);
                    for dir in dirs {
                        stack.push(dir);
                    }

                    archive_file.remove_all()?;
                }
            } else {
                debug!("{:?} is not a directory, no pruning needed", relative_path);
            }
        } else {
            debug!("No entry {:?} in archive {}, no pruning needed", relative_path, archive_file);
        }
    }

    info!("Updating {:?} in {}", relative_path, archive_file);

    // update archives for this exact path
    let replicas = ArchiveEntryPerReplica::from_roots::<AL>(&roots, relative_path);
    entries.insert(relative_path, replicas);
    archive_file.write(&mut entries)?;

    // update archives for children of this path, only if it is a directory
    let first_root = roots[0].join(relative_path);
    if first_root.is_dir() {
        for entry in WalkDir::new(&first_root) {
            let entry = entry?;
            if entry.metadata()?.is_dir() {
                let dir_relative_path = relative_path.join(entry.path().strip_prefix(&first_root).unwrap().as_os_str());
                let mut entries = ArchiveEntries::<AL>::empty();

                for entry in entry.path().read_dir()? {
                    let entry = entry?;
                    if !entry.metadata()?.is_dir() {

                        let child_path = relative_path.join(entry.path().strip_prefix(&first_root).unwrap().as_os_str());
                        let replicas = ArchiveEntryPerReplica::from_roots::<AL>(&roots, &child_path);
                        entries.insert(&child_path, replicas)
                    }
                }

                let mut archive_file = archive.for_directory(&dir_relative_path);
                info!("Updating {}", archive_file);
                archive_file.write(&mut entries)?;
            }
        }
    }

    Ok(())
}

/// Searches to see if a directory exists at any of the replicas
fn any_directories_in(replicas: &[ArchiveEntryPerReplica]) -> bool {
    replicas.iter().any(|replica| {
        if let ArchiveEntryPerReplica::Directory(_) = *replica {
            true
        } else {
            false
        }
    })
}

/// PropagationOptions allow the client to customize how files are transferred/deleted.
pub trait PropagationOptions {
    /// return false to cancel deleting a directory
    fn should_remove(&self, &Path) -> bool;

    /// return `SyncError::Cancelled` to cancel deleting the file,
    /// otherwise delete the file/move it to the trash.
    /// This must return an error if the file was not removed successfully.
    fn remove_file(&self, &Path) -> Result<(), SyncError>;

    /// Delete the directory and its contents
    /// This must return an error if the directory was not removed successfully.
    /// Ignoring errors will mean that Ubiquity writes to the archive files when
    /// the replicas are still out of sync, resulting in an inconsistent state.
    fn remove_dir_all(&self, &Path) -> Result<(), SyncError>;

}

/// A zero-sized struct with a simple implementation of PropagationOptions.
pub struct DefaultPropagationOptions;

impl PropagationOptions for DefaultPropagationOptions {
    fn should_remove(&self, _: &Path) -> bool {
        true
    }
    fn remove_file(&self, path: &Path) -> Result<(), SyncError> {
        fs::remove_file(path)?;
        Ok(())
    }
    fn remove_dir_all(&self, path: &Path) -> Result<(), SyncError> {
        fs::remove_dir_all(path).describe(|| format!("when removing directory {:?}", path))?;
        Ok(())
    }
}