1 /*
2 * Copyright (c) 2024 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16 #include "io/dev/FileMonitor.h"
17
18 #include <algorithm>
19 #include <iostream>
20 #include <optional>
21
22 #ifdef _WIN32
23 #if __has_include(<filesystem>)
24 #include <filesystem>
25 #define HAS_FILESYSTEM 1
26 #endif
27 #define USE_WIN32
28 #include <Windows.h>
29 #else
30 #include <dirent.h>
31 #endif
32
33 #include <climits>
34 #include <sys/stat.h>
35
36 namespace ige {
37 namespace {
FormatPath(std::string & path,bool isDirectory)38 void FormatPath(std::string& path, bool isDirectory)
39 {
40 size_t length = path.length();
41
42 // Make directory separators consistent.
43 std::replace(path.begin(), path.end(), '\\', '/');
44
45 // Ensure there is last separator in place.
46 if (path.length() > 0 && isDirectory) {
47 if (path[length - 1] != '/') {
48 path += '/';
49 }
50 }
51 }
52
ResolveAbsolutePath(std::string_view path,bool isDirectory)53 std::string ResolveAbsolutePath(std::string_view path, bool isDirectory)
54 {
55 std::string absolutePath;
56
57 #ifdef HAS_FILESYSTEM
58 std::error_code error;
59 if (auto resolvedPath = std::filesystem::canonical(std::filesystem::u8path(path), error); !error) {
60 absolutePath = resolvedPath.u8string();
61 }
62 #elif defined(USE_WIN32)
63 char resolvedPath[_MAX_PATH] = {};
64 auto ret = GetFullPathNameA(path.data(), sizeof(resolvedPath), resolvedPath, nullptr);
65 if (ret < sizeof(resolvedPath)) {
66 WIN32_FIND_DATAA data;
67 HANDLE handle = FindFirstFileA(resolvedPath, &data);
68 if (handle != INVALID_HANDLE_VALUE) {
69 absolutePath = resolvedPath;
70 FindClose(handle);
71 }
72 }
73 #elif defined(__PLATFORM_MW_32__) || defined(__PLATFORM_MW_64__)
74 char resolvedPath[_MAX_PATH] = {};
75 if (_fullpath(resolvedPath, path.data(), _MAX_PATH) != nullptr) {
76 if (isDirectory) {
77 auto handle = opendir(resolvedPath);
78 if (handle) {
79 closedir(handle);
80 absolutePath = resolvedPath;
81 }
82 } else {
83 auto handle = fopen(resolvedPath, "r");
84 if (handle) {
85 fclose(handle);
86 absolutePath = resolvedPath;
87 }
88 }
89 }
90 #else
91 char resolvedPath[PATH_MAX];
92 if (realpath(path.data(), resolvedPath) != nullptr) {
93 absolutePath = resolvedPath;
94 }
95 #endif
96
97 FormatPath(absolutePath, isDirectory);
98
99 return absolutePath;
100 }
101
102 struct Collection {
103 std::vector<std::string> files;
104 std::vector<std::string> directories;
105 };
106
GatherFilesAndDirectories(std::string_view path)107 std::optional<Collection> GatherFilesAndDirectories(std::string_view path)
108 {
109 Collection collection;
110 #ifdef HAS_FILESYSTEM
111 std::error_code error;
112 for (auto const& entry : std::filesystem::directory_iterator { std::filesystem::u8path(path), error }) {
113 if (entry.is_directory()) {
114 collection.directories.push_back(entry.path().filename().u8string());
115 } else if (entry.is_regular_file()) {
116 collection.files.push_back(entry.path().filename().u8string());
117 }
118 }
119 if (error) {
120 return std::nullopt;
121 }
122 #elif defined(USE_WIN32)
123 WIN32_FIND_DATAA data;
124 HANDLE handle = INVALID_HANDLE_VALUE;
125 handle = FindFirstFileA((std::string(path) + "*.*").c_str(), &data);
126 if (handle == INVALID_HANDLE_VALUE) {
127 // Unable to open directory.
128 return std::nullopt;
129 }
130 do {
131 if (data.dwFileAttributes & FILE_ATTRIBUTE_DEVICE) {
132 // skip devices.
133 } else if (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
134 collection.directories.push_back(data.cFileName);
135 } else {
136 collection.files.push_back(data.cFileName);
137 }
138 } while (FindNextFileA(handle, &data));
139
140 FindClose(handle);
141 #else
142 DIR* handle = opendir(path.data());
143 if (!handle) {
144 // Unable to open directory.
145 return std::nullopt;
146 }
147
148 struct dirent* directory;
149 while ((directory = readdir(handle)) != nullptr) {
150 switch (directory->d_type) {
151 case DT_DIR:
152 collection.directories.push_back(directory->d_name);
153 break;
154
155 case DT_REG:
156 collection.files.push_back(directory->d_name);
157 break;
158
159 default:
160 break;
161 }
162 }
163
164 closedir(handle);
165 #endif
166 return collection;
167 }
168
RecursivelyCollectAllFiles(std::string_view path,std::vector<std::string> & aFiles)169 void RecursivelyCollectAllFiles(std::string_view path, std::vector<std::string>& aFiles)
170 {
171 auto filesAndDirs = GatherFilesAndDirectories(path);
172 if (!filesAndDirs) {
173 return;
174 }
175 // Collect files.
176 std::string originalPath;
177 for (const auto& filename : filesAndDirs->files) {
178 originalPath = path;
179 originalPath += filename;
180 std::string absoluteFilename = ResolveAbsolutePath(originalPath, false);
181 if (!absoluteFilename.empty()) {
182 aFiles.push_back(absoluteFilename);
183 }
184 }
185
186 // Process recursively.
187 for (const auto& directoryName : filesAndDirs->directories) {
188 if (directoryName != "." && directoryName != "..") {
189 originalPath = path;
190 originalPath += directoryName;
191 std::string absoluteDirectory = ResolveAbsolutePath(originalPath, true);
192 if (!absoluteDirectory.empty()) {
193 RecursivelyCollectAllFiles(absoluteDirectory, aFiles);
194 }
195 }
196 }
197 }
198 } // namespace
199
AddPath(std::string_view path)200 bool FileMonitor::AddPath(std::string_view path)
201 {
202 std::string absolutePath = ResolveAbsolutePath(path, true);
203 if (absolutePath.empty() || IsWatchingDirectory(absolutePath) || IsWatchingSubDirectory(absolutePath)) {
204 // Already exists or unable to resolve.
205 return false;
206 }
207
208 std::vector<std::string> files;
209 RecursivelyCollectAllFiles(absolutePath, files);
210
211 // Add all files to watch list.
212 for (const auto& ref : files) {
213 AddFile(ref);
214 }
215
216 // Store directory to watch list.
217 mDirectories.push_back(std::move(absolutePath));
218
219 return true;
220 }
221
RemovePath(std::string_view path)222 bool FileMonitor::RemovePath(std::string_view path)
223 {
224 std::string absolutePath = ResolveAbsolutePath(path, true);
225
226 std::vector<std::string>::iterator iterator = std::find(mDirectories.begin(), mDirectories.end(), absolutePath);
227 if (iterator != mDirectories.end()) {
228 // Collect all tracked files within this directory.
229 std::vector<std::string> files;
230 RecursivelyCollectAllFiles(absolutePath, files);
231
232 // Stop tracking of removed files.
233 for (const auto& ref : files) {
234 // Remove from tracked list.
235 RemoveFile(ref);
236 }
237
238 // Remove directory from watch list.
239 mDirectories.erase(iterator);
240 return true;
241 }
242
243 return false;
244 }
245
AddFile(std::string_view path)246 bool FileMonitor::AddFile(std::string_view path)
247 {
248 std::string absolutePath = ResolveAbsolutePath(path, false);
249 if (absolutePath.empty() || mFiles.find(absolutePath) != mFiles.end()) {
250 // Already exists or unable to resolve.
251 return false;
252 }
253 #if HAS_FILESYSTEM
254 std::error_code error;
255 const auto time = std::filesystem::last_write_time(std::filesystem::u8path(absolutePath), error);
256 if (error) {
257 return false;
258 }
259 // Collect file data.
260 mFiles[absolutePath].timestamp = time.time_since_epoch().count();
261 #else
262 struct stat ds;
263 if (stat(absolutePath.c_str(), &ds) != 0) {
264 // Unable to get file stats.
265 return false;
266 }
267
268 // Collect file data.
269 FileInfo info;
270 info.timestamp = ds.st_mtime;
271 mFiles[absolutePath] = info;
272 #endif
273 return true;
274 }
275
RemoveFile(const std::string & path)276 bool FileMonitor::RemoveFile(const std::string& path)
277 {
278 auto iterator = mFiles.find(path);
279 if (iterator != mFiles.end()) {
280 mFiles.erase(iterator);
281 return true;
282 }
283
284 return false;
285 }
286
IsWatchingDirectory(std::string_view path)287 bool FileMonitor::IsWatchingDirectory(std::string_view path)
288 {
289 for (const auto& ref : mDirectories) {
290 if (path.find(ref) != std::string::npos) {
291 // Already watching this directory or it's parent.
292 return true;
293 }
294 }
295
296 return false;
297 }
298
IsWatchingSubDirectory(std::string_view path)299 bool FileMonitor::IsWatchingSubDirectory(std::string_view path)
300 {
301 for (const auto& ref : mDirectories) {
302 if (ref.find(path) != std::string::npos) {
303 // Already watching subdirectory of given directory.
304 return true;
305 }
306 }
307
308 return false;
309 }
310
ScanModifications(std::vector<std::string> & aAdded,std::vector<std::string> & aRemoved,std::vector<std::string> & aModified)311 void FileMonitor::ScanModifications(
312 std::vector<std::string>& aAdded, std::vector<std::string>& aRemoved, std::vector<std::string>& aModified)
313 {
314 // Collect all files that are under monitoring.
315 std::vector<std::string> files;
316 for (const auto& ref : mDirectories) {
317 RecursivelyCollectAllFiles(ref, files);
318 }
319
320 // See which of the files are modified.
321 for (const auto& file : files) {
322 auto iterator = mFiles.find(file);
323 if (iterator != mFiles.end()) {
324 #if HAS_FILESYSTEM
325 const auto u8path = std::filesystem::u8path(file);
326 std::error_code error;
327 const auto time = std::filesystem::last_write_time(u8path, error).time_since_epoch().count();
328 if ((!error) && (time != iterator->second.timestamp)) {
329 // This file is modified.
330 aModified.push_back(file);
331
332 // Store new time.
333 iterator->second.timestamp = time;
334 }
335 #else
336 // File being watched, see if it is modified.
337 struct stat fs;
338 if ((stat(file.c_str(), &fs) == 0) && (fs.st_mtime != iterator->second.timestamp)) {
339 // This file is modified.
340 aModified.push_back(file);
341
342 // Store new time.
343 iterator->second.timestamp = fs.st_mtime;
344 }
345 #endif
346 } else {
347 // This is a new file.
348 aAdded.push_back(file);
349 }
350 }
351
352 // See which of the files are removed.
353 for (const auto& ref : mFiles) {
354 if (std::find(files.begin(), files.end(), ref.first) == files.end()) {
355 // This file no longer exists.
356 aRemoved.push_back(ref.first);
357 }
358 }
359
360 // Stop tracking of removed files.
361 for (const auto& ref : aRemoved) {
362 // Remove from tracked list.
363 RemoveFile(ref);
364 }
365
366 // Start tracking of new files.
367 for (const auto& ref : aAdded) {
368 // Add to tracking list.
369 AddFile(ref);
370 }
371 }
372
GetMonitoredFiles() const373 std::vector<std::string> FileMonitor::GetMonitoredFiles() const
374 {
375 std::vector<std::string> files;
376 files.reserve(mFiles.size());
377
378 for (auto it = mFiles.begin(), end = mFiles.end(); it != end; ++it) {
379 files.push_back(it->first);
380 }
381 return files;
382 }
383 } // namespace ige
384