Artem Senichev | e8837d5 | 2020-06-07 11:59:04 +0300 | [diff] [blame] | 1 | // SPDX-License-Identifier: Apache-2.0 |
| 2 | // Copyright (C) 2020 YADRO |
| 3 | |
| 4 | #include "file_storage.hpp" |
| 5 | |
| 6 | #include "zlib_file.hpp" |
| 7 | |
| 8 | #include <set> |
| 9 | |
| 10 | namespace fs = std::filesystem; |
| 11 | |
| 12 | /** @brief File extension for log files. */ |
| 13 | static const std::string fileExt = ".log.gz"; |
| 14 | |
| 15 | FileStorage::FileStorage(const std::string& path, const std::string& prefix, |
| 16 | size_t maxFiles) : |
| 17 | outDir(path), |
| 18 | filePrefix(prefix), filesLimit(maxFiles) |
| 19 | { |
| 20 | // Check path |
| 21 | if (!outDir.is_absolute()) |
| 22 | { |
| 23 | throw std::invalid_argument("Path must be absolute"); |
| 24 | } |
| 25 | fs::create_directories(outDir); |
| 26 | |
| 27 | // Normalize file name prefix |
| 28 | if (filePrefix.empty()) |
| 29 | { |
| 30 | filePrefix = "host"; |
| 31 | } |
| 32 | } |
| 33 | |
| 34 | std::string FileStorage::save(const LogBuffer& buf) const |
| 35 | { |
| 36 | if (buf.empty()) |
| 37 | { |
| 38 | return std::string(); // Buffer is empty, nothing to save |
| 39 | } |
| 40 | |
| 41 | const std::string fileName = newFile(); |
| 42 | ZlibFile logFile(fileName); |
| 43 | |
| 44 | // Write full datetime stamp as the first record |
| 45 | tm tmLocal; |
| 46 | localtime_r(&buf.begin()->timeStamp, &tmLocal); |
| 47 | char tmText[20]; // asciiz for YYYY-MM-DD HH:MM:SS |
| 48 | strftime(tmText, sizeof(tmText), "%F %T", &tmLocal); |
| 49 | std::string titleMsg = ">>> Log collection started at "; |
| 50 | titleMsg += tmText; |
| 51 | logFile.write(tmLocal, titleMsg); |
| 52 | |
| 53 | // Write messages |
| 54 | for (const auto& msg : buf) |
| 55 | { |
| 56 | localtime_r(&msg.timeStamp, &tmLocal); |
| 57 | logFile.write(tmLocal, msg.text); |
| 58 | } |
| 59 | |
| 60 | logFile.close(); |
| 61 | |
| 62 | rotate(); |
| 63 | |
| 64 | return fileName; |
| 65 | } |
| 66 | |
| 67 | std::string FileStorage::newFile() const |
| 68 | { |
| 69 | // Prepare directory |
| 70 | fs::create_directories(outDir); |
| 71 | |
| 72 | // Construct log file name: {prefix}_{timestamp}[_N].{ext} |
| 73 | std::string fileName = outDir / (filePrefix + '_'); |
| 74 | |
| 75 | time_t tmCurrent; |
| 76 | time(&tmCurrent); |
| 77 | tm tmLocal; |
| 78 | localtime_r(&tmCurrent, &tmLocal); |
| 79 | char tmText[16]; // asciiz for YYYYMMDD_HHMMSS |
| 80 | strftime(tmText, sizeof(tmText), "%Y%m%d_%H%M%S", &tmLocal); |
| 81 | fileName += tmText; |
| 82 | |
| 83 | // Handle duplicate files |
| 84 | std::string dupPostfix; |
| 85 | size_t dupCounter = 0; |
| 86 | while (fs::exists(fileName + dupPostfix + fileExt)) |
| 87 | { |
| 88 | dupPostfix = '_' + std::to_string(++dupCounter); |
| 89 | } |
| 90 | fileName += dupPostfix; |
| 91 | fileName += fileExt; |
| 92 | |
| 93 | return fileName; |
| 94 | } |
| 95 | |
| 96 | void FileStorage::rotate() const |
| 97 | { |
| 98 | if (!filesLimit) |
| 99 | { |
| 100 | return; // Unlimited |
| 101 | } |
| 102 | |
| 103 | // Get file list to ordered set |
| 104 | std::set<std::string> logFiles; |
| 105 | for (const auto& file : fs::directory_iterator(outDir)) |
| 106 | { |
| 107 | if (!fs::is_regular_file(file)) |
| 108 | { |
| 109 | continue; |
| 110 | } |
| 111 | const std::string fileName = file.path().filename(); |
| 112 | |
| 113 | const size_t minFileNameLen = filePrefix.length() + |
| 114 | 15 + // time stamp YYYYMMDD_HHMMSS |
| 115 | fileExt.length(); |
| 116 | if (fileName.length() < minFileNameLen) |
| 117 | { |
| 118 | continue; |
| 119 | } |
| 120 | |
| 121 | if (fileName.compare(fileName.length() - fileExt.length(), |
| 122 | fileExt.length(), fileExt)) |
| 123 | { |
| 124 | continue; |
| 125 | } |
| 126 | |
| 127 | const std::string fullPrefix = filePrefix + '_'; |
| 128 | if (fileName.compare(0, fullPrefix.length(), fullPrefix)) |
| 129 | { |
| 130 | continue; |
| 131 | } |
| 132 | |
| 133 | logFiles.insert(fileName); |
| 134 | } |
| 135 | |
| 136 | // Log file has a name with a timestamp generated. The sorted set contains |
| 137 | // the oldest file on the top, remove them. |
| 138 | if (logFiles.size() > filesLimit) |
| 139 | { |
| 140 | size_t removeCount = logFiles.size() - filesLimit; |
| 141 | for (const auto& fileName : logFiles) |
| 142 | { |
| 143 | fs::remove(outDir / fileName); |
| 144 | if (!--removeCount) |
| 145 | { |
| 146 | break; |
| 147 | } |
| 148 | } |
| 149 | } |
| 150 | } |