Add multi-host support

This refactoring includes:
- added multi-host mode support;
- added support for graceful shutdown of the service;
- added support to flush the log buffer as it fills;
- D-Bus service xyz.openbmc_project.HostLogger replaced with SIGUSR1
  signal handler;
- self diagnostic messages now registered via phosphor-logging;
- added unit tests;
- build system migrated from autotools to meson;
- source code aligned with OpenBMC conventions.

Change-Id: If6c1dfde278af685d8563450543a6587a282c7e4
Signed-off-by: Artem Senichev <a.senichev@yadro.com>
diff --git a/src/file_storage.cpp b/src/file_storage.cpp
new file mode 100644
index 0000000..ace7a1b
--- /dev/null
+++ b/src/file_storage.cpp
@@ -0,0 +1,150 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (C) 2020 YADRO
+
+#include "file_storage.hpp"
+
+#include "zlib_file.hpp"
+
+#include <set>
+
+namespace fs = std::filesystem;
+
+/** @brief File extension for log files. */
+static const std::string fileExt = ".log.gz";
+
+FileStorage::FileStorage(const std::string& path, const std::string& prefix,
+                         size_t maxFiles) :
+    outDir(path),
+    filePrefix(prefix), filesLimit(maxFiles)
+{
+    // Check path
+    if (!outDir.is_absolute())
+    {
+        throw std::invalid_argument("Path must be absolute");
+    }
+    fs::create_directories(outDir);
+
+    // Normalize file name prefix
+    if (filePrefix.empty())
+    {
+        filePrefix = "host";
+    }
+}
+
+std::string FileStorage::save(const LogBuffer& buf) const
+{
+    if (buf.empty())
+    {
+        return std::string(); // Buffer is empty, nothing to save
+    }
+
+    const std::string fileName = newFile();
+    ZlibFile logFile(fileName);
+
+    // Write full datetime stamp as the first record
+    tm tmLocal;
+    localtime_r(&buf.begin()->timeStamp, &tmLocal);
+    char tmText[20]; // asciiz for YYYY-MM-DD HH:MM:SS
+    strftime(tmText, sizeof(tmText), "%F %T", &tmLocal);
+    std::string titleMsg = ">>> Log collection started at ";
+    titleMsg += tmText;
+    logFile.write(tmLocal, titleMsg);
+
+    // Write messages
+    for (const auto& msg : buf)
+    {
+        localtime_r(&msg.timeStamp, &tmLocal);
+        logFile.write(tmLocal, msg.text);
+    }
+
+    logFile.close();
+
+    rotate();
+
+    return fileName;
+}
+
+std::string FileStorage::newFile() const
+{
+    // Prepare directory
+    fs::create_directories(outDir);
+
+    // Construct log file name: {prefix}_{timestamp}[_N].{ext}
+    std::string fileName = outDir / (filePrefix + '_');
+
+    time_t tmCurrent;
+    time(&tmCurrent);
+    tm tmLocal;
+    localtime_r(&tmCurrent, &tmLocal);
+    char tmText[16]; // asciiz for YYYYMMDD_HHMMSS
+    strftime(tmText, sizeof(tmText), "%Y%m%d_%H%M%S", &tmLocal);
+    fileName += tmText;
+
+    // Handle duplicate files
+    std::string dupPostfix;
+    size_t dupCounter = 0;
+    while (fs::exists(fileName + dupPostfix + fileExt))
+    {
+        dupPostfix = '_' + std::to_string(++dupCounter);
+    }
+    fileName += dupPostfix;
+    fileName += fileExt;
+
+    return fileName;
+}
+
+void FileStorage::rotate() const
+{
+    if (!filesLimit)
+    {
+        return; // Unlimited
+    }
+
+    // Get file list to ordered set
+    std::set<std::string> logFiles;
+    for (const auto& file : fs::directory_iterator(outDir))
+    {
+        if (!fs::is_regular_file(file))
+        {
+            continue;
+        }
+        const std::string fileName = file.path().filename();
+
+        const size_t minFileNameLen = filePrefix.length() +
+                                      15 + // time stamp YYYYMMDD_HHMMSS
+                                      fileExt.length();
+        if (fileName.length() < minFileNameLen)
+        {
+            continue;
+        }
+
+        if (fileName.compare(fileName.length() - fileExt.length(),
+                             fileExt.length(), fileExt))
+        {
+            continue;
+        }
+
+        const std::string fullPrefix = filePrefix + '_';
+        if (fileName.compare(0, fullPrefix.length(), fullPrefix))
+        {
+            continue;
+        }
+
+        logFiles.insert(fileName);
+    }
+
+    // Log file has a name with a timestamp generated. The sorted set contains
+    // the oldest file on the top, remove them.
+    if (logFiles.size() > filesLimit)
+    {
+        size_t removeCount = logFiles.size() - filesLimit;
+        for (const auto& fileName : logFiles)
+        {
+            fs::remove(outDir / fileName);
+            if (!--removeCount)
+            {
+                break;
+            }
+        }
+    }
+}