Files
duplicati/Duplicati/Library/Modules/Builtin/RunScript.cs
T
kenneth@hexad.dk 6822fc69fa Update issue #29
This adds the email feature to Duplicati.
Currently there is no GUI for this, just advanced options.
The options supported are:
--send-mail-to="Name <test@example.com>, Other <test2@example.com>, test3@example.com"
--send-mail-from="Backup Admin <admin@example.com>
--send-mail-subject="Duplicati %OPERATIONNAME% Report"
--send-mail-body="%RESULT%%"
--send-mail-url="tls://smtp.google.com:587"
--send-mail-username="username"
--send-mail-password="password"
--send-mail-level="Warning"
--send-mail-any-operation=true

The only required setting is --send-mail-to.
If the --send-mail-from is not set, it will use the host of the "to" and set "no-reply@" in front.
E.g. --send-mail-to="name@example.com" will cause the sender to be "no-reply@example.com".

If the --send-mail-url is not set, a DNS lookup will find the MX record for the first recipient domain,
and send directly to that. The url can be either "smtp://server:port" or "tis://server:port".

The --send-mail-subject and --send-mail-body parameters can contain placeholders like %RESULT%.
Each option will be exported like this, e.g. %volsize% or %use-ssl%. The special placeholders are:
%RESULT% - The result/log of the backup
%OPERATIONNAME% - The name of the operation, usually "Backup", but could be "Restore" etc.
%REMOTEURL% - The backend url
%LOCALPATH% - The path to the local folders involved (i.e. the folders being backed up)

The --send-mail-level can be either "Success", "Warning" or "Error". Default "Success" means all backups generate an email. "Warning" will only send an email if the backup caused a warning or an error. "Error" will only send an email if the backup failed.

The --send-mail-any-operation can be set to "true" if all operations (e.g. "Restore", "Cleanup", "DeleteAllButN", ...) should generate an email. By default only backup operations will send an email.

git-svn-id: https://duplicati.googlecode.com/svn/trunk@1436 59da171f-624f-0410-aa54-27559c288bec
2012-08-21 09:35:55 +00:00

343 lines
15 KiB
C#

// Copyright (C) 2011, 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Collections.Generic;
using Duplicati.Library.Utility;
namespace Duplicati.Library.Modules.Builtin
{
public class RunScript : Duplicati.Library.Interface.IGenericCallbackModule
{
private const string STARTUP_OPTION = "run-script-before";
private const string FINISH_OPTION = "run-script-after";
private const string REQUIRED_OPTION = "run-script-before-required";
private const string TIMEOUT_OPTION = "run-script-timeout";
private const string DEFAULT_TIMEOUT = "60s";
private string m_requiredScript = null;
private string m_startScript = null;
private string m_finishScript = null;
private int m_timeout = 0;
private string m_operationName;
private string m_remoteurl;
private string[] m_localpath;
private IDictionary<string, string> m_options;
#region IGenericModule implementation
public void Configure(IDictionary<string, string> commandlineOptions)
{
commandlineOptions.TryGetValue(STARTUP_OPTION, out m_startScript);
commandlineOptions.TryGetValue(REQUIRED_OPTION, out m_requiredScript);
commandlineOptions.TryGetValue(FINISH_OPTION, out m_finishScript);
string t;
if (!commandlineOptions.TryGetValue(TIMEOUT_OPTION, out t))
t = DEFAULT_TIMEOUT;
m_timeout = (int)Utility.Timeparser.ParseTimeSpan(t).TotalMilliseconds;
m_options = commandlineOptions;
}
public string Key { get { return "runscript"; } }
public string DisplayName { get { return Strings.RunScript.DisplayName; } }
public string Description { get { return Strings.RunScript.Description; } }
public bool LoadAsDefault { get { return true; } }
public IList<Duplicati.Library.Interface.ICommandLineArgument> SupportedCommands
{
get
{
return new List<Duplicati.Library.Interface.ICommandLineArgument>(new Duplicati.Library.Interface.ICommandLineArgument[] {
new Duplicati.Library.Interface.CommandLineArgument(STARTUP_OPTION, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Path, Strings.RunScript.StartupoptionShort, Strings.RunScript.StartupoptionLong),
new Duplicati.Library.Interface.CommandLineArgument(FINISH_OPTION, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Path, Strings.RunScript.FinishoptionShort, Strings.RunScript.FinishoptionLong),
new Duplicati.Library.Interface.CommandLineArgument(REQUIRED_OPTION, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Path, Strings.RunScript.RequiredoptionShort, Strings.RunScript.RequiredoptionLong),
new Duplicati.Library.Interface.CommandLineArgument(TIMEOUT_OPTION, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Timespan, Strings.RunScript.TimeoutoptionShort, Strings.RunScript.TimeoutoptionLong, DEFAULT_TIMEOUT),
});
}
}
#endregion
#region IGenericCallbackModule implementation
public void OnStart(string operationname, ref string remoteurl, ref string[] localpath)
{
m_operationName = operationname;
m_remoteurl = remoteurl;
m_localpath = localpath;
if (!string.IsNullOrEmpty(m_requiredScript))
Execute(m_requiredScript, "BEFORE", m_operationName, ref m_remoteurl, ref m_localpath, m_timeout, true, m_options, null);
if (!string.IsNullOrEmpty(m_startScript))
Execute(m_startScript, "BEFORE", m_operationName, ref m_remoteurl, ref m_localpath, m_timeout, false, m_options, null);
}
public void OnFinish (object result)
{
if (string.IsNullOrEmpty(m_finishScript))
return;
using (TempFile tmpfile = new TempFile())
{
SerializeResult(tmpfile, result);
Execute(m_finishScript, "AFTER", m_operationName, ref m_remoteurl, ref m_localpath, m_timeout, false, m_options, tmpfile);
}
}
#endregion
public static void SerializeResult(string file, object result)
{
using (StreamWriter sw = new StreamWriter(file))
{
if (result == null)
{
sw.WriteLine("null?");
}
else if (result is System.Collections.IEnumerable)
{
System.Collections.IEnumerable ie = (System.Collections.IEnumerable)result;
System.Collections.IEnumerator ien = ie.GetEnumerator();
ien.Reset();
while (ien.MoveNext())
{
object c = ien.Current;
if (c == null)
continue;
if (c.GetType().IsGenericType && !c.GetType().IsGenericTypeDefinition && c.GetType().GetGenericTypeDefinition() == typeof(KeyValuePair<,>))
{
object key = c.GetType().GetProperty("Key").GetValue(c, null);
object value = c.GetType().GetProperty("Value").GetValue(c, null);
sw.WriteLine("{0}: {1}", key, value);
}
else
sw.WriteLine(c.ToString());
}
}
else if (result.GetType().IsArray)
{
Array a = (Array)result;
for (int i = a.GetLowerBound(0); i <= a.GetUpperBound(0); i++)
{
object c = a.GetValue(i);
if (c == null)
continue;
if (c.GetType().IsGenericType && !c.GetType().IsGenericTypeDefinition && c.GetType().GetGenericTypeDefinition() == typeof(KeyValuePair<,>))
{
object key = c.GetType().GetProperty("Key").GetValue(c, null);
object value = c.GetType().GetProperty("Value").GetValue(c, null);
sw.WriteLine("{0}: {1}", key, value);
}
else
sw.WriteLine(c.ToString());
}
}
else if (result is Exception)
{
//No localization, must be parseable by script
Exception e = (Exception)result;
sw.WriteLine("Failed: {0}", e.Message);
sw.WriteLine("Details: {0}", e.ToString());
}
else
{
sw.WriteLine(result.ToString());
}
}
}
private static void Execute(string scriptpath, string eventname, string operationname, ref string remoteurl, ref string[] localpath, int timeout, bool requiredScript, IDictionary<string, string> options, string datafile)
{
try
{
System.Diagnostics.ProcessStartInfo psi = new System.Diagnostics.ProcessStartInfo(scriptpath);
psi.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
psi.CreateNoWindow = true;
psi.UseShellExecute = false;
psi.RedirectStandardOutput = true;
psi.RedirectStandardError = true;
foreach(KeyValuePair<string, string> kv in options)
psi.EnvironmentVariables["DUPLICATI__" + kv.Key.Replace('-', '_')] = kv.Value;
psi.EnvironmentVariables["DUPLICATI__EVENTNAME"] = eventname;
psi.EnvironmentVariables["DUPLICATI__OPERATIONNAME"] = operationname;
psi.EnvironmentVariables["DUPLICATI__REMOTEURL"] = remoteurl;
if (localpath != null)
psi.EnvironmentVariables["DUPLICATI__LOCALPATH"] = string.Join(System.IO.Path.PathSeparator.ToString(), localpath);
string stderr = null;
string stdout = null;
if (!string.IsNullOrEmpty(datafile))
psi.EnvironmentVariables["DUPLICATI__RESULTFILE"] = datafile;
using(System.Diagnostics.Process p = System.Diagnostics.Process.Start(psi))
{
ConsoleDataHandler cs = new ConsoleDataHandler(p);
if (timeout <= 0)
p.WaitForExit();
else
p.WaitForExit(timeout);
if (requiredScript)
{
if (!p.HasExited)
throw new Exception(string.Format(Strings.RunScript.ScriptTimeoutError, scriptpath));
else if (p.ExitCode != 0)
throw new Exception(string.Format(Strings.RunScript.InvalidExitCodeError, scriptpath, p.ExitCode));
}
if (p.HasExited)
{
stderr = cs.StandardError;
stdout = cs.StandardOutput;
if (p.ExitCode != 0)
Logging.Log.WriteMessage(string.Format(Strings.RunScript.InvalidExitCodeError, scriptpath, p.ExitCode), Duplicati.Library.Logging.LogMessageType.Warning);
}
else
{
Logging.Log.WriteMessage(string.Format(Strings.RunScript.ScriptTimeoutError, scriptpath), Duplicati.Library.Logging.LogMessageType.Warning);
}
}
if (!string.IsNullOrEmpty(stderr))
Logging.Log.WriteMessage(string.Format(Strings.RunScript.StdErrorReport, scriptpath, stderr), Duplicati.Library.Logging.LogMessageType.Warning);
//We only allow setting parameters on startup
if (eventname == "BEFORE" && stdout != null)
{
foreach(string rawline in stdout.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries))
{
string line = rawline.Trim();
if (!line.StartsWith("--"))
continue; //Ingore anything that does not start with --
line = line.Substring(2);
int lix = line.IndexOf('=');
if (lix == 0) //Skip --= as that makes no sense
continue;
string key;
string value;
if (lix < 0)
{
key = line.Trim();
value = "";
}
else
{
key = line.Substring(0, lix).Trim();
value = line.Substring(lix + 1).Trim();
if (value.Length >= 2 && value.StartsWith("\"") && value.EndsWith("\""))
value = value.Substring(1, value.Length - 2);
}
if (string.Equals(key, "remoteurl", StringComparison.InvariantCultureIgnoreCase))
{
remoteurl = value;
}
else if (string.Equals(key, "localpath", StringComparison.InvariantCultureIgnoreCase))
{
localpath = value.Split(System.IO.Path.PathSeparator);
}
else if (
string.Equals(key, "eventname", StringComparison.InvariantCultureIgnoreCase) ||
string.Equals(key, "operationname", StringComparison.InvariantCultureIgnoreCase) ||
string.Equals(key, "main-action", StringComparison.InvariantCultureIgnoreCase) ||
key == ""
)
{
//Ignore
}
else
options[key] = value;
}
}
}
catch (Exception ex)
{
Logging.Log.WriteMessage(string.Format(Strings.RunScript.ScriptExecuteError, scriptpath, ex.Message), Duplicati.Library.Logging.LogMessageType.Warning, ex);
if (requiredScript)
throw;
}
}
private class ConsoleDataHandler
{
public ConsoleDataHandler(System.Diagnostics.Process p)
{
p.OutputDataReceived += new System.Diagnostics.DataReceivedEventHandler(HandleOutputDataReceived);
p.ErrorDataReceived += new System.Diagnostics.DataReceivedEventHandler(HandleErrorDataReceived);
p.BeginErrorReadLine();
p.BeginOutputReadLine();
}
private readonly StringBuilder m_standardOutput = new StringBuilder();
private readonly StringBuilder m_standardError = new StringBuilder();
private readonly object m_lock = new object();
private void HandleOutputDataReceived (object sender, System.Diagnostics.DataReceivedEventArgs e)
{
lock(m_lock)
m_standardOutput.AppendLine(e.Data);
}
private void HandleErrorDataReceived (object sender, System.Diagnostics.DataReceivedEventArgs e)
{
lock(m_lock)
m_standardError.AppendLine(e.Data);
}
public string StandardOutput
{
get
{
lock(m_lock)
return m_standardOutput.ToString().Trim();
}
}
public string StandardError
{
get
{
lock(m_lock)
return m_standardError.ToString().Trim();
}
}
}
}
}