#region Disclaimer / License // Copyright (C) 2008, Kenneth Skovhede // http://www.hexad.dk, opensource@hexad.dk // // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Lesser General Public // License as published by the Free Software Foundation; either // version 2.1 of the License, or (at your option) any later version. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA // #endregion using System; using System.Collections.Generic; using System.Text; using System.IO; namespace Duplicati.Library.Main.RSync { /// /// This class wraps the process of creating a diff for an entire folder /// public class RSyncDir : IDisposable { private static readonly string SIGNATURE_ROOT = "signature"; private static readonly string CONTENT_ROOT = "snapshot"; private static readonly string DELTA_ROOT = "diff"; //private static readonly string DACL_ROOT = "dacl"; private static readonly string DELETED_FILES = "deleted_files.txt"; private static readonly string DELETED_FOLDERS = "deleted_folders.txt"; private static readonly string ADDED_FOLDERS = "added_folders.txt"; /// /// This is the folder being backed up /// public string m_sourcefolder; /// /// This is the folder containing signatures for the previous backup /// public string m_basefolder; /// /// This is the folder that gets filled with the next backup /// public Core.TempFolder m_targetfolder; /// /// This is a list of existing file signatures. /// Key is path to the file. /// value is path to the signature file. /// private Dictionary m_oldSignatures; /// /// This is a list of existing folders. /// private Dictionary m_oldFolders; /// /// This is the list of added files /// private Dictionary m_newfiles; /// /// This is the list of modified files /// private Dictionary m_modifiedFiles; /// /// This is the list of deleted files /// private List m_deletedfiles; /// /// This is the list of added folders /// private List m_newfolders; /// /// This is the list of deleted folders /// private List m_deletedfolders; /// /// The total number of files found /// private long m_totalfiles; /// /// The number of files examined /// private long m_examinedfiles; /// /// The combined size of the examined files /// private long m_examinedfilesize; /// /// The number of files that are found to be modified /// private long m_diffedfiles; /// /// The combined size of all the modified files /// private long m_diffedfilessize; /// /// The combines size of all delta files generated /// private long m_diffsize; /// /// The number of files added /// private long m_addedfiles; /// /// The combined size of all added files /// private long m_addedfilessize; /// /// Statistics reporting /// private CommunicationStatistics m_stat; /// /// Flag indicating if the final values are written to the signature file /// private bool m_finalized = false; /// /// This is a list of unprocessed files, used in multipass runs /// private List m_unproccesed; public RSyncDir(string sourcefolder, string basefolder, CommunicationStatistics stat) { if (!System.IO.Path.IsPathRooted(sourcefolder)) sourcefolder = System.IO.Path.GetFullPath(sourcefolder); if (!System.IO.Path.IsPathRooted(basefolder)) sourcefolder = System.IO.Path.GetFullPath(basefolder); m_sourcefolder = Core.Utility.AppendDirSeperator(sourcefolder); m_basefolder = Core.Utility.AppendDirSeperator(basefolder); m_targetfolder = new Duplicati.Library.Core.TempFolder(); m_stat = stat; PrepareTargetFolder(); BuildIndex(); } private void PrepareTargetFolder() { System.IO.Directory.CreateDirectory(System.IO.Path.Combine(m_targetfolder, SIGNATURE_ROOT)); System.IO.Directory.CreateDirectory(System.IO.Path.Combine(m_targetfolder, CONTENT_ROOT)); System.IO.Directory.CreateDirectory(System.IO.Path.Combine(m_targetfolder, DELTA_ROOT)); } private void BuildIndex() { if (m_basefolder == null || !System.IO.Directory.Exists(m_basefolder)) { m_oldSignatures = new Dictionary(); m_oldFolders = new Dictionary(); return; } m_basefolder = Core.Utility.AppendDirSeperator(m_basefolder); m_oldSignatures = new Dictionary(); string sigfolder = Core.Utility.AppendDirSeperator(System.IO.Path.Combine(m_basefolder, SIGNATURE_ROOT)); foreach (string s in Core.Utility.EnumerateFiles(sigfolder)) m_oldSignatures.Add(s.Substring(sigfolder.Length), s); m_oldFolders = new Dictionary(); if (System.IO.File.Exists(System.IO.Path.Combine(m_basefolder, ADDED_FOLDERS))) foreach(string s in System.IO.File.ReadAllLines(System.IO.Path.Combine(m_basefolder, ADDED_FOLDERS))) m_oldFolders.Add(s, s); } public void InitiateMultiPassDiff(bool full, Core.FilenameFilter filter) { if (full) { m_oldFolders = new Dictionary(); m_oldSignatures = new Dictionary(); } m_newfiles = new Dictionary(); m_modifiedFiles = new Dictionary(); m_deletedfiles = new List(); m_newfolders = new List(); m_deletedfolders = new List(); //TODO: Figure out how to make this faster, but still random m_unproccesed = Core.Utility.EnumerateFileSystemEntries(m_sourcefolder, filter); m_totalfiles = m_unproccesed.Count; string dirmarker = System.IO.Path.DirectorySeparatorChar.ToString(); for(int i = 0; i < m_unproccesed.Count; i++) { string s = m_unproccesed[i]; if (s.EndsWith(dirmarker)) { string relpath = s.Substring(m_sourcefolder.Length); if (!m_oldFolders.ContainsKey(relpath)) m_newfolders.Add(relpath); else m_oldFolders.Remove(relpath); m_unproccesed.RemoveAt(i); i--; } } m_deletedfolders = new List(); m_deletedfolders.AddRange(m_oldFolders.Keys); } public void FinalizeMultiPass(Compression.Compression sigfile) { if (!m_finalized) { if (m_unproccesed.Count == 0) m_deletedfiles.AddRange(m_oldSignatures.Keys); if (m_deletedfiles.Count > 0) { System.IO.StreamWriter sw = new StreamWriter(sigfile.AddStream(DELETED_FILES)); foreach (string s in m_deletedfiles) sw.WriteLine(s); sw.Flush(); } if (m_deletedfolders.Count > 0) { System.IO.StreamWriter sw = new StreamWriter(sigfile.AddStream(DELETED_FOLDERS)); foreach (string s in m_deletedfolders) sw.WriteLine(s); sw.Flush(); } if (m_newfolders.Count > 0) { System.IO.StreamWriter sw = new StreamWriter(sigfile.AddStream(ADDED_FOLDERS)); foreach (string s in m_newfolders) sw.WriteLine(s); sw.Flush(); } if (m_stat is BackupStatistics) { BackupStatistics bs = m_stat as BackupStatistics; bs.DeletedFiles = m_deletedfiles.Count; bs.DeletedFolders = m_deletedfolders.Count; bs.ModifiedFiles = m_diffedfiles; bs.AddedFiles = m_addedfiles; bs.ExaminedFiles = m_examinedfiles; bs.SizeOfModifiedFiles = m_diffedfilessize; bs.SizeOfAddedFiles = m_addedfilessize; bs.SizeOfExaminedFiles = m_examinedfilesize; bs.UnprocessedFiles = m_unproccesed.Count; } m_finalized = true; } } public bool MakeMultiPassDiff(Compression.Compression sigfile, string zipfile, long volumesize) { if (m_unproccesed == null) throw new Exception("Multi pass is not initialized"); Random r = new Random(); long totalSize = 0; using (Compression.Compression c = new Duplicati.Library.Compression.Compression(m_targetfolder, zipfile)) { while (m_unproccesed.Count > 0 && totalSize < volumesize) { int next = r.Next(0, m_unproccesed.Count); string s = m_unproccesed[next]; m_unproccesed.RemoveAt(next); try { using (System.IO.FileStream fs = System.IO.File.Open(s, FileMode.Open, FileAccess.Read, FileShare.Read)) if (ProccessDiff(fs, s, sigfile)) totalSize = AddFileToCompression(fs, s, c); } catch (Exception ex) { if (m_stat != null) m_stat.LogError("Failed to process file: \"" + s + "\", Error message: " + ex.Message); Logging.Log.WriteMessage("Failed to process file: \"" + s + "\"", Duplicati.Library.Logging.LogMessageType.Error, ex); } } if (m_unproccesed.Count == 0) FinalizeMultiPass(sigfile); } return m_unproccesed.Count == 0; } private bool ProccessDiff(System.IO.FileStream fs, string s, Compression.Compression sigfile) { string relpath = s.Substring(m_sourcefolder.Length); string target = System.IO.Path.Combine(System.IO.Path.Combine(m_targetfolder, SIGNATURE_ROOT), relpath); using (System.IO.MemoryStream ms = new MemoryStream()) { m_examinedfilesize += fs.Length; m_examinedfiles++; SharpRSync.Interface.GenerateSignature(fs, ms); if (!m_oldSignatures.ContainsKey(relpath)) { Core.Utility.CopyStream(ms, sigfile.AddStream(target), true); m_newfiles.Add(s, null); return true; } else { ms.Position = 0; bool equals; using (System.IO.FileStream fs2 = System.IO.File.OpenRead(m_oldSignatures[relpath])) equals = Core.Utility.CompareStreams(fs2, ms, true); if (!equals) { Core.Utility.CopyStream(ms, sigfile.AddStream(target), true); m_modifiedFiles.Add(s, null); m_oldSignatures.Remove(relpath); return true; } else { m_oldSignatures.Remove(relpath); return false; } } } } private long AddFileToCompression(System.IO.FileStream fs, string s, Compression.Compression c) { fs.Position = 0; if (m_modifiedFiles.ContainsKey(s)) { string relpath = s.Substring(m_sourcefolder.Length); string target = System.IO.Path.Combine(System.IO.Path.Combine(m_targetfolder, DELTA_ROOT), relpath); string signature = System.IO.Path.Combine(System.IO.Path.Combine(m_basefolder, SIGNATURE_ROOT), relpath); using (System.IO.FileStream sigfs = System.IO.File.OpenRead(signature)) { System.IO.Stream s3 = c.AddStream(target); SharpRSync.Interface.GenerateDelta(sigfs, fs, s3); //TODO: s3.Length the size of the archive //m_diffsize += s3.Length; m_diffedfilessize += fs.Length; m_diffedfiles++; } m_modifiedFiles.Remove(s); } else { string relpath = s.Substring(m_sourcefolder.Length); string target = System.IO.Path.Combine(System.IO.Path.Combine(m_targetfolder, CONTENT_ROOT), relpath); Core.Utility.CopyStream(fs, c.AddStream(target)); m_addedfiles++; m_addedfilessize += fs.Length; m_newfiles.Remove(s); } return c.Size; } public bool HasChanges { get { return m_newfiles.Count > 0 || m_modifiedFiles.Count > 0; } } public List EnumerateSourceFolders() { List folders = Core.Utility.EnumerateFolders(m_sourcefolder); for (int i = 0; i < folders.Count; i++) folders[i] = System.IO.Path.Combine(System.IO.Path.Combine(m_targetfolder, SIGNATURE_ROOT), folders[i].Substring(m_sourcefolder.Length)); return folders; } public void Restore(string destination, List patchfolders) { Patch(destination, m_basefolder); if (patchfolders != null) foreach (string s in patchfolders) Patch(destination, s); } public void Patch(string destination, string patch) { string deletedfiles = System.IO.Path.Combine(patch, DELETED_FILES); if (System.IO.File.Exists(deletedfiles)) foreach (string s in System.IO.File.ReadAllLines(deletedfiles)) { string target = System.IO.Path.Combine(destination, s.Trim()); if (System.IO.File.Exists(target)) { try { System.IO.File.Delete(target); } catch (Exception ex) { if (m_stat != null) m_stat.LogError("Failed to delete file: \"" + target + "\", Error message: " + ex.Message); Logging.Log.WriteMessage("Failed to delete file: " + target, Duplicati.Library.Logging.LogMessageType.Warning, ex); } } else { Logging.Log.WriteMessage("Filed marked for deletion did not exist: " + target, Duplicati.Library.Logging.LogMessageType.Warning); } } string deletedfolders = System.IO.Path.Combine(patch, DELETED_FOLDERS); if (System.IO.File.Exists(deletedfolders)) { //Make sure subfolders are deleted first string[] folderlist = System.IO.File.ReadAllLines(deletedfolders); Array.Sort(folderlist); Array.Reverse(folderlist); foreach (string s in folderlist) { string target = System.IO.Path.Combine(destination, s.Trim()); if (System.IO.Directory.Exists(target)) try { System.IO.Directory.Delete(target, false); } catch (Exception ex) { if (m_stat != null) m_stat.LogError("Failed to remove folder: \"" + target + "\", Error message: " + ex.Message); Logging.Log.WriteMessage("Failed to remove folder: " + target, Duplicati.Library.Logging.LogMessageType.Warning, ex); } } } string addedfolders = System.IO.Path.Combine(patch, ADDED_FOLDERS); if (System.IO.File.Exists(addedfolders)) { //Make sure subfolders are deleted first string[] folderlist = System.IO.File.ReadAllLines(addedfolders); Array.Sort(folderlist); Array.Reverse(folderlist); foreach (string s in folderlist) { string target = System.IO.Path.Combine(destination, s.Trim()); if (!System.IO.Directory.Exists(target)) try { System.IO.Directory.CreateDirectory(target); } catch (Exception ex) { if (m_stat != null) m_stat.LogError("Failed to create folder: \"" + target + "\", Error message: " + ex.Message); Logging.Log.WriteMessage("Failed to create folder: " + target, Duplicati.Library.Logging.LogMessageType.Warning, ex); } } } string sourcefolder = Core.Utility.AppendDirSeperator(System.IO.Path.Combine(patch, CONTENT_ROOT)); destination = Core.Utility.AppendDirSeperator(destination); //TODO: Handle file access exceptions foreach (string s in Core.Utility.EnumerateFiles(sourcefolder)) { string relpath = s.Substring(sourcefolder.Length); string target = System.IO.Path.Combine(destination, relpath); try { System.IO.File.Copy(s, target); } catch (Exception ex) { if (m_stat != null) m_stat.LogError("Failed to restore file: \"" + relpath + "\", Error message: " + ex.Message); Logging.Log.WriteMessage("Failed to restore file " + relpath, Duplicati.Library.Logging.LogMessageType.Error, ex); } } sourcefolder = Core.Utility.AppendDirSeperator(System.IO.Path.Combine(patch, DELTA_ROOT)); foreach (string s in Core.Utility.EnumerateFiles(sourcefolder)) { string relpath = s.Substring(sourcefolder.Length); string target = System.IO.Path.Combine(destination, relpath); //string source = System.IO.Path.Combine(sourcefolder, relpath) string tempfile = System.IO.Path.GetTempFileName(); try { SharpRSync.Interface.PatchFile(target, s, tempfile); System.IO.File.Delete(target); System.IO.File.Move(tempfile, target); } catch (Exception ex) { if (m_stat != null) m_stat.LogError("Failed to restore file: \"" + relpath + "\", Error message: " + ex.Message); Logging.Log.WriteMessage("Failed to restore file " + relpath, Duplicati.Library.Logging.LogMessageType.Error, ex); System.IO.File.Delete(target); System.IO.File.Delete(tempfile); } } } #region IDisposable Members public void Dispose() { if (m_targetfolder != null) { m_targetfolder.Dispose(); m_targetfolder = null; } } #endregion /// /// Compares two files to see if they are identical /// /// One file /// Another file /// True if they are binary equals, false otherwise private static bool CompareFiles(string file1, string file2) { using (System.IO.FileStream fs1 = new System.IO.FileStream(file1, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.Read)) using (System.IO.FileStream fs2 = new System.IO.FileStream(file2, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.Read)) return Core.Utility.CompareStreams(fs1, fs2, true); } public static void MergeSignatures(string basefolder, string updatefolder) { basefolder = Core.Utility.AppendDirSeperator(basefolder); updatefolder = Core.Utility.AppendDirSeperator(updatefolder); Dictionary deletedfiles = new Dictionary(); if (System.IO.File.Exists(System.IO.Path.Combine(basefolder, DELETED_FILES))) foreach (string s in System.IO.File.ReadAllLines(System.IO.Path.Combine(basefolder, DELETED_FILES))) deletedfiles.Add(s, s); if (System.IO.File.Exists(System.IO.Path.Combine(updatefolder, DELETED_FILES))) foreach (string s in System.IO.File.ReadAllLines(System.IO.Path.Combine(updatefolder, DELETED_FILES))) System.IO.File.Delete(System.IO.Path.Combine(System.IO.Path.Combine(basefolder, SIGNATURE_ROOT), s)); Dictionary addedfolders = new Dictionary(); if (System.IO.File.Exists(System.IO.Path.Combine(basefolder, ADDED_FOLDERS))) foreach (string s in System.IO.File.ReadAllLines(System.IO.Path.Combine(basefolder, ADDED_FOLDERS))) addedfolders.Add(s, s); Dictionary deletedfolders = new Dictionary(); if (System.IO.File.Exists(System.IO.Path.Combine(basefolder, DELETED_FOLDERS))) foreach (string s in System.IO.File.ReadAllLines(System.IO.Path.Combine(basefolder, DELETED_FOLDERS))) deletedfolders.Add(s,s ); if (System.IO.File.Exists(System.IO.Path.Combine(updatefolder, DELETED_FOLDERS))) foreach (string s in System.IO.File.ReadAllLines(System.IO.Path.Combine(updatefolder, DELETED_FOLDERS))) { if (addedfolders.ContainsKey(s)) addedfolders.Remove(s); deletedfolders[s] = s; } if (System.IO.File.Exists(System.IO.Path.Combine(updatefolder, ADDED_FOLDERS))) foreach (string s in System.IO.File.ReadAllLines(System.IO.Path.Combine(updatefolder, ADDED_FOLDERS))) addedfolders[s] = s; List updates = Core.Utility.EnumerateFiles(System.IO.Path.Combine(updatefolder, SIGNATURE_ROOT)); foreach(string s in updates) { string relpath = s.Substring(updatefolder.Length); string target = System.IO.Path.Combine(basefolder, relpath); if (!System.IO.Directory.Exists(System.IO.Path.GetDirectoryName(target))) System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(target)); System.IO.File.Copy(s, target, true); //If they are created again relpath = relpath.Substring(SIGNATURE_ROOT.Length + 1); if (deletedfiles.ContainsKey(relpath)) deletedfiles.Remove(relpath); } List delfiles = new List(deletedfiles.Values); if (System.IO.File.Exists(System.IO.Path.Combine(updatefolder, DELETED_FILES))) foreach (string s in System.IO.File.ReadAllLines(System.IO.Path.Combine(updatefolder, DELETED_FILES))) if (!delfiles.Contains(s)) delfiles.Add(s); if (!System.IO.Directory.Exists(basefolder)) System.IO.Directory.CreateDirectory(basefolder); List dfo = new List(deletedfolders.Values); List dfi = new List(deletedfiles.Values); List afo = new List(addedfolders.Values); /*dfo.Sort(); dfi.Sort(); afo.Sort();*/ System.IO.File.WriteAllLines(System.IO.Path.Combine(basefolder, DELETED_FOLDERS), dfo.ToArray()); System.IO.File.WriteAllLines(System.IO.Path.Combine(basefolder, DELETED_FILES), dfi.ToArray()); System.IO.File.WriteAllLines(System.IO.Path.Combine(basefolder, ADDED_FOLDERS), afo.ToArray()); } } }