Files
duplicati/Duplicati/GUI/Program.cs
T
kenneth.skovhede@gmail.com b81d63ea73 Fixed a bug with async uploads that caused some files to no be uploaded.
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
2010-09-25 12:02:14 +00:00

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);
}
}
}