RetentionPolicy.cs 7.48 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167
using System;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Schema;
using NChronicle.File.Configuration;
using NChronicle.File.Delegates;
using NChronicle.File.Interfaces;

namespace NChronicle.File {

    /// <summary>
    /// The default <see cref="IRetentionPolicy"/> archiving iteratively on file size and age. 
    /// </summary>
    /// <remarks>
    /// <para>
    /// This retention policy is based on file size and file age. Depending 
    /// on the configuration, when the output file grows to the set size limit, 
    /// or the time since it was created is greater than the set age limit, it 
    /// will be archived and named with the time the output file was created. 
    /// </para>
    /// <para>
    /// If the file name for the archive is already taken it will be appended by
    /// a subsequently growing number. 
    /// </para>
    /// <para>
    /// It is also possible to set a retention limit, this limit defines how 
    /// many of the newest archived logs are kept. 
    /// </para>
    /// <para>
    /// The default configuration is a 100MB file size limit, 1 day file age limit, 
    /// and a retention limit of 20.</para>
    /// </remarks>
    /// <example>
    /// Starting an application at 12:35 on December 12th 2016 with
    /// a file size limit of 100MB, a file age limit of 30 minutes and 
    /// an output path of <c>"chronicle.log"</c> will result in the output 
    /// file being archived with the name of <c>"chronicle.2016.12.01.12.35.log"</c> 
    /// at 13:05 or when 100MB of records have been written to it; which ever comes first.
    /// </example>
    public class RetentionPolicy : IRetentionPolicy {

        private readonly RetentionPolicyConfiguration _configuration;

        /// <summary>
        /// Create a new <see cref="RetentionPolicy"/> instance with the default configuration.
        /// </summary>
        public RetentionPolicy () {
            this._configuration = new RetentionPolicyConfiguration();
        }

        /// <summary>
        /// Check if the output file at the given <paramref name="path"/> is still within the configured
        /// limits or is to be archived. It is ready if the file - with the pending bytes to be written - 
        /// is over the configured file size limit or the file is older than the configured file age limit. 
        /// </summary>
        /// <param name="path">The path to the output file.</param>
        /// <param name="pendingBytes">Any pending bytes that are to be written to the output file.</param>
        /// <returns>A <see cref="bool"/> indicating if the output file is to be archived.</returns>
        public bool CheckPolicy (string path, byte[] pendingBytes) {
            var fileInfo = new FileInfo(path);
            if (this._configuration.FileSizeLimit != 0 && fileInfo.Length + pendingBytes.Length > this._configuration.FileSizeLimit) {
                return true;
            }
            return this._configuration.AgeLimit.HasValue && fileInfo.CreationTimeUtc < DateTime.UtcNow - this._configuration.AgeLimit.Value;
        }

        /// <summary>
        /// Archive the output file at the given <paramref name="path"/>, naming with time the output file 
        /// was created. 
        /// </summary>
        /// <param name="path">The path to the output file.</param>
        public void InvokePolicy (string path) {
            System.IO.File.Move(path, this.GetFileName(path));
            this.PurgeFiles(path);
        }

        /// <summary>
        /// Configure this <see cref="RetentionPolicy"/> with the specified options.
        /// </summary>
        /// <param name="configurationDelegate">A function to set <see cref="RetentionPolicy"/> configuration.</param>
        public void Configure (RetentionPolicyConfigurationDelegate configurationDelegate) {
            configurationDelegate(this._configuration);
        }

        private string GetFileName (string path) {
            var now = new FileInfo(path).CreationTime;
            var extension = string.Empty;
            if (!string.IsNullOrEmpty(extension = Path.GetExtension(path))) {
                path = $"{Path.GetDirectoryName(path)}\\{Path.GetFileNameWithoutExtension(path)}";
            }
            var newPath = $"{path}.{now.Year}.{now.Month}.{now.Day}.{now.Hour}.{now.Minute}";
            if (!System.IO.File.Exists($"{newPath}{extension}")) {
                return $"{newPath}{extension}";
            }
            var i = 0;
            string fileName;
            do {
                i++;
                fileName = $"{newPath}.{i}{extension}";
            }
            while (System.IO.File.Exists(fileName));
            return fileName;
        }

        private void PurgeFiles (string path) {
            if (this._configuration.RetentionLimit <= 0) return;
            var fileName = this.EscapeRegex(Path.GetFileNameWithoutExtension(path));
            var fileExt = this.EscapeRegex(Path.GetExtension(path));
            var regex = new Regex
                ($"{fileName}\\.\\d\\d\\d\\d\\.\\d\\d?\\.\\d\\d?\\.\\d\\d?\\.\\d\\d?(\\.\\d+)?{fileExt}$");
            var files = Directory.EnumerateFiles(Path.GetDirectoryName(path)).Where(p => regex.IsMatch(p));
            var toDelete = files.Count() - this._configuration.RetentionLimit;
            if (toDelete > 0) {
                var filesOrdered = files.OrderBy(f => new FileInfo(f).CreationTimeUtc).ToArray();
                for (var i = 0; i < toDelete; i++) {
                    System.IO.File.Delete(filesOrdered[i]);
                }
            }
        }

        private string EscapeRegex (string str) {
            if (str == null) return null;
            if (str == string.Empty) return string.Empty;
            return
                str.Replace("\\", "\\\\")
                   .Replace(".", "\\.")
                   .Replace("$", "\\$")
                   .Replace("^", "\\^")
                   .Replace("[", "\\[")
                   .Replace("]", "\\]")
                   .Replace("(", "\\(")
                   .Replace(")", "\\)")
                   .Replace("{", "\\{")
                   .Replace("}", "\\}")
                   .Replace("*", "\\*")
                   .Replace("+", "\\+")
                   .Replace("?", "\\?")
                   .Replace("#", "\\#");
        }

        #region Xml Serialization
        /// <summary>
        /// Required for XML serialization, this method offers no functionality.
        /// </summary>
        /// <returns>A null <see cref="XmlSchema"/>.</returns>
        public XmlSchema GetSchema() => null;

        /// <summary>
        /// Populate configuration from XML via the specified <see cref="XmlReader" />.
        /// </summary>
        /// <param name="reader"><see cref="XmlReader" /> stream from the configuration file.</param>
        /// <seealso cref="Core.NChronicle.ConfigureFrom(string, bool, int)"/>
        public void ReadXml(XmlReader reader) => this._configuration.ReadXml(reader);

        /// <summary>
        /// Write configuration to XML via the specified <see cref="XmlWriter" />.
        /// </summary>
        /// <param name="writer"><see cref="XmlWriter" /> stream to the configuration file.</param>
        /// <seealso cref="Core.NChronicle.SaveConfigurationTo(string)"/>
        public void WriteXml(XmlWriter writer) => this._configuration.WriteXml(writer);
        #endregion

    }

}