Fixed a protential race condition with warning messages and file counts (cosmetic only) when using async transfers. Spelling correction, Seperator -> Separator. Changed so a restore operation can also supply advanced options. Changed the format of timestamps in filenames to follow the Duplicity convention, which removes the need for --time-separator and --use-short-filenames. Moved the URL utility into Core, to avoid compilation errors with Monodevelop. Reorganized the runner to better support the LiveControls on all operations rather than only for backup/restore. Update issue #210 Status: Fixed I have now changed the time format to follow the Duplicity naming convention. This means that all filenames are now generated with the time set as "yyyyMMddTHHmmssZ". This format is more portable across all platforms, and easily sortable/readable. The drawback is that the Z indicates UTC time, so if one looks at the filenames, the times are not local. The motivation for this is to avoid trouble with daylight savings time offsets. There is full backwards compatibility as long as backups have not used more than one character for --time-separator (only possible via commandline). This new format means that the options --time-separator and --use-short-filenames are now deprecated and removed from the user interface. It is possible to access them from the advanced options page, along with the --use-old-filenames option. If anyone needs these options for some reason, let me know, otherwise they will be removed in a later version. git-svn-id: https://duplicati.googlecode.com/svn/trunk@523 59da171f-624f-0410-aa54-27559c288bec
438 lines
20 KiB
C#
438 lines
20 KiB
C#
#region Disclaimer / License
|
|
// Copyright (C) 2010, 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.Windows.Forms;
|
|
using System.Data.LightDatamodel;
|
|
using System.Drawing;
|
|
using Duplicati.Datamodel;
|
|
|
|
namespace Duplicati.GUI
|
|
{
|
|
static class Program
|
|
{
|
|
/// <summary>
|
|
/// The name of the environment variable that holds the path to the data folder used by Duplicati
|
|
/// </summary>
|
|
public const string DATAFOLDER_ENV_NAME = "DUPLICATI_HOME";
|
|
|
|
/// <summary>
|
|
/// The environment variable that holdes the database key used to encrypt the SQLite database
|
|
/// </summary>
|
|
public const string DB_KEY_ENV_NAME = "DUPLICATI_DB_KEY";
|
|
|
|
/// <summary>
|
|
/// Gets the folder where Duplicati data is stored
|
|
/// </summary>
|
|
public static string DATAFOLDER { get { return Library.Core.Utility.AppendDirSeparator(Environment.ExpandEnvironmentVariables("%" + DATAFOLDER_ENV_NAME + "%").TrimStart('"').TrimEnd('"')); } }
|
|
|
|
/// <summary>
|
|
/// A flag indicating if database encryption is in use
|
|
/// </summary>
|
|
public static bool UseDatabaseEncryption;
|
|
|
|
/// <summary>
|
|
/// This is the only access to the database
|
|
/// </summary>
|
|
public static IDataFetcherWithRelations DataConnection;
|
|
|
|
/// <summary>
|
|
/// This is the lock to be used before manipulating the shared resources
|
|
/// </summary>
|
|
public static object MainLock = new object();
|
|
|
|
/// <summary>
|
|
/// This is the scheduling thread
|
|
/// </summary>
|
|
public static Scheduler Scheduler;
|
|
|
|
/// <summary>
|
|
/// This is the working thread
|
|
/// </summary>
|
|
public static WorkerThread<IDuplicityTask> WorkThread;
|
|
|
|
/// <summary>
|
|
/// The path to the file that contains the current database
|
|
/// </summary>
|
|
public static string DatabasePath;
|
|
|
|
/// <summary>
|
|
/// The actual runner, do not call directly. Only used for events.
|
|
/// </summary>
|
|
public static DuplicatiRunner Runner;
|
|
|
|
/// <summary>
|
|
/// The controller interface for pause/resume and throttle options
|
|
/// </summary>
|
|
public static LiveControls LiveControl;
|
|
|
|
/// <summary>
|
|
/// The main form that contains the tray icon
|
|
/// </summary>
|
|
public static MainForm DisplayHelper;
|
|
|
|
/// <summary>
|
|
/// The single instance keeper
|
|
/// </summary>
|
|
public static SingleInstance SingleInstance;
|
|
|
|
/// <summary>
|
|
/// The main entry point for the application.
|
|
/// </summary>
|
|
[STAThread]
|
|
static void Main(string[] args)
|
|
{
|
|
Application.EnableVisualStyles();
|
|
Application.SetCompatibleTextRenderingDefault(false);
|
|
|
|
//If we are on Windows, append the bundled "win-tools" programs to the search path
|
|
//We add it last, to allow the user to override with other versions
|
|
if (!Library.Core.Utility.IsClientLinux)
|
|
{
|
|
Environment.SetEnvironmentVariable("PATH",
|
|
Environment.GetEnvironmentVariable("PATH") +
|
|
System.IO.Path.PathSeparator.ToString() +
|
|
System.IO.Path.Combine(
|
|
System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location),
|
|
"win-tools")
|
|
);
|
|
}
|
|
|
|
Library.Core.UrlUtillity.ErrorHandler = new Duplicati.Library.Core.UrlUtillity.ErrorHandlerDelegate(DisplayURLOpenError);
|
|
|
|
//If we are on windows we encrypt the database by default
|
|
//We do not encrypt on Linux as most distros use a SQLite library without encryption support,
|
|
//Linux users can use an encrypted home folder, or install a SQLite library with encryption support
|
|
if (!Library.Core.Utility.IsClientLinux && string.IsNullOrEmpty(Environment.GetEnvironmentVariable(DB_KEY_ENV_NAME)))
|
|
{
|
|
//Note that the password here is a default password and public knowledge
|
|
//
|
|
//The purpose of this is to prevent casual read of the database, as well
|
|
// as protect from harddisk string scans, not to protect from determined
|
|
// attacks.
|
|
//
|
|
//If you desire better security, start Duplicati once with the commandline option
|
|
// --unencrypted-database to decrypt the database.
|
|
//Then set the environment variable DUPLICATI_DB_KEY to the desired key,
|
|
// and run Duplicati again without the --unencrypted-database option
|
|
// to re-encrypt it with the new key
|
|
//
|
|
//If you change the key, please note that you need to supply the same
|
|
// key when restoring the setup, as the setup being backed up will
|
|
// be encrypted as well.
|
|
Environment.SetEnvironmentVariable(DB_KEY_ENV_NAME, "Duplicati_Key_42");
|
|
}
|
|
|
|
|
|
//Find commandline options here for handling special startup cases
|
|
Dictionary<string, string> commandlineOptions = CommandLine.CommandLineParser.ExtractOptions(new List<string>(args));
|
|
|
|
//Set the %DUPLICATI_HOME% env variable, if it is not already set
|
|
if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable(DATAFOLDER_ENV_NAME)))
|
|
{
|
|
#if DEBUG
|
|
//debug mode uses a lock file located in the app folder
|
|
Environment.SetEnvironmentVariable(DATAFOLDER_ENV_NAME, System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location));
|
|
#else
|
|
bool portableMode = commandlineOptions.ContainsKey("portable-mode") ? Library.Core.Utility.ParseBool(commandlineOptions["portable-mode"], true) : false;
|
|
|
|
if (portableMode)
|
|
{
|
|
//Portable mode uses a data folder in the application home dir
|
|
Environment.SetEnvironmentVariable(DATAFOLDER_ENV_NAME, System.IO.Path.Combine(System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "data"));
|
|
}
|
|
else
|
|
{
|
|
//Normal release mode uses the systems "Application Data" folder
|
|
Environment.SetEnvironmentVariable(DATAFOLDER_ENV_NAME, System.IO.Path.Combine(System.Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), Application.ProductName));
|
|
}
|
|
#endif
|
|
}
|
|
|
|
try
|
|
{
|
|
try
|
|
{
|
|
//This will also create Program.DATAFOLDER if it does not exist
|
|
SingleInstance = new SingleInstance(Application.ProductName, Program.DATAFOLDER);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show(string.Format(Strings.Program.StartupFailure, ex.ToString()), Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
return;
|
|
}
|
|
|
|
if (!SingleInstance.IsFirstInstance)
|
|
{
|
|
//Linux shows this output
|
|
Console.WriteLine(Strings.Program.AnotherInstanceDetected);
|
|
return;
|
|
}
|
|
|
|
Version sqliteVersion = new Version((string)SQLiteLoader.SQLiteConnectionType.GetProperty("SQLiteVersion").GetValue(null, null));
|
|
if (sqliteVersion < new Version(3, 6, 3))
|
|
{
|
|
//The official Mono SQLite provider is also broken with less than 3.6.3
|
|
MessageBox.Show(string.Format(Strings.Program.WrongSQLiteVersion, sqliteVersion, "3.6.3"), Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
return;
|
|
}
|
|
|
|
//Create the connection instance
|
|
System.Data.IDbConnection con = (System.Data.IDbConnection)Activator.CreateInstance(SQLiteLoader.SQLiteConnectionType);
|
|
|
|
try
|
|
{
|
|
DatabasePath = System.IO.Path.Combine(Program.DATAFOLDER, "Duplicati.sqlite");
|
|
if (!System.IO.Directory.Exists(System.IO.Path.GetDirectoryName(DatabasePath)))
|
|
System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(DatabasePath));
|
|
|
|
#if DEBUG
|
|
//Default is to not use encryption for debugging
|
|
Program.UseDatabaseEncryption = commandlineOptions.ContainsKey("unencrypted-database") ? !Library.Core.Utility.ParseBool(commandlineOptions["unencrypted-database"], true) : false;
|
|
#else
|
|
Program.UseDatabaseEncryption = commandlineOptions.ContainsKey("unencrypted-database") ? !Library.Core.Utility.ParseBool(commandlineOptions["unencrypted-database"], true) : true;
|
|
#endif
|
|
con.ConnectionString = "Data Source=" + DatabasePath;
|
|
|
|
//Attempt to open the database, handling any encryption present
|
|
OpenDatabase(con);
|
|
|
|
DatabaseUpgrader.UpgradeDatebase(con, DatabasePath);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
//Unwrap the reflection exceptions
|
|
if (ex is System.Reflection.TargetInvocationException && ex.InnerException != null)
|
|
ex = ex.InnerException;
|
|
|
|
MessageBox.Show(string.Format(Strings.Program.DatabaseOpenError, ex.Message), Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
return;
|
|
}
|
|
|
|
DataConnection = new DataFetcherWithRelations(new SQLiteDataProvider(con));
|
|
|
|
if (!string.IsNullOrEmpty(new Datamodel.ApplicationSettings(DataConnection).DisplayLanguage))
|
|
try
|
|
{
|
|
System.Threading.Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.GetCultureInfo(new Datamodel.ApplicationSettings(DataConnection).DisplayLanguage);
|
|
System.Threading.Thread.CurrentThread.CurrentUICulture = System.Globalization.CultureInfo.GetCultureInfo(new Datamodel.ApplicationSettings(DataConnection).DisplayLanguage);
|
|
}
|
|
catch(Exception ex)
|
|
{
|
|
MessageBox.Show(string.Format(Strings.Program.LanguageSelectionError, ex.Message), Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
//This is non-fatal, just keep running with system default language
|
|
}
|
|
|
|
#if DEBUG
|
|
//Log various information in the logfile
|
|
string logfile = System.IO.Path.Combine(Application.StartupPath, "Duplicati.debug.log");
|
|
if (System.IO.File.Exists(logfile))
|
|
System.IO.File.Delete(logfile);
|
|
|
|
Duplicati.Library.Logging.Log.LogLevel = Duplicati.Library.Logging.LogMessageType.Profiling;
|
|
Duplicati.Library.Logging.Log.CurrentLog = new Duplicati.Library.Logging.StreamLog(logfile);
|
|
#endif
|
|
|
|
LiveControl = new LiveControls(new ApplicationSettings(DataConnection));
|
|
LiveControl.StateChanged += new EventHandler(LiveControl_StateChanged);
|
|
LiveControl.ThreadPriorityChanged += new EventHandler(LiveControl_ThreadPriorityChanged);
|
|
LiveControl.ThrottleSpeedChanged += new EventHandler(LiveControl_ThrottleSpeedChanged);
|
|
|
|
Runner = new DuplicatiRunner();
|
|
WorkThread = new WorkerThread<IDuplicityTask>(new WorkerThread<IDuplicityTask>.ProcessItemDelegate(Runner.ExecuteTask), LiveControl.State == LiveControls.LiveControlState.Paused);
|
|
Scheduler = new Scheduler(DataConnection, WorkThread, MainLock);
|
|
|
|
DataConnection.AfterDataConnection += new DataConnectionEventHandler(DataConnection_AfterDataConnection);
|
|
|
|
DisplayHelper = new MainForm();
|
|
DisplayHelper.InitialArguments = args;
|
|
|
|
Application.Run(DisplayHelper);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show(string.Format(Strings.Program.SeriousError, ex.ToString()), Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
}
|
|
|
|
|
|
if (Scheduler != null)
|
|
Scheduler.Terminate(true);
|
|
if (WorkThread != null)
|
|
WorkThread.Terminate(true);
|
|
if (SingleInstance != null)
|
|
SingleInstance.Dispose();
|
|
|
|
#if DEBUG
|
|
using(Duplicati.Library.Logging.Log.CurrentLog as Duplicati.Library.Logging.StreamLog)
|
|
Duplicati.Library.Logging.Log.CurrentLog = null;
|
|
#endif
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles a change in the LiveControl and updates the Runner
|
|
/// </summary>
|
|
/// <param name="sender"></param>
|
|
/// <param name="e"></param>
|
|
private static void LiveControl_ThreadPriorityChanged(object sender, EventArgs e)
|
|
{
|
|
if (LiveControl.ThreadPriority == null)
|
|
Runner.UnsetThreadPriority();
|
|
else
|
|
Runner.SetThreadPriority(LiveControl.ThreadPriority.Value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles a change in the LiveControl and updates the Runner
|
|
/// </summary>
|
|
/// <param name="sender"></param>
|
|
/// <param name="e"></param>
|
|
private static void LiveControl_ThrottleSpeedChanged(object sender, EventArgs e)
|
|
{
|
|
if (LiveControl.DownloadLimit == null)
|
|
Runner.SetDownloadLimit(null);
|
|
else
|
|
Runner.SetDownloadLimit(LiveControl.DownloadLimit.Value.ToString() + "b");
|
|
|
|
if (LiveControl.UploadLimit == null)
|
|
Runner.SetUploadLimit(null);
|
|
else
|
|
Runner.SetUploadLimit(LiveControl.UploadLimit.Value.ToString() + "b");
|
|
}
|
|
|
|
/// <summary>
|
|
/// This event handler updates the trayicon menu with the current state of the runner.
|
|
/// </summary>
|
|
static void LiveControl_StateChanged(object sender, EventArgs e)
|
|
{
|
|
switch (LiveControl.State)
|
|
{
|
|
case LiveControls.LiveControlState.Paused:
|
|
WorkThread.Pause();
|
|
Runner.Pause();
|
|
break;
|
|
case LiveControls.LiveControlState.Running:
|
|
WorkThread.Resume();
|
|
Runner.Resume();
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
private static void DataConnection_AfterDataConnection(object sender, DataActions action)
|
|
{
|
|
if (action == DataActions.Insert || action == DataActions.Update)
|
|
Scheduler.Reschedule();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a localized name for a task type
|
|
/// </summary>
|
|
/// <param name="type"></param>
|
|
/// <returns></returns>
|
|
public static string LocalizeTaskType(DuplicityTaskType type)
|
|
{
|
|
switch (type)
|
|
{
|
|
case DuplicityTaskType.FullBackup:
|
|
return Strings.TaskType.FullBackup;
|
|
case DuplicityTaskType.IncrementalBackup:
|
|
return Strings.TaskType.IncrementalBackup;
|
|
case DuplicityTaskType.ListActualFiles:
|
|
return Strings.TaskType.ListActualFiles;
|
|
case DuplicityTaskType.ListBackupEntries:
|
|
return Strings.TaskType.ListBackupEntries;
|
|
case DuplicityTaskType.ListBackups:
|
|
return Strings.TaskType.ListBackups;
|
|
case DuplicityTaskType.ListFiles:
|
|
return Strings.TaskType.ListFiles;
|
|
case DuplicityTaskType.RemoveAllButNFull:
|
|
return Strings.TaskType.RemoveAllButNFull;
|
|
case DuplicityTaskType.RemoveOlderThan:
|
|
return Strings.TaskType.RemoveOlderThan;
|
|
case DuplicityTaskType.Restore:
|
|
return Strings.TaskType.Restore;
|
|
case DuplicityTaskType.RestoreSetup:
|
|
return Strings.TaskType.RestoreSetup;
|
|
default:
|
|
return type.ToString();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper method with logic to handle opening a database in possibly encrypted format
|
|
/// </summary>
|
|
/// <param name="con">The SQLite connection object</param>
|
|
internal static void OpenDatabase(System.Data.IDbConnection con)
|
|
{
|
|
bool noEncryption = !Program.UseDatabaseEncryption;
|
|
string password = Environment.GetEnvironmentVariable(DB_KEY_ENV_NAME);
|
|
|
|
System.Reflection.MethodInfo setPwdMethod = con.GetType().GetMethod("SetPassword", new Type[] { typeof(string) });
|
|
string attemptedPassword;
|
|
|
|
if (noEncryption || string.IsNullOrEmpty(password))
|
|
attemptedPassword = null; //No encryption specified, attempt to open without
|
|
else
|
|
attemptedPassword = password; //Encryption specified, attempt to open with
|
|
|
|
setPwdMethod.Invoke(con, new object[] { attemptedPassword });
|
|
|
|
try
|
|
{
|
|
//Attempt to open in preferred state
|
|
con.Open();
|
|
}
|
|
catch
|
|
{
|
|
try
|
|
{
|
|
//We can't try anything else without a password
|
|
if (string.IsNullOrEmpty(password))
|
|
throw;
|
|
|
|
//Open failed, now try the reverse
|
|
if (attemptedPassword == null)
|
|
attemptedPassword = password;
|
|
else
|
|
attemptedPassword = null;
|
|
|
|
setPwdMethod.Invoke(con, new object[] { attemptedPassword });
|
|
con.Open();
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
|
|
//If the db is not open now, it won't open
|
|
if (con.State != System.Data.ConnectionState.Open)
|
|
throw; //Report original error
|
|
|
|
//The open method succeeded with the non-default method, now change the password
|
|
System.Reflection.MethodInfo changePwdMethod = con.GetType().GetMethod("ChangePassword", new Type[] { typeof(string) });
|
|
changePwdMethod.Invoke(con, new object[] { noEncryption ? null : password });
|
|
}
|
|
}
|
|
|
|
private static void DisplayURLOpenError(string message)
|
|
{
|
|
System.Windows.Forms.MessageBox.Show(message, Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
}
|
|
}
|
|
} |