filesystem.cpp
Go to the documentation of this file.
1#include "filesystem.h"
2
3#include <errno.h>
4
5#include <filesystem>
6#include <fstream>
7#include <iostream>
8#include <stdexcept>
9#include <string>
10#include <system_error>
11#include <vector>
12
13#include <fcntl.h>
14#include <sys/stat.h>
15#include <unistd.h>
16#include <zlib.h>
17
18#include <SimoxUtility/algorithm/string.h>
19
21
24
26{
27
28 namespace detail
29 {
30 std::string
31 escapeName(const std::string& segmentName)
32 {
33 std::string ret = segmentName;
34 //simox::alg::replace_all(ret, Prefix, PrefixEscaped);
35 for (const auto& [s, r] : EscapeTable)
36 {
37 ret = simox::alg::replace_all(ret, s, r);
38 }
39 return ret;
40 }
41
42 std::string
43 unescapeName(const std::string& escapedName)
44 {
45 std::string ret = escapedName;
46 for (
47 const auto& [s, r] :
48 EscapeTable) // Here we assume that noone uses the replaced char usually in the segment name... TODO
49 {
50 ret = simox::alg::replace_all(ret, r, s);
51 }
52 return ret;
53 }
54
55 std::string
56 extractLastDirectoryFromPath(const std::string& path)
57 {
58 size_t pos = path.rfind('/');
59
60 if (pos != std::string::npos)
61 {
62 return path.substr(pos + 1);
63 }
64 else
65 {
66 return path;
67 }
68 }
69
70 std::string
71 toDayString(const Time& t)
72 {
73 return t.toDateString();
74 }
75
76 std::string
78 {
79 return std::to_string(t.toSecondsSinceEpoch());
80 }
81
82 bool
83 isNumberString(const std::string& s)
84 {
85 for (char const& ch : s)
86 {
87 if (std::isdigit(ch) == 0)
88 {
89 return false;
90 }
91 }
92 return true;
93 }
94
95 bool
96 isDateString(const std::string& s)
97 {
98 auto split = simox::alg::split(s, "-");
99 if (split.size() != 3)
100 {
101 return false;
102 }
103
105 }
106 } // namespace detail
107
108 std::filesystem::path
109 toPath(const std::filesystem::path& base, const armem::MemoryID& id)
110 {
111 ARMARX_CHECK(id.isWellDefined());
112
113 std::filesystem::path p = base;
114 if (id.hasMemoryName())
115 {
116 p /= detail::escapeName(id.memoryName);
117 }
118 if (id.hasCoreSegmentName())
119 {
120 p /= detail::escapeName(id.coreSegmentName);
121 }
122 if (id.hasProviderSegmentName())
123 {
124 p /= detail::escapeName(id.providerSegmentName);
125 }
126 if (id.hasEntityName())
127 {
128 p /= detail::escapeName(id.entityName);
129 }
130 if (id.hasTimestamp())
131 {
134 p /= id.timestampStr();
135 }
136 if (id.hasInstanceIndex())
137 {
138 p /= id.instanceIndexStr();
139 }
140
141 return p;
142 }
143
144 bool
145 directoryExists(const std::filesystem::path& p)
146 {
147 return std::filesystem::exists(p) and std::filesystem::is_directory(p);
148 }
149
150 bool
151 fileExists(const std::filesystem::path& p)
152 {
153 return std::filesystem::exists(p) && std::filesystem::is_regular_file(p);
154 }
155
156 void
157 ensureDirectoryExists(const std::filesystem::path& p, bool createIfNotExistent)
158 {
159 if (createIfNotExistent)
160 {
161 // Use error_code version to avoid exceptions for already-exists case
162 std::error_code ec;
163 std::filesystem::create_directories(p, ec);
164
165 // Only error if creation failed AND directory still doesn't exist
166 if (ec && !std::filesystem::is_directory(p))
167 {
168 throw armarx::LocalException("Failed to create directory '" + p.string() +
169 "': " + ec.message() + " (code: " +
170 std::to_string(ec.value()) + ")");
171 }
172 // If directory exists now (either we created it or someone else did), success!
173 }
174 else if (!directoryExists(p))
175 {
176 throw armarx::LocalException("Directory existence cannot be ensured: " + p.string());
177 }
178 }
179
180 void
181 ensureFileExists(const std::filesystem::path& p, bool createIfNotExistent)
182 {
183 ensureDirectoryExists(p.parent_path(), createIfNotExistent);
184
185 // Don't create the file here - just verify parent directory exists
186 // The actual file will be created by writeDataToFile
187
188 if (!createIfNotExistent && !fileExists(p))
189 {
190 throw armarx::LocalException("File existence cannot be ensured: " + p.string());
191 }
192 }
193
194 void
195 writeDataToFile(const std::filesystem::path& p,
196 const std::vector<unsigned char>& data,
197 WriteMode mode)
198 {
199 namespace fs = std::filesystem;
200
201 if (mode == WriteMode::Plain)
202 {
203 std::ofstream dataofs;
204 dataofs.open(p);
205 if (!dataofs)
206 {
207 throw armarx::LocalException("Could not write data to filesystem file '" +
208 p.string() + "'. Skipping this file.");
209 }
210 dataofs.write(reinterpret_cast<const char*>(data.data()), data.size());
211 dataofs.close();
212 return;
213 }
214
215 const fs::path dir = p.parent_path().empty() ? fs::current_path() : p.parent_path();
216 const std::string filename = p.filename().string();
217
218 // mkstemp template must end with XXXXXX and be mutable
219 fs::path tmpl = dir / (filename + ".tmpXXXXXX");
220 std::string tmpl_str = tmpl.string();
221
222 // 1) Create a unique temp file in the same directory
223 int fd = ::mkstemp(tmpl_str.data());
224 if (fd == -1)
225 {
226 throw std::system_error(errno, std::generic_category(), "mkstemp failed");
227 }
228
229 // 2) Write the whole buffer
230 const unsigned char* buf = data.data();
231 size_t left = data.size();
232 while (left > 0)
233 {
234 ssize_t n = ::write(fd, buf, left);
235 if (n < 0)
236 {
237 int e = errno;
238 ::close(fd);
239 ::unlink(tmpl_str.c_str());
240 throw std::system_error(e, std::generic_category(), "write failed");
241 }
242 buf += n;
243 left -= static_cast<size_t>(n);
244 }
245
246 // 3) Flush file contents to disk. Skipped for WriteMode::Atomic: the rename below still
247 // gives readers all-or-nothing contents, and a batch writer wants one directory sync at
248 // the end rather than a synchronous flush per file.
249 if (mode == WriteMode::AtomicDurable and ::fdatasync(fd) != 0)
250 {
251 int e = errno;
252 ::close(fd);
253 ::unlink(tmpl_str.c_str());
254 throw std::system_error(e, std::generic_category(), "fdatasync failed");
255 }
256
257 if (::close(fd) != 0)
258 {
259 int e = errno;
260 ::unlink(tmpl_str.c_str());
261 throw std::system_error(e, std::generic_category(), "close failed");
262 }
263
264 // 4) Atomically replace the destination
265 if (::rename(tmpl_str.c_str(), p.c_str()) != 0)
266 {
267 int e = errno;
268 ::unlink(tmpl_str.c_str());
269 throw std::system_error(e, std::generic_category(), "rename failed");
270 }
271
272 // 5) Make the rename durable by syncing the directory. The caller does this once for the
273 // whole batch under WriteMode::Atomic.
274 if (mode == WriteMode::AtomicDurable)
275 {
276 int dfd = ::open(dir.c_str(), O_DIRECTORY | O_RDONLY);
277 if (dfd >= 0)
278 {
279 (void)::fsync(dfd);
280 (void)::close(dfd);
281 }
282 }
283 }
284
285 std::vector<unsigned char>
286 readDataFromFile(const std::filesystem::path& p)
287 {
288
289 if (!std::filesystem::exists(p))
290 {
291 throw std::runtime_error("File not found: " + p.string());
292 }
293
294 std::ifstream dataifs(p, std::ios::binary);
295
296 if (!dataifs)
297 {
298 throw std::runtime_error("Could not open file: " + p.string());
299 }
300
301 std::vector<unsigned char> datafilecontent((std::istreambuf_iterator<char>(dataifs)),
302 (std::istreambuf_iterator<char>()));
303
304
305 if (dataifs.bad())
306 {
307 throw std::runtime_error("Error reading file: " + p.string());
308 }
309
310 dataifs.close();
311 return datafilecontent;
312 }
313
314 bool
315 isGzip(const std::vector<unsigned char>& data)
316 {
317 return data.size() >= 2 and data[0] == 0x1f and data[1] == 0x8b;
318 }
319
320 namespace
321 {
322 /// windowBits 15 + 16 selects a gzip container (rather than raw deflate or zlib).
323 constexpr int GZIP_WINDOW_BITS = 15 + 16;
324 constexpr int GZIP_MEM_LEVEL = 8;
325 constexpr std::size_t GZIP_CHUNK = 64 * 1024;
326 } // namespace
327
328 std::vector<unsigned char>
329 gzipCompress(const std::vector<unsigned char>& data, int level)
330 {
331 z_stream zs{};
332 int ret = ::deflateInit2(
333 &zs, level, Z_DEFLATED, GZIP_WINDOW_BITS, GZIP_MEM_LEVEL, Z_DEFAULT_STRATEGY);
334 if (ret != Z_OK)
335 {
336 throw std::runtime_error("gzipCompress: deflateInit2 failed with code " +
337 std::to_string(ret));
338 }
339
340 std::vector<unsigned char> out;
341 // deflateBound is an upper bound on the output size, so a single deflate() call suffices.
342 out.resize(::deflateBound(&zs, static_cast<uLong>(data.size())));
343
344 zs.next_in = const_cast<Bytef*>(data.data());
345 zs.avail_in = static_cast<uInt>(data.size());
346 zs.next_out = out.data();
347 zs.avail_out = static_cast<uInt>(out.size());
348
349 ret = ::deflate(&zs, Z_FINISH);
350 const std::size_t written = zs.total_out;
351 ::deflateEnd(&zs);
352
353 if (ret != Z_STREAM_END)
354 {
355 throw std::runtime_error("gzipCompress: deflate did not finish, code " +
356 std::to_string(ret));
357 }
358
359 out.resize(written);
360 return out;
361 }
362
363 std::vector<unsigned char>
364 gzipDecompress(const std::vector<unsigned char>& data)
365 {
366 z_stream zs{};
367 int ret = ::inflateInit2(&zs, GZIP_WINDOW_BITS);
368 if (ret != Z_OK)
369 {
370 throw std::runtime_error("gzipDecompress: inflateInit2 failed with code " +
371 std::to_string(ret));
372 }
373
374 zs.next_in = const_cast<Bytef*>(data.data());
375 zs.avail_in = static_cast<uInt>(data.size());
376
377 std::vector<unsigned char> out;
378 std::vector<unsigned char> chunk(GZIP_CHUNK);
379
380 do
381 {
382 zs.next_out = chunk.data();
383 zs.avail_out = static_cast<uInt>(chunk.size());
384
385 ret = ::inflate(&zs, Z_NO_FLUSH);
386
387 if (ret != Z_OK and ret != Z_STREAM_END and ret != Z_BUF_ERROR)
388 {
389 ::inflateEnd(&zs);
390 throw std::runtime_error("gzipDecompress: inflate failed with code " +
391 std::to_string(ret));
392 }
393
394 out.insert(out.end(), chunk.begin(), chunk.end() - zs.avail_out);
395 // Keep going only while the output buffer was filled completely; if inflate left
396 // room and did not report Z_STREAM_END, the input is truncated.
397 } while (ret != Z_STREAM_END and zs.avail_out == 0);
398
399 ::inflateEnd(&zs);
400
401 if (ret != Z_STREAM_END)
402 {
403 throw std::runtime_error("gzipDecompress: truncated gzip stream");
404 }
405
406 return out;
407 }
408
409 std::vector<std::filesystem::path>
410 getAllDirectories(const std::filesystem::path& p)
411 {
412 std::vector<std::filesystem::path> ret;
413 for (const auto& subdir : std::filesystem::directory_iterator(p))
414 {
415 std::filesystem::path subdirPath = subdir.path();
416 if (std::filesystem::is_directory(subdirPath))
417 {
418 ret.push_back(subdirPath);
419 }
420 }
421 std::sort(ret.begin(),
422 ret.end(),
423 [](const std::filesystem::path& a, const std::filesystem::path& b) -> bool
424 { return a.string() < b.string(); });
425 return ret;
426 }
427
428 std::vector<std::filesystem::path>
429 getAllFiles(const std::filesystem::path& p)
430 {
431 std::vector<std::filesystem::path> ret;
432 for (const auto& subdir : std::filesystem::directory_iterator(p))
433 {
434 std::filesystem::path subdirPath = subdir.path();
435 if (std::filesystem::is_regular_file(subdirPath))
436 {
437 ret.push_back(subdirPath);
438 }
439 }
440 std::sort(ret.begin(),
441 ret.end(),
442 [](const std::filesystem::path& a, const std::filesystem::path& b) -> bool
443 { return a.string() > b.string(); });
444 return ret;
445 }
446
447 bool
448 hasWritePermission(const std::filesystem::path& p)
449 {
450 namespace fs = std::filesystem;
451
452 if (!fs::exists(p))
453 {
454 return false; // Can't write to non-existent directory
455 }
456
457 if (!fs::is_directory(p))
458 {
459 return false; // Not a directory
460 }
461
462 // Try to get permissions
463 std::error_code ec;
464 auto perms = fs::status(p, ec).permissions();
465
466 if (ec)
467 {
468 return false; // Can't determine permissions
469 }
470
471 // Check for owner write permission
472 // Note: This is a basic check, doesn't account for group/other or ACLs
473 using fs::perms;
474 return (perms & perms::owner_write) != perms::none;
475 }
476
477 bool
478 canCreateFiles(const std::filesystem::path& dir)
479 {
480 namespace fs = std::filesystem;
481
482 if (!fs::exists(dir) || !fs::is_directory(dir))
483 {
484 return false;
485 }
486
487 // Try to create a temp file
488 fs::path testFile = dir / ".ltm_write_test_XXXXXX";
489 std::string testPath = testFile.string();
490
491 int fd = ::mkstemp(testPath.data());
492 if (fd == -1)
493 {
494 return false; // Can't create files
495 }
496
497 ::close(fd);
498 ::unlink(testPath.c_str());
499 return true;
500 }
501} // namespace armarx::armem::server::ltm::util::fs
std::string timestamp()
std::int64_t toSecondsSinceEpoch() const
Definition DateTime.cpp:99
std::string toDateString() const
Definition DateTime.cpp:63
#define ARMARX_CHECK(expression)
Shortcut for ARMARX_CHECK_EXPRESSION.
std::string unescapeName(const std::string &escapedName)
std::string escapeName(const std::string &segmentName)
std::string toSecondsString(const Time &t)
std::string extractLastDirectoryFromPath(const std::string &path)
bool isNumberString(const std::string &s)
void writeDataToFile(const std::filesystem::path &p, const std::vector< unsigned char > &data, WriteMode mode)
std::vector< unsigned char > readDataFromFile(const std::filesystem::path &p)
bool directoryExists(const std::filesystem::path &p)
std::vector< std::filesystem::path > getAllFiles(const std::filesystem::path &p)
void ensureDirectoryExists(const std::filesystem::path &p, bool createIfNotExistent)
bool canCreateFiles(const std::filesystem::path &dir)
std::filesystem::path toPath(const std::filesystem::path &base, const armem::MemoryID &id)
std::vector< unsigned char > gzipCompress(const std::vector< unsigned char > &data, int level)
bool hasWritePermission(const std::filesystem::path &p)
void ensureFileExists(const std::filesystem::path &p, bool createIfNotExistent)
bool isGzip(const std::vector< unsigned char > &data)
std::vector< unsigned char > gzipDecompress(const std::vector< unsigned char > &data)
bool fileExists(const std::filesystem::path &p)
std::vector< std::filesystem::path > getAllDirectories(const std::filesystem::path &p)
armarx::core::time::DateTime Time
std::vector< std::string > split(const std::string &source, const std::string &splitBy, bool trimElements=false, bool removeEmptyElements=false)