DiskPersistence.cpp
Go to the documentation of this file.
2
3#include <fcntl.h>
4#include <unistd.h>
5
6#include <set>
7
9
11{
12
13 std::vector<std::string>
15 {
16 std::vector<std::string> containers;
17
18 if (!enabled_)
19 {
20 return containers;
21 }
22
23 // If it has a nice form
24 if (!id.hasEntityName() || id.hasTimestamp())
25 {
26 std::vector<std::filesystem::path> dirs = getAllDirectories(id);
27
28
29 for (auto& path : dirs)
30 {
31 std::string container = path.filename().string();
32
33 if (!container.empty())
34 {
35 containers.emplace_back(container);
36 }
37 }
38 }
39 // If it has not a nice form
40 else
41 {
42 std::vector<std::filesystem::path> dayDirs = getAllDirectories(id);
43
44 for (std::filesystem::path& dayDir : dayDirs)
45 {
46 if (!util::fs::detail::isDateString(dayDir.filename()))
47 {
48 ARMARX_WARNING << "Found a non-date folder inside an entity '" << id.str()
49 << "' with name '" << dayDir.filename() << "'. "
50 << "Ignoring this folder, however this is a bad situation.";
51 continue;
52 }
53
54 std::vector<std::filesystem::path> secondDirs = util::fs::getAllDirectories(dayDir);
55
56 for (std::filesystem::path& secondDir : secondDirs)
57 {
58 if (!util::fs::detail::isNumberString(secondDir.filename()))
59 {
60 ARMARX_WARNING << "Found a non-timestamp folder inside an entity '"
61 << id.str() << "' hours folder with name '"
62 << secondDir.filename() << "'. "
63 << "Ignoring this folder, however this is a bad situation.";
64 continue;
65 }
66
67 std::vector<std::filesystem::path> timestampDirs =
69
70 for (std::filesystem::path& timestampDir : timestampDirs)
71 {
72 if (!util::fs::detail::isNumberString(timestampDir.filename()))
73 {
75 << "Found a non-timestamp folder inside an entity '" << id.str()
76 << "' seconds folder with name '" << timestampDir.filename()
77 << "'. "
78 << "Ignoring this folder, however this is a bad situation.";
79 continue;
80 }
81
82 std::string container = timestampDir.filename().string();
83
84 if (!container.empty())
85 {
86 containers.emplace_back(container);
87 }
88 }
89 }
90 }
91 }
92
93 return containers;
94 }
95
96 std::vector<std::string>
98 {
99 if (!enabled_)
100 {
101 return std::vector<std::string>();
102 }
103
104 // THREAD-SAFETY: Acquire per-directory mutex to get consistent file listing
105 std::string directoryPath = getFullPath(id).string();
106
107 std::mutex* dirMutex = nullptr;
108 {
109 std::lock_guard mapLock(directoryMutexMapLock_);
110 auto& mutexPtr = directoryMutexes_[directoryPath];
111 if (!mutexPtr)
112 {
113 mutexPtr = std::make_unique<std::mutex>();
114 }
115 dirMutex = mutexPtr.get();
116 }
117
118 std::lock_guard dirLock(*dirMutex);
119
120 std::vector<std::filesystem::path> files = getAllFiles(id);
121 std::vector<std::string> filesStr;
122
123 for (auto& path : files)
124 {
125 std::string item = path.filename().string();
126
127 if (!item.empty())
128 {
129 filesStr.emplace_back(item);
130 }
131 }
132
133 return filesStr;
134 }
135
136 bool
138 {
139 if (!enabled_)
140 {
141 return false;
142 }
143
144 //from id get the directory for this memory item and append the key to it, then check if the directory exists
145 auto path_to_id = getFullPath(id);
146 auto correct_container_path = path_to_id / key;
147 bool contains_container = util::fs::directoryExists(correct_container_path);
148
149 return contains_container;
150 }
151
152 bool
154 {
155 if (!enabled_)
156 {
157 return false;
158 }
159
160 return fileExists(id, key);
161 }
162
163 void
165 std::string key,
166 std::vector<unsigned char>& data)
167 {
168 if (!enabled_)
169 {
170 return;
171 }
172
173 // If batch writing is enabled, add to batch buffer instead of writing immediately
174 if (batchWriteEnabled_.load(std::memory_order_acquire))
175 {
176 enqueueBatchItem(id, key, std::move(data));
177 return;
178 }
179
180 // Original immediate write path
181 // THREAD-SAFETY: Get per-directory mutex for fine-grained locking
182 // This allows parallel writes to different directories (different entities)
183 // while preventing filesystem corruption within the same directory
184 std::string directoryPath = getFullPath(id).string();
185
186 // Get or create mutex for this directory
187 std::mutex* dirMutex = nullptr;
188 {
189 std::lock_guard mapLock(directoryMutexMapLock_);
190 auto& mutexPtr = directoryMutexes_[directoryPath];
191 if (!mutexPtr)
192 {
193 mutexPtr = std::make_unique<std::mutex>();
194 }
195 dirMutex = mutexPtr.get();
196 }
197
198 // Lock this specific directory for the duration of the write
199 // Other threads can write to different directories in parallel
200 std::lock_guard dirLock(*dirMutex);
201
202 try
203 {
204 auto dir = getFullPath(id);
205 auto parentDir = dir.parent_path();
206
207 // Check permissions before attempting write
208 if (util::fs::directoryExists(parentDir) && !util::fs::hasWritePermission(parentDir))
209 {
210 ARMARX_ERROR << "No write permission for directory: " << parentDir
211 << ". Cannot store " << id.str() << "/" << key;
212 storageErrorCount_.fetch_add(1, std::memory_order_relaxed);
213 return;
214 }
215
216 ensureFullPathExists(id, true);
217
218 if (enoughDiskSpaceLeft())
219 {
220 writeDataToFile(id, key, data);
221 }
222 else
223 {
224 ARMARX_ERROR << "Not enough disk space available for DiskPersistence. "
225 << "Skipping storage of " << id.str() << "/" << key;
226 storageErrorCount_.fetch_add(1, std::memory_order_relaxed);
227 }
228 }
229 catch (const armarx::LocalException& e)
230 {
231 ARMARX_ERROR << "ArmarX exception while storing " << id.str() << "/" << key
232 << ": " << e.what();
233 storageErrorCount_.fetch_add(1, std::memory_order_relaxed);
234 }
235 catch (const std::filesystem::filesystem_error& e)
236 {
237 ARMARX_ERROR << "Filesystem error while storing " << id.str() << "/" << key
238 << ": " << e.what() << " (error code: " << e.code() << ")";
239 storageErrorCount_.fetch_add(1, std::memory_order_relaxed);
240 }
241 catch (const std::system_error& e)
242 {
243 ARMARX_ERROR << "System error while storing " << id.str() << "/" << key
244 << ": " << e.what() << " (error code: " << e.code() << ")";
245 storageErrorCount_.fetch_add(1, std::memory_order_relaxed);
246 }
247 catch (const std::exception& e)
248 {
249 ARMARX_ERROR << "Unexpected exception while storing " << id.str() << "/" << key
250 << ": " << e.what();
251 storageErrorCount_.fetch_add(1, std::memory_order_relaxed);
252 }
253 catch (...)
254 {
255 ARMARX_ERROR << "Unknown exception while storing " << id.str() << "/" << key;
256 storageErrorCount_.fetch_add(1, std::memory_order_relaxed);
257 }
258 }
259
260 std::vector<unsigned char>
262 {
263 if (!enabled_)
264 {
265 return std::vector<unsigned char>();
266 }
267
268 // THREAD-SAFETY: Acquire per-directory mutex to prevent reading during writes
269 // This ensures we don't read partial/empty files while another thread is writing
270 std::string directoryPath = getFullPath(id).string();
271
272 std::mutex* dirMutex = nullptr;
273 {
274 std::lock_guard mapLock(directoryMutexMapLock_);
275 auto& mutexPtr = directoryMutexes_[directoryPath];
276 if (!mutexPtr)
277 {
278 mutexPtr = std::make_unique<std::mutex>();
279 }
280 dirMutex = mutexPtr.get();
281 }
282
283 std::lock_guard dirLock(*dirMutex);
284
285 if (fileExists(id, key))
286 {
287 return readDataFromFile(id, key);
288 }
289
290 return std::vector<unsigned char>();
291 }
292
293 std::filesystem::path
294 DiskPersistence::getMemoryParentPath()
295 {
296 std::string p = memoryParentPath_.string();
297
299
300 return p;
301 }
302
303 std::filesystem::path
304 DiskPersistence::getFullPath(const armarx::armem::MemoryID& id)
305 {
306 auto p = getMemoryParentPath() / getExportName();
307
308 auto cleanID =
309 id.cleanID(); //somehow, the iDs are jumbled when loading the LTM from disk, this solves it for now
310
311 auto fullPath = util::fs::toPath(p, cleanID);
312
313 return fullPath;
314 }
315
316 bool
317 DiskPersistence::fullPathExists(const armarx::armem::MemoryID& id)
318 {
319 auto p = getFullPath(id);
321 }
322
323 bool
324 DiskPersistence::fileExists(const armarx::armem::MemoryID& id, const std::string& filename)
325 {
326 auto p = getFullPath(id) / filename;
327 return util::fs::fileExists(p);
328 }
329
330 void
331 DiskPersistence::ensureFullPathExists(const armarx::armem::MemoryID& id,
332 bool createIfNotExistent)
333 {
334 auto p = getFullPath(id);
335 util::fs::ensureDirectoryExists(p, createIfNotExistent);
336 }
337
338 void
339 DiskPersistence::ensureFileExists(const armarx::armem::MemoryID& id,
340 const std::string& filename,
341 bool createIfNotExistent)
342 {
343 auto p = getFullPath(id) / filename;
344 util::fs::ensureFileExists(p, createIfNotExistent);
345 }
346
347 void
348 DiskPersistence::writeDataToFile(const armarx::armem::MemoryID& id,
349 const std::string& filename,
350 const std::vector<unsigned char>& data)
351 {
352 auto p = getFullPath(id) / filename;
354 }
355
356 std::vector<unsigned char>
357 DiskPersistence::readDataFromFile(const armarx::armem::MemoryID& id,
358 const std::string& filename)
359 {
360 auto p = getFullPath(id) / filename;
362 }
363
364 std::vector<std::filesystem::path>
365 DiskPersistence::getAllFiles(const armarx::armem::MemoryID& id)
366 {
367 if (fullPathExists(id))
368 {
369 auto p = getFullPath(id);
370 return util::fs::getAllFiles(p);
371 }
372
373 return std::vector<std::filesystem::path>();
374 }
375
376 std::vector<std::filesystem::path>
377 DiskPersistence::getAllDirectories(const armarx::armem::MemoryID& id)
378 {
379 if (fullPathExists(id))
380 {
381 auto p = getFullPath(id);
383 }
384
385 return std::vector<std::filesystem::path>();
386 }
387
388 bool
389 DiskPersistence::enoughDiskSpaceLeft()
390 {
391 const std::filesystem::path configured_path = this->getMemoryParentPath();
392 bool debug_info_output_enabled = false;
393
394 // The memory directory need not exist yet on the first write, so walk up to the
395 // nearest existing ancestor: it lives on the same disk and gives a usable reading.
396 std::error_code ec;
397 std::filesystem::path path_to_disk = configured_path;
398 while (!path_to_disk.empty() && !std::filesystem::exists(path_to_disk, ec))
399 {
400 const std::filesystem::path parent = path_to_disk.parent_path();
401 if (parent == path_to_disk)
402 {
403 break;
404 }
405 path_to_disk = parent;
406 }
407
408 if (!path_to_disk.empty() && std::filesystem::exists(path_to_disk, ec))
409 {
410 try
411 {
412 auto space_info = std::filesystem::space(path_to_disk);
413 int const conversion_factor = 1024;
414
415 auto available_space = space_info.available /
416 (conversion_factor * conversion_factor * conversion_factor);
417
418 if (debug_info_output_enabled)
419 {
420 ARMARX_DEBUG << "Capacity: "
421 << space_info.capacity /
422 (conversion_factor * conversion_factor * conversion_factor)
423 << " GB\n";
424 ARMARX_DEBUG << "Free space: "
425 << space_info.free /
426 (conversion_factor * conversion_factor * conversion_factor)
427 << " GB\n";
428 ARMARX_DEBUG << "Available space: "
429 << space_info.available /
430 (conversion_factor * conversion_factor * conversion_factor)
431 << " GB\n";
432
433 ARMARX_DEBUG << "Min disk space: " << this->minDiskSpace << " GB\n";
434 }
435 // minDiskSpace is a signed int; compare only after ruling out negative
436 // values, which would otherwise convert to a huge unsigned bound.
437 return this->minDiskSpace <= 0 ||
438 available_space >= static_cast<std::uintmax_t>(this->minDiskSpace);
439 }
440 catch (const std::filesystem::filesystem_error& e)
441 {
442 ARMARX_WARNING << "Error: " << e.what() << '\n';
443 return false;
444 }
445 catch (...)
446 {
447 ARMARX_DEBUG << "Error while trying to get info on available disk space";
448 return false;
449 }
450 }
451 else
452 {
453 // Nothing on the configured path resolves to an existing directory, so free
454 // space cannot be determined. Refuse the write rather than proceeding blindly,
455 // matching how the failure paths above behave.
456 ARMARX_WARNING << "Cannot resolve any existing directory for '" << configured_path
457 << "' and thus cannot check the available disk space. "
458 << "Refusing to write.";
459 return false;
460 }
461 }
462
463 void
465 {
466 auto basePath = getMemoryParentPath();
467
468 if (!util::fs::canCreateFiles(basePath))
469 {
470 ARMARX_ERROR << "LTM storage path '" << basePath
471 << "' is not writable! Data will not be persisted.";
472 ARMARX_ERROR << "Please check:\n"
473 << " 1. Directory exists\n"
474 << " 2. Current user has write permissions\n"
475 << " 3. Filesystem is not read-only\n"
476 << " 4. No SELinux/AppArmor restrictions";
477 }
478 else
479 {
480 ARMARX_INFO << "LTM storage path validated: " << basePath;
481 }
482 }
483
484 // ==================== Batch Write Implementation ====================
485
486 void
488 {
489 bool wasEnabled = batchWriteEnabled_.exchange(enable, std::memory_order_acq_rel);
490
491 if (enable && !wasEnabled)
492 {
493 // Starting batch mode - start the background flush thread
494 startBatchWriter();
495 ARMARX_INFO << "Batch write mode ENABLED (threshold: " << batchSizeThreshold_
496 << " items or " << batchTimeThresholdMs_ << "ms)";
497 }
498 else if (!enable && wasEnabled)
499 {
500 // Stopping batch mode - flush remaining items and stop thread
501 flushBatch();
502 stopBatchWriter();
503 ARMARX_INFO << "Batch write mode DISABLED";
504 }
505 }
506
507 void
508 DiskPersistence::enqueueBatchItem(const armarx::armem::MemoryID& id,
509 const std::string& key,
510 std::vector<unsigned char> data)
511 {
512 bool shouldFlush = false;
513 {
514 std::lock_guard<std::mutex> lock(batchMutex_);
515
516 // Set batch start time on first item
517 if (batchBuffer_.empty())
518 {
519 batchStartTime_ = std::chrono::steady_clock::now();
520 }
521
522 // Add item to batch
523 batchBuffer_.push_back({id, key, std::move(data), std::chrono::steady_clock::now()});
524
525 // Check if we should flush due to size threshold
526 if (batchBuffer_.size() >= batchSizeThreshold_)
527 {
528 shouldFlush = true;
529 }
530 }
531
532 // Flush outside the lock if needed
533 if (shouldFlush)
534 {
535 flushBatchInternal(0); // 0 = flush by size
536 }
537 else
538 {
539 // Notify the background thread that there's work
540 batchCondition_.notify_one();
541 }
542 }
543
544 void
546 {
547 flushBatchInternal(2); // 2 = explicit flush
548 }
549
550 size_t
552 {
553 std::lock_guard<std::mutex> lock(batchMutex_);
554 return batchBuffer_.size();
555 }
556
557 void
558 DiskPersistence::flushBatchInternal(int reason)
559 {
560 std::vector<BatchWriteItem> itemsToWrite;
561
562 {
563 std::lock_guard<std::mutex> lock(batchMutex_);
564 if (batchBuffer_.empty())
565 {
566 return;
567 }
568 itemsToWrite = std::move(batchBuffer_);
569 batchBuffer_.clear();
570 }
571
572 // Update statistics for flush reason
573 switch (reason)
574 {
575 case 0: batchStats_.flushBySize.fetch_add(1, std::memory_order_relaxed); break;
576 case 1: batchStats_.flushByTime.fetch_add(1, std::memory_order_relaxed); break;
577 case 2: batchStats_.flushByExplicit.fetch_add(1, std::memory_order_relaxed); break;
578 }
579
580 // Write the batch
581 auto startTime = std::chrono::steady_clock::now();
582 writeBatch(itemsToWrite);
583 auto endTime = std::chrono::steady_clock::now();
584
585 // Update statistics
586 uint64_t durationNs = std::chrono::duration_cast<std::chrono::nanoseconds>(endTime - startTime).count();
587 batchStats_.totalBatchesWritten.fetch_add(1, std::memory_order_relaxed);
588 batchStats_.totalItemsBatched.fetch_add(itemsToWrite.size(), std::memory_order_relaxed);
589 batchStats_.totalFlushTimeNs.fetch_add(durationNs, std::memory_order_relaxed);
590
591 // Update max batch size
592 uint64_t currentMax = batchStats_.maxBatchSize.load(std::memory_order_relaxed);
593 while (itemsToWrite.size() > currentMax)
594 {
595 if (batchStats_.maxBatchSize.compare_exchange_weak(currentMax, itemsToWrite.size(), std::memory_order_relaxed))
596 {
597 break;
598 }
599 }
600
601 ARMARX_DEBUG << "Batch flush: " << itemsToWrite.size() << " items in "
602 << (durationNs / 1e6) << "ms (reason=" << reason << ")";
603 }
604
605 void
606 DiskPersistence::writeBatch(std::vector<BatchWriteItem>& items)
607 {
608 if (items.empty())
609 {
610 return;
611 }
612
613 // Check disk space once for the whole batch
614 if (!enoughDiskSpaceLeft())
615 {
616 ARMARX_ERROR << "Not enough disk space for batch write of " << items.size() << " items. Dropping batch!";
617 storageErrorCount_.fetch_add(items.size(), std::memory_order_relaxed);
618 return;
619 }
620
621 // Group items by directory path for efficient directory creation
622 std::unordered_map<std::string, std::vector<BatchWriteItem*>> itemsByDirectory;
623 for (auto& item : items)
624 {
625 std::string dirPath = getFullPath(item.id).string();
626 itemsByDirectory[dirPath].push_back(&item);
627 }
628
629 // Process each directory group
630 std::set<std::string> createdDirectories; // Track directories we've created
631 std::vector<std::string> directoriesToSync; // Directories that need fsync
632
633 for (auto& [dirPath, dirItems] : itemsByDirectory)
634 {
635 // Get or create directory mutex
636 std::mutex* dirMutex = nullptr;
637 {
638 std::lock_guard mapLock(directoryMutexMapLock_);
639 auto& mutexPtr = directoryMutexes_[dirPath];
640 if (!mutexPtr)
641 {
642 mutexPtr = std::make_unique<std::mutex>();
643 }
644 dirMutex = mutexPtr.get();
645 }
646
647 // Lock this directory for the batch write
648 std::lock_guard dirLock(*dirMutex);
649
650 try
651 {
652 // Create directory once for all items in this group
653 if (createdDirectories.find(dirPath) == createdDirectories.end())
654 {
655 // Use the first item's ID to create the directory
656 ensureFullPathExists(dirItems[0]->id, true);
657 createdDirectories.insert(dirPath);
658 }
659
660 // Write all files in this directory without individual fsync
661 for (auto* item : dirItems)
662 {
663 auto filePath = getFullPath(item->id) / item->key;
664
665 // Use non-atomic write for batch mode (we'll sync at the end)
666 util::fs::writeDataToFile(filePath, item->data, false);
667
668 batchStats_.totalBytesWritten.fetch_add(item->data.size(), std::memory_order_relaxed);
669 }
670
671 // Track this directory for final sync
672 directoriesToSync.push_back(dirPath);
673 }
674 catch (const std::exception& e)
675 {
676 ARMARX_ERROR << "Error writing batch to directory " << dirPath << ": " << e.what();
677 storageErrorCount_.fetch_add(dirItems.size(), std::memory_order_relaxed);
678 }
679 }
680
681 // Single fsync for all directories at the end (deferred sync)
682 for (const auto& dirPath : directoriesToSync)
683 {
684 int dfd = ::open(dirPath.c_str(), O_DIRECTORY | O_RDONLY);
685 if (dfd >= 0)
686 {
687 ::fsync(dfd);
688 ::close(dfd);
689 }
690 }
691 }
692
693 void
694 DiskPersistence::startBatchWriter()
695 {
696 if (batchWriterThread_.joinable())
697 {
698 return; // Already running
699 }
700
701 stopBatchWriter_.store(false, std::memory_order_release);
702 batchWriterThread_ = std::thread(&DiskPersistence::batchWriterThread, this);
703 }
704
705 void
706 DiskPersistence::stopBatchWriter()
707 {
708 stopBatchWriter_.store(true, std::memory_order_release);
709 batchCondition_.notify_all();
710
711 if (batchWriterThread_.joinable())
712 {
713 batchWriterThread_.join();
714 }
715
716 // Flush any remaining items
717 flushBatchInternal(2);
718 }
719
720 void
721 DiskPersistence::batchWriterThread()
722 {
723 ARMARX_DEBUG << "Batch writer thread started";
724
725 while (!stopBatchWriter_.load(std::memory_order_acquire))
726 {
727 bool shouldFlush = false;
728
729 {
730 std::unique_lock<std::mutex> lock(batchMutex_);
731
732 // Wait for items or timeout
733 auto timeout = std::chrono::milliseconds(batchTimeThresholdMs_);
734 batchCondition_.wait_for(lock, timeout, [this]() {
735 return stopBatchWriter_.load(std::memory_order_acquire) ||
736 batchBuffer_.size() >= batchSizeThreshold_;
737 });
738
739 // Check if we should flush due to time threshold
740 if (!batchBuffer_.empty())
741 {
742 auto now = std::chrono::steady_clock::now();
743 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
744 now - batchStartTime_).count();
745
746 if (elapsed >= static_cast<long long>(batchTimeThresholdMs_) ||
747 batchBuffer_.size() >= batchSizeThreshold_)
748 {
749 shouldFlush = true;
750 }
751 }
752 }
753
754 if (shouldFlush)
755 {
756 flushBatchInternal(1); // 1 = flush by time
757 }
758 }
759
760 ARMARX_DEBUG << "Batch writer thread stopped";
761 }
762
763} // namespace armarx::armem::server::ltm::persistence
static bool ReplaceEnvVars(std::string &string)
ReplaceEnvVars replaces environment variables in a string with their values, if the env.
bool containsContainer(const armarx::armem::MemoryID &id, std::string key) override
Checks if the container is available for the current memory id.
void setBatchWriteEnabled(bool enable)
Enable or disable batch writing.
std::vector< std::string > getItemKeys(const armarx::armem::MemoryID &id) override
Returns all items for the current id.
void storeItem(const armarx::armem::MemoryID &id, std::string key, std::vector< unsigned char > &data) override
Create a new file with name 'key' and stores the data inside it.
bool containsItem(const armarx::armem::MemoryID &id, std::string key) override
Checks if current container contains the item defined by its key.
std::vector< std::string > getContainerKeys(const armarx::armem::MemoryID &id) override
Returns all containers for the current id.
void flushBatch()
Explicitly flush all pending batch writes to disk.
size_t getBatchPendingCount() const
Get the current number of items pending in the batch buffer.
std::vector< unsigned char > retrieveItem(const armarx::armem::MemoryID &id, std::string key) override
Reads the data of the file with name 'key' at the current location.
bool enabled_
If false, the strategy is not writing or reading anything.
#define ARMARX_INFO
The normal logging level.
Definition Logging.h:179
#define ARMARX_ERROR
The logging level for unexpected behaviour, that must be fixed.
Definition Logging.h:194
#define ARMARX_DEBUG
The logging level for output that is only interesting while debugging.
Definition Logging.h:182
#define ARMARX_WARNING
The logging level for unexpected behaviour, but not a serious problem.
Definition Logging.h:191
bool isDateString(const std::string &s)
bool isNumberString(const std::string &s)
std::vector< unsigned char > readDataFromFile(const std::filesystem::path &p)
void ensureFileExists(const std::filesystem::path &p, bool createIfNotExistent=false)
bool directoryExists(const std::filesystem::path &p)
std::vector< std::filesystem::path > getAllFiles(const std::filesystem::path &p)
bool canCreateFiles(const std::filesystem::path &dir)
Check if we can create files in this directory.
std::filesystem::path toPath(const std::filesystem::path &base, const armem::MemoryID &id)
bool hasWritePermission(const std::filesystem::path &p)
Check if directory has write permission.
void writeDataToFile(const std::filesystem::path &p, const std::vector< unsigned char > &data, bool write_atomic=true)
bool fileExists(const std::filesystem::path &p)
void ensureDirectoryExists(const std::filesystem::path &p, bool createIfNotExistent=false)
std::vector< std::filesystem::path > getAllDirectories(const std::filesystem::path &p)