Unravel Engine C++ Reference
Loading...
Searching...
No Matches
syncer.cpp
Go to the documentation of this file.
1#include "syncer.h"
2#include "watcher.h"
3
4#include <algorithm>
5#include <mutex>
6#include <unordered_map>
7#include <unordered_set>
8#include <vector>
9
10#define POOLSTL_STD_SUPPLEMENT 1
11#include <poolstl/poolstl.hpp>
12
13#include <logging/logging.h>
14namespace fs
15{
16
17static auto extract_entry_extension(const fs::path& path) -> std::string
18{
19 auto entry_path = path;
20 std::string entry_extension;
21 while(entry_path.has_extension())
22 {
23 entry_extension = entry_path.extension().string() + entry_extension;
24 entry_path.replace_extension();
25 }
26 return entry_extension;
27}
28
29static void ensure_directory_exists(const fs::path& path)
30{
31 fs::error_code err;
32 if(path.has_extension())
33 {
34 fs::create_directories(fs::path(path).parent_path(), err);
35 }
36 else
37 {
38 fs::create_directories(path, err);
39 }
40}
41
43static auto entry_serialization_key(const fs::path& watch_root, const fs::watcher::entry& entry) -> std::string
44{
45 const auto normalize_key = [](const fs::path& path) -> std::string
46 {
47 return path.lexically_normal().generic_string();
48 };
49
51 {
52 const fs::path parent_old = entry.last_path.parent_path();
53 const fs::path parent_new = entry.path.parent_path();
54 if(parent_old == parent_new)
55 {
56 const fs::path key = parent_old.empty() ? watch_root : parent_old;
57 return normalize_key(key);
58 }
59 // Cross-directory rename: keep ordering relative to the rest of the tree.
60 return normalize_key(watch_root);
61 }
62
63 if(entry.type == fs::file_type::directory)
64 {
65 return normalize_key(entry.path);
66 }
67
68 const fs::path parent = entry.path.parent_path();
69 const fs::path key = parent.empty() ? watch_root : parent;
70 return normalize_key(key);
71}
72
74{
75 unsync();
76}
77
78void syncer::set_mapping(const std::string& ref_ext,
79 const std::vector<std::string>& synced_ext,
80 on_entry_created_t on_entry_created = nullptr,
81 on_entry_modified_t on_entry_modified = nullptr,
82 on_entry_removed_t on_entry_removed = nullptr,
83 on_entry_renamed_t on_entry_renamed = nullptr)
84{
85 std::lock_guard<std::mutex> lock(mutex_);
86 auto& mapping = mapping_[ref_ext];
87 mapping.extensions = synced_ext;
88 mapping.on_entry_created = std::move(on_entry_created);
89 mapping.on_entry_modified = std::move(on_entry_modified);
90 mapping.on_entry_removed = std::move(on_entry_removed);
91 mapping.on_entry_renamed = std::move(on_entry_renamed);
92}
93
95 syncer::on_entry_modified_t on_entry_modified,
96 syncer::on_entry_removed_t on_entry_removed,
97 syncer::on_entry_renamed_t on_entry_renamed)
98{
99 std::lock_guard<std::mutex> lock(mutex_);
100 auto& mapping = mapping_[""];
101 mapping.on_entry_created = std::move(on_entry_created);
102 mapping.on_entry_modified = std::move(on_entry_modified);
103 mapping.on_entry_removed = std::move(on_entry_removed);
104 mapping.on_entry_renamed = std::move(on_entry_renamed);
105}
106
108{
109 const auto id = watch_id_.exchange(0);
110 if(id != 0)
111 {
113 }
114}
115
116auto syncer::get_mapping(const std::string& ext) -> mapping
117{
118 std::lock_guard<std::mutex> lock(mutex_);
119 auto it = mapping_.find(ext);
120 if(it != mapping_.end())
121 {
122 return it->second;
123 }
124
125 return {};
126}
127
128auto syncer::get_on_created_callback(const std::string& ext) -> on_entry_created_t
129{
130 return get_mapping(ext).on_entry_created;
131}
132
133auto syncer::get_on_modified_callback(const std::string& ext) -> on_entry_modified_t
134{
135 return get_mapping(ext).on_entry_modified;
136}
137
138auto syncer::get_on_removed_callback(const std::string& ext) -> on_entry_removed_t
139{
140 return get_mapping(ext).on_entry_removed;
141}
142
143auto syncer::get_on_renamed_callback(const std::string& ext) -> on_entry_renamed_t
144{
145 return get_mapping(ext).on_entry_renamed;
146}
147
148void syncer::sync(const fs::path& reference_dir, const fs::path& synced_dir, const on_sync_progress_t& on_progress)
149{
150 unsync();
151
152 {
153 std::lock_guard<std::mutex> lock(mutex_);
154 reference_dir_ = reference_dir;
155 synced_dir_ = synced_dir;
156 reference_dir_.make_preferred();
157 synced_dir_.make_preferred();
158 ensure_directory_exists(reference_dir_);
159 ensure_directory_exists(synced_dir_);
160 }
161
162 const auto on_change = [this, on_progress](const auto& entries, bool is_initial_listing)
163 {
164 const auto process_entry = [this, is_initial_listing](const watcher::entry& entry)
165 {
166 const bool is_directory = (entry.type == fs::file_type::directory);
167 const std::string entry_extension = extract_entry_extension(entry.path);
168
169 switch(entry.status)
170 {
172 {
173 const auto synced_entries = this->get_synced_entries(entry.path, is_directory);
174
175 for(const auto& synced_entry : synced_entries)
176 {
177 ensure_directory_exists(synced_entry);
178 }
179
180 auto callback = this->get_on_created_callback(entry_extension);
181 if(callback)
182 {
183 callback(entry_extension, entry.path, synced_entries, is_initial_listing);
184 }
185 }
186 break;
188 {
189 auto callback = this->get_on_modified_callback(entry_extension);
190 if(callback)
191 {
192 const auto synced_entries = this->get_synced_entries(entry.path, is_directory);
193 callback(entry_extension, entry.path, synced_entries, is_initial_listing);
194 }
195 }
196 break;
198 {
199 const auto callback = this->get_on_removed_callback(entry_extension);
200
201 if(callback)
202 {
203 const auto synced_entries = this->get_synced_entries(entry.path, is_directory);
204 callback(entry_extension, entry.path, synced_entries);
205 }
206 }
207 break;
209 {
210 const auto last_synced_entries = this->get_synced_entries(entry.last_path, is_directory);
211 const auto synced_entries = this->get_synced_entries(entry.path, is_directory);
212 auto callback = this->get_on_renamed_callback(entry_extension);
213
214 if(callback && synced_entries.size() == last_synced_entries.size())
215 {
216 std::vector<rename_pair_t> synced_renamed;
217 synced_renamed.reserve(synced_entries.size());
218
219 for(std::size_t i = 0; i < synced_entries.size(); ++i)
220 {
221 const auto& last_synced_entry = last_synced_entries[i];
222 const auto& synced_entry = synced_entries[i];
223 rename_pair_t p(last_synced_entry, synced_entry);
224 synced_renamed.emplace_back(std::move(p));
225 }
226 rename_pair_t p(entry.last_path, entry.path);
227 callback(entry_extension, p, synced_renamed);
228 }
229 }
230 break;
231 default:
232 break;
233 }
234 };
235
236 const fs::path watch_root = get_watch_path();
237
238 // Initial listing may include a folder and files under it, but every
239 // created entry calls ensure_directory_exists() before its callback, so
240 // parent/child create order does not matter. Live batches with
241 // renamed/removed still need per-folder ordering (e.g. delete files
242 // before their parent directory, create before same-folder rename).
243 const auto needs_per_folder_ordering = [&](auto begin, auto end) -> bool
244 {
245 if(is_initial_listing)
246 {
247 return false;
248 }
249 for(auto it = begin; it != end; ++it)
250 {
252 || it->status == fs::watcher::entry_status::removed)
253 {
254 return true;
255 }
256 }
257 return false;
258 };
259
260 const auto run_entry_batch = [&](auto begin, auto end)
261 {
262 if(!needs_per_folder_ordering(begin, end))
263 {
264 std::for_each(poolstl::par, begin, end, process_entry);
265 return;
266 }
267
268 std::unordered_map<std::string, std::vector<const fs::watcher::entry*>> groups;
269 groups.reserve(static_cast<std::size_t>(std::distance(begin, end)) / 4 + 1);
270
271 for(auto it = begin; it != end; ++it)
272 {
273 const std::string key = entry_serialization_key(watch_root, *it);
274 groups[key].push_back(&(*it));
275 }
276
277 std::vector<std::vector<const fs::watcher::entry*>> group_list;
278 group_list.reserve(groups.size());
279 for(auto& group : groups)
280 {
281 group_list.push_back(std::move(group.second));
282 }
283
284 const auto process_group = [&](const std::vector<const fs::watcher::entry*>& group_entries)
285 {
286 for(const fs::watcher::entry* entry : group_entries)
287 {
288 process_entry(*entry);
289 }
290 };
291
292 std::for_each(poolstl::par, group_list.begin(), group_list.end(), process_group);
293 };
294
295 if(entries.empty())
296 {
297 return;
298 }
299
300 // Progress must run on the watcher thread (not pool workers). Batch entries so
301 // on_progress fires a bounded number of times while work still runs in parallel.
302 if(!is_initial_listing || !on_progress)
303 {
304 run_entry_batch(entries.begin(), entries.end());
305 return;
306 }
307
308 constexpr size_t k_max_progress_updates = 16;
309 const size_t progress_stride =
310 std::max<size_t>(1, (entries.size() + k_max_progress_updates - 1) / k_max_progress_updates);
311
312 size_t completed = 0;
313 for(size_t offset = 0; offset < entries.size(); offset += progress_stride)
314 {
315 const auto chunk_begin = entries.begin() + static_cast<std::ptrdiff_t>(offset);
316 const auto chunk_end =
317 entries.begin() + static_cast<std::ptrdiff_t>(std::min(offset + progress_stride, entries.size()));
318
319 run_entry_batch(chunk_begin, chunk_end);
320
321 completed = std::min(offset + progress_stride, entries.size());
322 on_progress(completed, entries.size(), extract_entry_extension(std::prev(chunk_end)->path));
323 }
324 };
325
326
327 // const auto on_change = [this, on_progress](const auto& entries, bool is_initial_listing)
328 // {
329 // size_t completed = 0;
330 // for(const auto& entry : entries)
331 // {
332 // bool is_directory = (entry.type == fs::file_type::directory);
333 // auto entry_path = entry.path;
334 // std::string entry_extension = extract_entry_extension(entry_path);
335
336 // // APPLOG_TRACE("Syncer: process entry {}", fs::to_string(entry));
337
338 // if(is_initial_listing)
339 // {
340 // if(on_progress)
341 // {
342 // on_progress(completed, entries.size(), entry_extension);
343 // }
344 // }
345
346 // switch(entry.status)
347 // {
348 // case fs::watcher::entry_status::created:
349 // {
350 // const auto synced_entries = this->get_synced_entries(entry.path, is_directory);
351
352 // for(const auto& synced_entry : synced_entries)
353 // {
354 // ensure_directory_exists(synced_entry);
355 // }
356
357 // auto callback = this->get_on_created_callback(entry_extension);
358 // if(callback)
359 // {
360 // callback(entry_extension, entry.path, synced_entries, is_initial_listing);
361 // }
362 // }
363 // break;
364 // case fs::watcher::entry_status::modified:
365 // {
366 // auto callback = this->get_on_modified_callback(entry_extension);
367 // if(callback)
368 // {
369 // const auto synced_entries = this->get_synced_entries(entry.path, is_directory);
370 // callback(entry_extension, entry.path, synced_entries, is_initial_listing);
371 // }
372 // }
373 // break;
374 // case fs::watcher::entry_status::removed:
375 // {
376 // const auto callback = this->get_on_removed_callback(entry_extension);
377
378 // if(callback)
379 // {
380 // const auto synced_entries = this->get_synced_entries(entry.path, is_directory);
381 // callback(entry_extension, entry.path, synced_entries);
382 // }
383 // }
384 // break;
385 // case fs::watcher::entry_status::renamed:
386 // {
387 // const auto last_synced_entries = this->get_synced_entries(entry.last_path, is_directory);
388 // const auto synced_entries = this->get_synced_entries(entry.path, is_directory);
389 // auto callback = this->get_on_renamed_callback(entry_extension);
390
391 // if(callback && synced_entries.size() == last_synced_entries.size())
392 // {
393 // std::vector<rename_pair_t> synced_renamed;
394 // synced_renamed.reserve(synced_entries.size());
395
396 // for(std::size_t i = 0; i < synced_entries.size(); ++i)
397 // {
398 // const auto& last_synced_entry = last_synced_entries[i];
399 // const auto& synced_entry = synced_entries[i];
400 // rename_pair_t p(last_synced_entry, synced_entry);
401 // synced_renamed.emplace_back(std::move(p));
402 // }
403 // rename_pair_t p(entry.last_path, entry.path);
404 // callback(entry_extension, p, synced_renamed);
405 // }
406 // }
407
408 // break;
409 // default:
410 // break;
411 // }
412 // completed++;
413 // }
414 // };
415 using namespace std::literals;
416 const fs::path watch_dir = get_watch_path();
417 watch_id_ = fs::watcher::watch(watch_dir, pattern_filter("*"), true, true, 500ms, on_change);
418}
419
420auto syncer::get_synced_entries(const fs::path& path, bool is_directory) -> std::vector<fs::path>
421{
422 std::vector<fs::path> synced_entries;
423 auto synced_dir = get_synced_directory(path);
424
425 if(is_directory)
426 {
427 synced_entries.emplace_back(std::move(synced_dir));
428 }
429 else
430 {
431 auto entry_path = path;
432 std::string entry_extension;
433 while(entry_path.has_extension())
434 {
435 auto ext = entry_path.extension().string() + entry_extension;
436 entry_extension = ext;
437 entry_path.replace_extension();
438 }
439
440 {
441 std::lock_guard<std::mutex> lock(mutex_);
442 auto it = mapping_.find(entry_extension);
443 if(it != mapping_.end())
444 {
445 const auto& mapping = it->second;
446 const auto& extensions = mapping.extensions;
447
448 synced_entries.reserve(extensions.size());
449 for(const auto& cache_ext : extensions)
450 {
451 fs::path file = synced_dir / path.filename();
452 file.concat(cache_ext);
453
454 synced_entries.emplace_back(std::move(file));
455 }
456 }
457 }
458 }
459
460 return synced_entries;
461}
462
463auto syncer::get_watch_path() -> fs::path
464{
465 std::lock_guard<std::mutex> lock(mutex_);
466 const fs::path watch_dir = reference_dir_;
467 return watch_dir;
468}
469
470auto syncer::get_synced_directory(const fs::path& path) -> fs::path
471{
472 fs::path result;
473
474 {
475 std::lock_guard<std::mutex> lock(mutex_);
476 result = fs::replace(path, reference_dir_, synced_dir_);
477 }
478
479 fs::error_code err;
480 if(fs::is_directory(path, err) || !path.has_extension())
481 {
482 return result;
483 }
484
485 return result.parent_path();
486}
487} // namespace fs
A filter that combines include and exclude patterns for file/directory filtering.
std::pair< fs::path, fs::path > rename_pair_t
Definition syncer.h:16
std::function< void(const std::string &, const fs::path &, const std::vector< fs::path > &, bool)> on_entry_created_t
Definition syncer.h:19
std::function< void(const std::string &, const fs::path &, const std::vector< fs::path > &, bool)> on_entry_modified_t
Definition syncer.h:20
void unsync()
Stops syncing.
Definition syncer.cpp:107
void sync(const fs::path &reference_dir, const fs::path &synced_dir, const on_sync_progress_t &on_progress=nullptr)
Start syncing the synced_dir with reference to the reference_dir i.e changes that occur in the refere...
Definition syncer.cpp:148
void set_mapping(const std::string &ref_ext, const std::vector< std::string > &synced_ext, on_entry_created_t on_entry_created, on_entry_modified_t on_entry_modified, on_entry_removed_t on_entry_removed, on_entry_renamed_t on_entry_renamed)
Remaps a specific extension of the reference directory to extensions of the synced directory....
Definition syncer.cpp:78
std::function< void(const std::string &, const rename_pair_t &, const std::vector< rename_pair_t > &)> on_entry_renamed_t
Definition syncer.h:22
void set_directory_mapping(on_entry_created_t on_entry_created, on_entry_modified_t on_entry_modified, on_entry_removed_t on_entry_removed, on_entry_renamed_t on_entry_renamed)
Definition syncer.cpp:94
std::function< void(const std::string &, const fs::path &, const std::vector< fs::path > &)> on_entry_removed_t
Definition syncer.h:21
std::function< void(size_t completed, size_t total, const std::string &current_job)> on_sync_progress_t
Definition syncer.h:18
static void unwatch(std::uint64_t key)
Un-watches a previously registered file or directory.
Definition watcher.cpp:45
static auto watch(const fs::path &path, const pattern_filter &filter, bool recursive, bool initial_list, clock_t::duration poll_interval, notify_callback callback, const std::string &watcher_name="") -> std::uint64_t
Watches a file or directory for modification and call back the specified std::function....
Definition watcher.cpp:27
std::vector< render_pass_entry > entries
Definition cache.hpp:11
static void ensure_directory_exists(const fs::path &path)
Definition syncer.cpp:29
static auto extract_entry_extension(const fs::path &path) -> std::string
Definition syncer.cpp:17
path replace(const path &_path, const path &_sequence, const path &_new_sequence)
Replacing any occurences of the specified path sequence with another.
static auto entry_serialization_key(const fs::path &watch_root, const fs::watcher::entry &entry) -> std::string
Paths that share a key are processed on one worker in order (create -> rename, etc....
Definition syncer.cpp:43
std::vector< std::string > extensions
Definition syncer.h:26
on_entry_removed_t on_entry_removed
Definition syncer.h:29
on_entry_modified_t on_entry_modified
Definition syncer.h:28
on_entry_renamed_t on_entry_renamed
Definition syncer.h:30
on_entry_created_t on_entry_created
Definition syncer.h:27