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