// Copyright (C) 2018, The Duplicati Team
// http://www.duplicati.com, info@duplicati.com
//
// 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.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Duplicati.Library.Interface;
using Duplicati.Library.Modules.Builtin.ResultSerialization;
using Duplicati.Library.Utility;
namespace Duplicati.Library.Modules.Builtin
{
///
/// A helper module that contains all shared code used in the various reporting modules
///
public abstract class ReportHelper : Interface.IGenericCallbackModule
{
///
/// The tag used for logging
///
private static readonly string LOGTAG = Logging.Log.LogTagFromType();
///
/// Name of the option used to specify subject
///
protected abstract string SubjectOptionName { get; }
///
/// Name of the option used to specify the body
///
protected abstract string BodyOptionName { get; }
///
/// Name of the option used to specify the level on which the operation is activated
///
protected abstract string ActionLevelOptionName { get; }
///
/// Name of the option used to specify if reports are sent for other operations than backups
///
protected abstract string ActionOnAnyOperationOptionName { get; }
///
/// Name of the option used to specify the log level
///
protected abstract string LogLevelOptionName { get; }
///
/// Name of the option used to specify the log filter
///
protected abstract string LogFilterOptionName { get; }
///
/// Name of the option used to specify the maximum number of log lines to include
///
protected abstract string LogLinesOptionName { get; }
///
/// Name of the option used to the output format
///
protected abstract string ResultFormatOptionName { get; }
///
/// The default subject or title line
///
protected virtual string DEFAULT_SUBJECT { get; }= "Duplicati %OPERATIONNAME% report for %backup-name%";
///
/// The default report level
///
protected virtual string DEFAULT_LEVEL { get; } = "all";
///
/// The default report body
///
protected virtual string DEFAULT_BODY { get; } = "%RESULT%";
///
/// The default maximum number of log lines
///
protected virtual int DEFAULT_LOGLINES { get; } = 100;
///
/// The default log level
///
protected virtual Logging.LogMessageType DEFAULT_LOG_LEVEL { get; } = Logging.LogMessageType.Warning;
///
/// The default export format
///
protected virtual ResultExportFormat DEFAULT_EXPORT_FORMAT { get; } = ResultExportFormat.Duplicati;
///
/// The module key
///
public abstract string Key { get; }
///
/// The module display name
///
public abstract string DisplayName { get; }
///
/// The module description
///
public abstract string Description { get; }
///
/// The module default load setting
///
public abstract bool LoadAsDefault { get; }
///
/// The list of supported commands
///
public abstract IList SupportedCommands { get; }
///
/// Returns the format used by the serializer
///
protected ResultExportFormat ExportFormat => m_resultFormatSerializer.Format;
///
/// The cached name of the operation
///
protected string m_operationname;
///
/// The cached remote url
///
protected string m_remoteurl;
///
/// The cached local path
///
protected string[] m_localpath;
///
/// The cached set of options
///
protected IDictionary m_options;
///
/// The parsed result level
///
protected string m_parsedresultlevel = string.Empty;
///
/// The maximum number of log lines to include
///
protected int m_maxmimumLogLines;
///
/// A value indicating if this instance is configured
///
private bool m_isConfigured;
///
/// The mail subject
///
private string m_subject;
///
/// The mail body
///
private string m_body;
///
/// The mail send level
///
private string[] m_levels;
///
/// True to send all operations
///
private bool m_sendAll;
///
/// The log scope that should be disposed
///
private IDisposable m_logscope;
///
/// The log storage
///
private Utility.FileBackedStringList m_logstorage;
///
/// Serializer to use when serializing the message.
///
private IResultFormatSerializer m_resultFormatSerializer;
///
/// Configures the module
///
/// true, if module should be used, false otherwise.
/// A set of commandline options passed to Duplicati
protected abstract bool ConfigureModule(IDictionary commandlineOptions);
///
/// Sends the email message
///
/// The subject line.
/// The message body.
protected abstract void SendMessage(string subject, string body);
///
/// This method is the interception where the module can interact with the execution environment and modify the settings.
///
/// A set of commandline options passed to Duplicati
public void Configure(IDictionary commandlineOptions)
{
if (!ConfigureModule(commandlineOptions))
return;
m_isConfigured = true;
commandlineOptions.TryGetValue(SubjectOptionName, out m_subject);
commandlineOptions.TryGetValue(BodyOptionName, out m_body);
m_options = commandlineOptions;
string tmp;
commandlineOptions.TryGetValue(ActionLevelOptionName, out tmp);
if (!string.IsNullOrEmpty(tmp))
m_levels =
tmp
.Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries)
.Where(x => !string.IsNullOrWhiteSpace(x))
.Select(x => x.Trim())
.ToArray();
if (m_levels == null || m_levels.Length == 0)
m_levels =
DEFAULT_LEVEL
.Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries)
.Where(x => !string.IsNullOrWhiteSpace(x))
.Select(x => x.Trim())
.ToArray();
m_sendAll = Utility.Utility.ParseBoolOption(commandlineOptions, ActionOnAnyOperationOptionName);
ResultExportFormat resultFormat;
if (!commandlineOptions.TryGetValue(ResultFormatOptionName, out var tmpResultFormat))
resultFormat = DEFAULT_EXPORT_FORMAT;
else if (!Enum.TryParse(tmpResultFormat, true, out resultFormat))
resultFormat = DEFAULT_EXPORT_FORMAT;
m_resultFormatSerializer = ResultFormatSerializerProvider.GetSerializer(resultFormat);
commandlineOptions.TryGetValue(LogLinesOptionName, out var loglinestr);
if (!int.TryParse(loglinestr, out m_maxmimumLogLines))
m_maxmimumLogLines = DEFAULT_LOGLINES;
if (string.IsNullOrEmpty(m_subject))
m_subject = DEFAULT_SUBJECT;
if (string.IsNullOrEmpty(m_body))
m_body = DEFAULT_BODY;
m_options.TryGetValue(LogFilterOptionName, out var logfilterstring);
var filter = Utility.FilterExpression.ParseLogFilter(logfilterstring);
var logLevel = Utility.Utility.ParseEnumOption(m_options, LogLevelOptionName, DEFAULT_LOG_LEVEL);
m_logstorage = new FileBackedStringList();
m_logscope = Logging.Log.StartScope(m => m_logstorage.Add(m.AsString(true)), m => {
if (filter.Matches(m.FilterTag, out var result, out var match))
return result;
else if (m.Level < logLevel)
return false;
return true;
});
}
///
/// Called when the operation starts
///
/// The full name of the operation
/// The remote backend url
/// The local path, if required
public virtual void OnStart(string operationname, ref string remoteurl, ref string[] localpath)
{
m_operationname = operationname;
m_remoteurl = remoteurl;
m_localpath = localpath;
}
///
/// Helper method to perform template expansion
///
/// The expanded template.
/// The input template.
/// The result object.
/// If set to true, the result is intended for a subject or title line.
protected virtual string ReplaceTemplate(string input, object result, bool subjectline)
{
// For JSON, ignore the template and just use the contents
if (ExportFormat == ResultExportFormat.Json && !subjectline)
{
var extra = new Dictionary();
if (input.IndexOf("%OPERATIONNAME%", StringComparison.OrdinalIgnoreCase) >= 0)
extra["OperationName"] = m_operationname;
if (input.IndexOf("%REMOTEURL%", StringComparison.OrdinalIgnoreCase) >= 0)
extra["RemoteUrl"] = m_remoteurl;
if (input.IndexOf("%LOCALPATH%", StringComparison.OrdinalIgnoreCase) >= 0 && m_localpath != null)
extra["LocalPath"] = string.Join(System.IO.Path.PathSeparator.ToString(), m_localpath);
if (input.IndexOf("%PARSEDRESULT%", StringComparison.OrdinalIgnoreCase) >= 0)
extra["ParsedResult"] = m_parsedresultlevel;
if (input.IndexOf("%backup-name%", StringComparison.OrdinalIgnoreCase) >= 0)
{
if (m_options.ContainsKey("backup-name"))
extra["backup-name"] = m_options["backup-name"];
else
extra["backup-name"] = System.IO.Path.GetFileNameWithoutExtension(Duplicati.Library.Utility.Utility.getEntryAssembly().Location);
}
foreach (KeyValuePair kv in m_options)
if (input.IndexOf($"%{kv.Key}%", StringComparison.OrdinalIgnoreCase) >= 0)
extra[kv.Key] = kv.Value;
return m_resultFormatSerializer.Serialize(result, LogLines, extra);
}
else
{
input = Regex.Replace(input, "\\%OPERATIONNAME\\%", m_operationname ?? "", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
input = Regex.Replace(input, "\\%REMOTEURL\\%", m_remoteurl ?? "", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
input = Regex.Replace(input, "\\%LOCALPATH\\%", m_localpath == null ? "" : string.Join(System.IO.Path.PathSeparator.ToString(), m_localpath), RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
input = Regex.Replace(input, "\\%PARSEDRESULT\\%", m_parsedresultlevel ?? "", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
if (subjectline)
{
input = Regex.Replace(input, "\\%RESULT\\%", m_parsedresultlevel ?? "", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
}
else
{
if (input.IndexOf("%RESULT%", StringComparison.OrdinalIgnoreCase) >= 0)
input = Regex.Replace(input, "\\%RESULT\\%", m_resultFormatSerializer.Serialize(result, LogLines, null), RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
}
foreach (KeyValuePair kv in m_options)
input = Regex.Replace(input, "\\%" + kv.Key + "\\%", kv.Value ?? "", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
if (!m_options.ContainsKey("backup-name"))
input = Regex.Replace(input, "\\%backup-name\\%", System.IO.Path.GetFileNameWithoutExtension(Duplicati.Library.Utility.Utility.getEntryAssembly().Location) ?? "", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
input = Regex.Replace(input, "\\%[^\\%]+\\%", "");
return input;
}
}
///
/// Gets the filtered set of log lines
///
protected IEnumerable LogLines
{
get
{
var logdata = m_logstorage.AsEnumerable();
if (m_maxmimumLogLines > 0)
{
logdata = logdata.Take(m_maxmimumLogLines);
if (m_logstorage.Count > m_maxmimumLogLines)
logdata = logdata.Concat(new string[] { $"... and {m_logstorage.Count - m_maxmimumLogLines} more" });
}
return logdata;
}
}
public void OnFinish(object result)
{
// Dispose the current log scope
if (m_logscope != null)
{
try { m_logscope.Dispose(); }
catch { }
m_logscope = null;
}
if (!m_isConfigured)
return;
//If we do not report this action, then skip
if (!m_sendAll && !string.Equals(m_operationname, "Backup", StringComparison.OrdinalIgnoreCase))
return;
ParsedResultType level;
if (result is Exception)
level = ParsedResultType.Fatal;
else if (result != null && result is IBasicResults results)
level = results.ParsedResult;
else
level = ParsedResultType.Error;
m_parsedresultlevel = level.ToString();
if (string.Equals(m_operationname, "Backup", StringComparison.OrdinalIgnoreCase))
{
if (!m_levels.Any(x => string.Equals(x, "all", StringComparison.OrdinalIgnoreCase)))
{
//Check if this level should send mail
if (!m_levels.Any(x => string.Equals(x, level.ToString(), StringComparison.OrdinalIgnoreCase)))
return;
}
}
try
{
string body = m_body;
string subject = m_subject;
if (body != DEFAULT_BODY && System.IO.Path.IsPathRooted(body) && System.IO.File.Exists(body))
body = System.IO.File.ReadAllText(body);
body = ReplaceTemplate(body, result, false);
subject = ReplaceTemplate(subject, result, true);
SendMessage(subject, body);
}
catch (Exception ex)
{
Exception top = ex;
var sb = new StringBuilder();
while (top != null)
{
if (sb.Length != 0)
sb.Append("--> ");
sb.AppendFormat("{0}: {1}{2}", top.GetType().FullName, top.Message, Environment.NewLine);
top = top.InnerException;
}
Logging.Log.WriteWarningMessage(LOGTAG, "ReportSubmitError", ex, Strings.ReportHelper.SendMessageFailedError(sb.ToString()));
}
}
}
}