#region Disclaimer / License // Copyright (C) 2011, Kenneth Bergeron, IAP Worldwide Services, Inc // NOAA :: National Marine Fisheries Service // // 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.Linq; using System.Windows.Forms; namespace Duplicati.Scheduler { /// /// List the files in a backup /// public partial class FileListDialog : Form { private string itsURI; private Dictionary itsOptions; /// /// List the files in a backup /// /// Job name /// Source /// Used by Duplicati public FileListDialog(string aName, string aURI, Dictionary aOptions) { InitializeComponent(); itsURI = aURI; itsOptions = aOptions; this.toolStripLabel1.Text = aName; this.TimeToolStripLabel.Text = aOptions.ContainsKey("restore-time") ? aOptions["restore-time"] : string.Empty; } /// /// Initialize the display /// protected override void OnShown(EventArgs e) { base.OnShown(e); MakeTree(itsURI, itsOptions); } /// /// Locate a node by name /// /// Node name /// Where to look /// The node or null if not found private TreeNode FindNode(string aName, TreeNodeCollection aNodeCollection) { foreach (TreeNode N in aNodeCollection) if (N.Text.Equals(aName)) return N; return null; } /// /// Nodes /// public enum NodeType { Backup, // a root node File, // a plain file Folder // a directory } /// /// Replace the leading number with the corresponding source root /// /// List of source files /// Index /// Corresponding source root private string Expand(string[] aSource, string aPart) { int ix; if (aSource != null && int.TryParse(aPart, out ix) && ix < aSource.Length) return aSource[ix]; return aPart; } // A list, handy for finding things without recursing the tree private List itsNodes = new List(); /// /// A new tree node contains the name and the icon /// /// File name /// Kind /// new decorated node private TreeNode NewNode(string aName, NodeType aType) { TreeNode Result = new TreeNode(aName); // Get the correct icon from this cool set if (OSGeo.MapGuide.Maestro.ResourceEditors.ShellIcons.Supported) { switch (aType) { case NodeType.Backup: Result.ImageIndex = Result.SelectedImageIndex = OSGeo.MapGuide.Maestro.ResourceEditors.ShellIcons.GetShellIcon("XXX.DupBaK"); break; case NodeType.File: Result.ImageIndex = Result.SelectedImageIndex = OSGeo.MapGuide.Maestro.ResourceEditors.ShellIcons.GetShellIcon(aName); break; case NodeType.Folder: Result.ImageIndex = Result.SelectedImageIndex = OSGeo.MapGuide.Maestro.ResourceEditors.ShellIcons.GetFolderIcon(false); break; } } itsNodes.Add(Result); // Easy access later return Result; } /// /// Get the source folders from Duplicati /// /// Source /// Duplicati options /// a list of source folders private string[] ListSourceFolders(string aURI, Dictionary aOptions) { string[] Result = null; Exception Ex = null; do { Utility.Tools.Background((Action)delegate() { try { Result = Duplicati.Library.Main.Interface.ListSourceFolders(aURI, FixOptions(aOptions)); } catch (System.Security.Cryptography.CryptographicException Exc) { Ex = Exc; } }); // If there was a crypto error, give user a chance to enter a different password // I'm not sure this works if (Ex != null && Ex is System.Security.Cryptography.CryptographicException) { if (MessageBox.Show("The password was incorrect, enter a different one?", "PASSWORD", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.No) return null; aOptions["Checksum"] = System.Convert.ToBase64String(EnterPassDialog.FetchProtectedPassword()); } } while (Ex != null); return Result; } /// /// Convert internal options to Duplicati options /// /// /// private Dictionary FixOptions(Dictionary aOptions) { Dictionary Result = new Dictionary(aOptions); // Try to find history entry if (aOptions.ContainsKey("restore-time")) { using (Duplicati.Scheduler.Data.HistoryDataSet hds = new Duplicati.Scheduler.Data.HistoryDataSet()) { hds.Load(); Duplicati.Scheduler.Data.HistoryDataSet.HistoryRow hRow = hds.History.FindByNameActionDate( this.toolStripLabel1.Text, DateTime.Parse(aOptions["restore-time"])); if (hRow != null) { Result["Checksum"] = System.Convert.ToBase64String( hRow.Checksum ); Result["encryption-module"] = hRow.CheckMod; } } } // Duplicati should use SecureString if (Result.ContainsKey("Checksum")) { // Unprotect will only work if process is the same user as it was protected, which this one should be. Result["passphrase"] = System.Text.ASCIIEncoding.ASCII.GetString(Utility.Tools.Unprotect(System.Convert.FromBase64String(Result["Checksum"]))); Result.Remove("Checksum"); } return Result; } /// /// Ask duplicati to list the backup's files /// /// Source /// Duplicati options /// Folders form backup /// private List ListCurrentFiles(string aURI, Dictionary aOptions, string[] aSourceFolders) { List Result = null; // Background will run the action in a separate thread and keep the forms pump going Utility.Tools.Background((Action) delegate() { Result = new List(Duplicati.Library.Main.Interface.ListCurrentFiles(aURI, FixOptions(aOptions))); } ); // If there is only one source folder, Duplicati does not prepend indexes, so add them manually if (aSourceFolders != null && aSourceFolders.Length == 1) { for (int i = 0; i < Result.Count; i++) { if (!System.Text.RegularExpressions.Regex.Match(Result[i], @"^[0-9]*\\").Success) Result[i] = @"0\" + Result[i]; } } return Result; } // Create the file tree private void MakeTree(string aURI, Dictionary aOptions) { this.Cursor = Cursors.WaitCursor; this.RestoreToolStripButton.Enabled = false; treeView.Nodes.Clear(); treeView.Visible = false; this.ProgressGroupBox.Visible = true; this.progressBar.Style = ProgressBarStyle.Marquee; this.MessageTextBox.Text = "Accessing files"; string[] Sources = ListSourceFolders(aURI, aOptions); if (Sources == null) return; this.progressBar.Style = ProgressBarStyle.Continuous; // Basically stolen from Duplicati try { treeView.BeginUpdate(); // Add root node icon if (OSGeo.MapGuide.Maestro.ResourceEditors.ShellIcons.Supported) { OSGeo.MapGuide.Maestro.ResourceEditors.ShellIcons.AddIcon("XXX.DupBaK", this.Icon.ToBitmap()); treeView.ImageList = OSGeo.MapGuide.Maestro.ResourceEditors.ShellIcons.ImageList; } List Files = ListCurrentFiles(aURI, aOptions, Sources); this.progressBar.Maximum = Files.Count; this.progressBar.Value = 0; foreach (string s in Files) { TreeNodeCollection c = treeView.Nodes; string[] parts = s.Split(System.IO.Path.DirectorySeparatorChar); string[] sParts = (string[])parts.Clone(); parts[0] = Expand(Sources, parts[0]); string Name = string.Empty; for (int i = 0; i < parts.Length; i++) { NodeType nt = (i == 0) ? NodeType.Backup : ((i == parts.Length - 1) ? NodeType.File : NodeType.Folder); Name += sParts[i]; if (nt != NodeType.File) Name += System.IO.Path.DirectorySeparatorChar; if (parts[i] != "") { TreeNode t = FindNode(parts[i], c); if (t == null) { t = NewNode(parts[i], nt); t.Tag = Name; c.Add(t); } c = t.Nodes; } } this.progressBar.Value++; Application.DoEvents(); } treeView.Sort(); } finally { treeView.EndUpdate(); this.ProgressGroupBox.Visible = false; treeView.Visible = true; CheckedNodes = 0; this.RestoreToolStripButton.Enabled = true; this.Cursor = Cursors.Default; } } private int CheckedNodes = 0; /// /// Duplicati style progress /// private void OperationProgress(Duplicati.Library.Main.Interface aCaller, Duplicati.Library.Main.DuplicatiOperation aOperation, Duplicati.Library.Main.DuplicatiOperationMode aSpecificOperation, int aProgress, int aSubprogress, string aMessage, string aSubmessage) { this.BeginInvoke((Action)delegate() { if (aProgress < 0) { this.progressBar.Style = ProgressBarStyle.Marquee; } else { this.progressBar.Style = ProgressBarStyle.Continuous; this.progressBar.Value = Math.Min(this.progressBar.Maximum, aProgress); } this.MessageTextBox.Text = aMessage + ":" + aSubmessage; this.ProgressGroupBox.Update(); }); } /// /// Call Duplicati for the restore /// /// Source file /// Place to put it /// Duplicati Options private void Restore(string aSource, string aTarget, Dictionary aOptions) { this.ProgressGroupBox.Visible = true; this.ProgressGroupBox.BringToFront(); this.progressBar.Style = ProgressBarStyle.Marquee; this.RestoreToolStripButton.Enabled = false; this.treeView.Enabled = false; string Result = null; Utility.Tools.Background((Action) delegate() { using (Duplicati.Library.Main.Interface i = new Duplicati.Library.Main.Interface(aSource, FixOptions(aOptions))) Result = i.Restore(new string[] { aTarget }); }); MessageBox.Show(Result); this.ProgressGroupBox.Visible = false; this.RestoreToolStripButton.Enabled = true; this.treeView.Enabled = true; } /// /// Restore button /// private void RestoreToolStripButton_Click(object sender, EventArgs e) { if (CheckedNodes <= 0) { MessageBox.Show("No files or folders checked."); return; } // Get where user wants to restore if (this.folderBrowserDialog.ShowDialog() == DialogResult.Cancel) return; string Items = string.Join(System.IO.Path.PathSeparator.ToString(), itsNodes.Where(Q => Q.Checked).Select(Q => (string)Q.Tag).ToArray()); if (Items.Length == 0) // Shouldn't happen { MessageBox.Show("No files selected."); CheckedNodes = 0; return; } // Do the restore try { itsOptions["file-to-restore"] = Items; Restore(itsURI, folderBrowserDialog.SelectedPath, itsOptions); MessageBox.Show("Restore to " + folderBrowserDialog.SelectedPath + " Done."); } catch (Exception Ex) { MessageBox.Show("Error restoring: " + Ex.Message); } finally { this.ProgressGroupBox.Visible = false; } } /// /// Check changes, keep count /// private void treeView_AfterCheck(object sender, TreeViewEventArgs e) { CheckedNodes = e.Node.Checked ? CheckedNodes + 1 : CheckedNodes - 1; } } }