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