230 lines
5.2 KiB
C++
230 lines
5.2 KiB
C++
/*
|
||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||
*/
|
||
|
||
#include "FsUtils.hpp"
|
||
#include "Base/Logger.hpp"
|
||
#include "Base/Utils.hpp"
|
||
#include <unordered_map>
|
||
#include <regex>
|
||
|
||
namespace fs = std::filesystem;
|
||
|
||
namespace OpenVulkano
|
||
{
|
||
bool FsUtils::DeleteEmptyFilesAndDirs(const std::filesystem::path& dir, const bool recursive)
|
||
{
|
||
bool deleted = false;
|
||
for (auto& p: fs::directory_iterator(dir))
|
||
{
|
||
if (fs::is_directory(p))
|
||
{
|
||
if (recursive)
|
||
{
|
||
deleted |= (DeleteEmptyFilesAndDirs(p, recursive));
|
||
}
|
||
if (fs::is_empty(p))
|
||
{
|
||
fs::remove(p);
|
||
Logger::FILESYS->info("Deleted empty directory: {}", p.path().string());
|
||
deleted = true;
|
||
}
|
||
}
|
||
else if (fs::is_empty(p))
|
||
{
|
||
fs::remove(p);
|
||
Logger::FILESYS->info("Deleted empty file: {}", p.path().string());
|
||
deleted = true;
|
||
}
|
||
}
|
||
return deleted;
|
||
}
|
||
|
||
bool FsUtils::DeleteEmptyDirs(const std::filesystem::path& dir, const bool recursive)
|
||
{
|
||
bool deleted = false;
|
||
for (auto& p: fs::directory_iterator(dir))
|
||
{
|
||
if (fs::is_directory(p))
|
||
{
|
||
if (recursive)
|
||
{
|
||
deleted |= (DeleteEmptyDirs(p, recursive));
|
||
}
|
||
if (fs::is_empty(p))
|
||
{
|
||
fs::remove(p);
|
||
Logger::FILESYS->info("Deleted empty directory: {}", p.path().string());
|
||
deleted = true;
|
||
}
|
||
}
|
||
}
|
||
return deleted;
|
||
}
|
||
|
||
bool FsUtils::DeleteEmptySubTrees(const std::filesystem::path& dir)
|
||
{
|
||
bool deleted = false;
|
||
for (auto& p: fs::directory_iterator(dir))
|
||
{
|
||
if (fs::is_directory(p))
|
||
{
|
||
if (IsTreeEmpty(p))
|
||
{
|
||
fs::remove_all(p);
|
||
deleted = true;
|
||
}
|
||
}
|
||
}
|
||
return deleted;
|
||
}
|
||
|
||
bool FsUtils::IsTreeEmpty(const std::filesystem::path& dir)
|
||
{
|
||
for (auto& p: fs::directory_iterator(dir))
|
||
{
|
||
if (fs::is_directory(p))
|
||
{
|
||
if (!IsTreeEmpty(p))
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
ByteSize FsUtils::GetDirSize(const std::filesystem::path& dir)
|
||
{
|
||
ByteSize size = 0;
|
||
|
||
for (auto& p: fs::directory_iterator(dir))
|
||
{
|
||
if (fs::is_directory(p))
|
||
{
|
||
size += GetDirSize(p);
|
||
}
|
||
else
|
||
{
|
||
size += p.file_size();
|
||
}
|
||
}
|
||
|
||
return size;
|
||
}
|
||
|
||
FsCount FsUtils::GetDirFileAndDirCount(const std::filesystem::path& dir)
|
||
{
|
||
FsCount count;
|
||
for (auto& p: fs::directory_iterator(dir))
|
||
{
|
||
if (fs::is_directory(p))
|
||
{
|
||
count += GetDirFileAndDirCount(p);
|
||
count.dirCount++;
|
||
}
|
||
else
|
||
{
|
||
count.fileCount++;
|
||
}
|
||
}
|
||
return count;
|
||
}
|
||
|
||
std::string FsUtils::ToValidFileName(std::string str)
|
||
{
|
||
// Map of characters to replace with similar looking ones
|
||
static const std::unordered_map<char, std::string> replacements = {
|
||
{'<', "<"}, // Use full-width less than
|
||
{'>', ">"}, // Use full-width greater than
|
||
{':', "꞉"}, // Use modifier letter colon
|
||
{'"', """}, // Use full-width quotation mark
|
||
{'/', "⁄"}, // Use fraction slash
|
||
{'\\', "⧹"}, // Use big reverse solidus
|
||
{'|', "|"}, // Use full-width vertical line
|
||
{'?', "?"}, // Use full-width question mark
|
||
{'*', "∗"}, // Use asterisk operator
|
||
};
|
||
|
||
// Characters to remove (control characters and others that might cause issues)
|
||
static const std::regex remove_pattern("[\\x00-\\x1F\\x7F~`!@#$%^&(){}\\[\\]=+,;]");
|
||
|
||
// Maximum length for many filesystems
|
||
static const size_t MAX_LENGTH = 255;
|
||
|
||
// Replace problematic characters with similar looking ones
|
||
for (const auto& [key, value] : replacements) {
|
||
size_t pos = 0;
|
||
while ((pos = str.find(key, pos)) != std::string::npos) {
|
||
str.replace(pos, 1, value);
|
||
pos += value.length();
|
||
}
|
||
}
|
||
|
||
// Remove other invalid characters
|
||
str = std::regex_replace(str, remove_pattern, "");
|
||
|
||
// Trim leading/trailing spaces and periods
|
||
str = std::regex_replace(str, std::regex("^[\\.\\s]+"), "");
|
||
str = std::regex_replace(str, std::regex("[\\.\\s]+$"), "");
|
||
|
||
// Replace multiple spaces with a single space
|
||
str = std::regex_replace(str, std::regex("\\s+"), " ");
|
||
|
||
// Ensure the string isn't empty after sanitization
|
||
if (str.empty()) {
|
||
str = "unnamed_file";
|
||
}
|
||
|
||
// Truncate if too long, being careful not to cut in the middle of a UTF-8 character
|
||
if (str.length() > MAX_LENGTH) {
|
||
size_t len = MAX_LENGTH;
|
||
while (len > 0 && (str[len] & 0xC0) == 0x80) {
|
||
--len;
|
||
}
|
||
str = str.substr(0, len);
|
||
}
|
||
|
||
return str;
|
||
}
|
||
|
||
std::vector<std::filesystem::path> FsUtils::FilesWithExtension(const std::filesystem::path& dir, std::string ext)
|
||
{
|
||
std::vector<std::filesystem::path> files;
|
||
|
||
if (!fs::exists(dir) || !fs::is_directory(dir))
|
||
{
|
||
Logger::FILESYS->warn("Directory does not exist or is not a directory: {}", dir.string());
|
||
return files;
|
||
}
|
||
|
||
if (!ext.empty() && ext[0] != '.') ext = "." + ext;
|
||
Utils::ToLower(ext);
|
||
|
||
try
|
||
{
|
||
for (const auto& entry : fs::directory_iterator(dir))
|
||
{
|
||
std::string fExt = entry.path().extension().string();
|
||
Utils::ToLower(fExt);
|
||
if (fs::is_regular_file(entry) && fExt == ext)
|
||
{
|
||
files.push_back(entry.path());
|
||
}
|
||
}
|
||
}
|
||
catch (const fs::filesystem_error& e)
|
||
{
|
||
Logger::FILESYS->error("Filesystem error when searching for files: {}", e.what());
|
||
}
|
||
|
||
return files;
|
||
}
|
||
}
|