| /* |
| * Copyright (C) 2004-2023 Savoir-faire Linux Inc. |
| * |
| * This program is free software: you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License as published by |
| * the Free Software Foundation, either version 3 of the License, or |
| * (at your option) any later version. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| * |
| * You should have received a copy of the GNU General Public License |
| * along with this program. If not, see <https://www.gnu.org/licenses/>. |
| */ |
| #include "fileutils.h" |
| |
| #include <opendht/crypto.h> |
| |
| #ifdef RING_UWP |
| #include <io.h> // for access and close |
| #endif |
| |
| #ifdef __APPLE__ |
| #include <TargetConditionals.h> |
| #endif |
| |
| #ifdef _WIN32 |
| #include <windows.h> |
| #include "string_utils.h" |
| #endif |
| |
| #include <sys/types.h> |
| #include <sys/stat.h> |
| #ifndef _MSC_VER |
| #include <libgen.h> |
| #endif |
| #include <fcntl.h> |
| #include <signal.h> |
| #include <unistd.h> |
| |
| #include <sstream> |
| #include <fstream> |
| #include <iostream> |
| #include <stdexcept> |
| #include <limits> |
| #include <array> |
| #include <filesystem> |
| |
| #include <cstdlib> |
| #include <cstring> |
| #include <cerrno> |
| #include <cstddef> |
| #include <ciso646> |
| |
| |
| #define ERASE_BLOCK 4096 |
| |
| namespace dhtnet { |
| namespace fileutils { |
| |
| // returns true if directory exists or was created |
| bool |
| check_dir(const std::filesystem::path& path, mode_t dirmode, mode_t parentmode) |
| { |
| if (std::filesystem::exists(path)) |
| return true; |
| if (path.has_parent_path()) |
| check_dir(path.parent_path(), parentmode, parentmode); |
| std::error_code ec; |
| if (std::filesystem::create_directory(path, ec)) { |
| std::filesystem::permissions(path, (std::filesystem::perms)dirmode); |
| return true; |
| } |
| return false; |
| } |
| |
| std::mutex& |
| getFileLock(const std::filesystem::path& path) |
| { |
| static std::mutex fileLockLock {}; |
| static std::map<std::string, std::mutex> fileLocks {}; |
| |
| std::lock_guard l(fileLockLock); |
| return fileLocks[path.string()]; |
| } |
| |
| bool |
| isFile(const std::filesystem::path& path, bool resolveSymlink) |
| { |
| auto status = resolveSymlink ? std::filesystem::status(path) : std::filesystem::symlink_status(path); |
| return std::filesystem::is_regular_file(status); |
| } |
| |
| bool |
| isDirectory(const std::filesystem::path& path) |
| { |
| return std::filesystem::is_directory(path); |
| } |
| |
| bool |
| hasHardLink(const std::filesystem::path& path) |
| { |
| return std::filesystem::hard_link_count(path) > 1; |
| } |
| |
| bool |
| isSymLink(const std::filesystem::path& path) |
| { |
| return std::filesystem::is_symlink(path); |
| } |
| |
| template <typename TP> |
| std::chrono::system_clock::time_point to_sysclock(TP tp) |
| { |
| using namespace std::chrono; |
| return time_point_cast<system_clock::duration>(tp - TP::clock::now() + system_clock::now()); |
| } |
| |
| bool |
| createSymlink(const std::string& linkFile, const std::string& target) |
| { |
| try { |
| std::filesystem::create_symlink(target, linkFile); |
| } catch (const std::exception& e) { |
| //JAMI_ERR("Couldn't create soft link: %s", e.what()); |
| return false; |
| } |
| return true; |
| } |
| |
| bool |
| createHardlink(const std::string& linkFile, const std::string& target) |
| { |
| try { |
| std::filesystem::create_hard_link(target, linkFile); |
| } catch (const std::exception& e) { |
| //JAMI_ERR("Couldn't create hard link: %s", e.what()); |
| return false; |
| } |
| return true; |
| } |
| |
| void |
| createFileLink(const std::string& linkFile, const std::string& target, bool hard) |
| { |
| if (not hard or not createHardlink(linkFile, target)) |
| createSymlink(linkFile, target); |
| } |
| |
| std::vector<uint8_t> |
| loadFile(const std::filesystem::path& path) |
| { |
| std::vector<uint8_t> buffer; |
| std::ifstream file(path, std::ios::binary); |
| if (!file) |
| throw std::runtime_error("Can't read file: " + path.string()); |
| file.seekg(0, std::ios::end); |
| auto size = file.tellg(); |
| if (size > std::numeric_limits<unsigned>::max()) |
| throw std::runtime_error("File is too big: " + path.string()); |
| buffer.resize(size); |
| file.seekg(0, std::ios::beg); |
| if (!file.read((char*) buffer.data(), size)) |
| throw std::runtime_error("Can't load file: " + path.string()); |
| return buffer; |
| } |
| |
| void |
| saveFile(const std::filesystem::path& path, const uint8_t* data, size_t data_size, mode_t mode) |
| { |
| std::ofstream file(path, std::ios::trunc | std::ios::binary); |
| if (!file.is_open()) { |
| //JAMI_ERR("Could not write data to %s", path.c_str()); |
| return; |
| } |
| file.write((char*) data, data_size); |
| file.close(); |
| std::filesystem::permissions(path, (std::filesystem::perms)mode); |
| } |
| |
| std::vector<std::string> |
| readDirectory(const std::filesystem::path& dir) |
| { |
| std::vector<std::string> files; |
| std::error_code ec; |
| for (const auto& entry : std::filesystem::directory_iterator(dir, ec)) { |
| files.emplace_back(entry.path().filename().string()); |
| } |
| return files; |
| } |
| |
| bool |
| recursive_mkdir(const std::filesystem::path& path, mode_t mode) |
| { |
| std::error_code ec; |
| std::filesystem::create_directories(path, ec); |
| if (!ec) |
| std::filesystem::permissions(path, (std::filesystem::perms)mode, ec); |
| return !ec; |
| } |
| |
| #ifdef _WIN32 |
| bool |
| eraseFile_win32(const std::string& path, bool dosync) |
| { |
| HANDLE h |
| = CreateFileA(path.c_str(), GENERIC_WRITE, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0); |
| if (h == INVALID_HANDLE_VALUE) { |
| // JAMI_WARN("Can not open file %s for erasing.", path.c_str()); |
| return false; |
| } |
| |
| LARGE_INTEGER size; |
| if (!GetFileSizeEx(h, &size)) { |
| // JAMI_WARN("Can not erase file %s: GetFileSizeEx() failed.", path.c_str()); |
| CloseHandle(h); |
| return false; |
| } |
| if (size.QuadPart == 0) { |
| CloseHandle(h); |
| return false; |
| } |
| |
| uint64_t size_blocks = size.QuadPart / ERASE_BLOCK; |
| if (size.QuadPart % ERASE_BLOCK) |
| size_blocks++; |
| |
| char* buffer; |
| try { |
| buffer = new char[ERASE_BLOCK]; |
| } catch (std::bad_alloc& ba) { |
| // JAMI_WARN("Can not allocate buffer for erasing %s.", path.c_str()); |
| CloseHandle(h); |
| return false; |
| } |
| memset(buffer, 0x00, ERASE_BLOCK); |
| |
| OVERLAPPED ovlp; |
| if (size.QuadPart < (1024 - 42)) { // a small file can be stored in the MFT record |
| ovlp.Offset = 0; |
| ovlp.OffsetHigh = 0; |
| WriteFile(h, buffer, (DWORD) size.QuadPart, 0, &ovlp); |
| FlushFileBuffers(h); |
| } |
| for (uint64_t i = 0; i < size_blocks; i++) { |
| uint64_t offset = i * ERASE_BLOCK; |
| ovlp.Offset = offset & 0x00000000FFFFFFFF; |
| ovlp.OffsetHigh = offset >> 32; |
| WriteFile(h, buffer, ERASE_BLOCK, 0, &ovlp); |
| } |
| |
| delete[] buffer; |
| |
| if (dosync) |
| FlushFileBuffers(h); |
| |
| CloseHandle(h); |
| return true; |
| } |
| |
| #else |
| |
| bool |
| eraseFile_posix(const std::string& path, bool dosync) |
| { |
| struct stat st; |
| if (stat(path.c_str(), &st) == -1) { |
| //JAMI_WARN("Can not erase file %s: fstat() failed.", path.c_str()); |
| return false; |
| } |
| // Remove read-only flag if possible |
| chmod(path.c_str(), st.st_mode | (S_IWGRP+S_IWUSR) ); |
| |
| int fd = open(path.c_str(), O_WRONLY); |
| if (fd == -1) { |
| //JAMI_WARN("Can not open file %s for erasing.", path.c_str()); |
| return false; |
| } |
| |
| if (st.st_size == 0) { |
| close(fd); |
| return false; |
| } |
| |
| lseek(fd, 0, SEEK_SET); |
| |
| std::array<char, ERASE_BLOCK> buffer; |
| buffer.fill(0); |
| decltype(st.st_size) written(0); |
| while (written < st.st_size) { |
| auto ret = write(fd, buffer.data(), buffer.size()); |
| if (ret < 0) { |
| //JAMI_WARNING("Error while overriding file with zeros."); |
| break; |
| } else |
| written += ret; |
| } |
| |
| if (dosync) |
| fsync(fd); |
| |
| close(fd); |
| return written >= st.st_size; |
| } |
| #endif |
| |
| bool |
| eraseFile(const std::string& path, bool dosync) |
| { |
| #ifdef _WIN32 |
| return eraseFile_win32(path, dosync); |
| #else |
| return eraseFile_posix(path, dosync); |
| #endif |
| } |
| |
| int |
| remove(const std::filesystem::path& path, bool erase) |
| { |
| if (erase and isFile(path, false) and !hasHardLink(path)) |
| eraseFile(path.string(), true); |
| |
| #ifdef _WIN32 |
| // use Win32 api since std::remove will not unlink directory in use |
| if (isDirectory(path)) |
| return !RemoveDirectory(dhtnet::to_wstring(path.string()).c_str()); |
| #endif |
| |
| std::error_code ec; |
| std::filesystem::remove(path, ec); |
| return ec.value(); |
| } |
| |
| int |
| removeAll(const std::filesystem::path& path, bool erase) |
| { |
| try { |
| std::error_code ec; |
| if (not erase) { |
| std::filesystem::remove_all(path, ec); |
| return ec.value(); |
| } |
| if (path.empty()) |
| return -1; |
| |
| auto status = std::filesystem::status(path, ec); |
| if (!ec && std::filesystem::is_directory(status) and not std::filesystem::is_symlink(status)) { |
| for (const auto& entry: std::filesystem::directory_iterator(path, ec)) { |
| removeAll(entry.path(), erase); |
| } |
| } |
| return remove(path, erase); |
| } catch (const std::exception& e) { |
| //JAMI_ERR("Error while removing %s: %s", path.c_str(), e.what()); |
| return -1; |
| } |
| } |
| |
| void |
| openStream(std::ifstream& file, const std::filesystem::path& path, std::ios_base::openmode mode) |
| { |
| #ifdef _WIN32 |
| file.open(dhtnet::to_wstring(path.string()), mode); |
| #else |
| file.open(path, mode); |
| #endif |
| } |
| |
| void |
| openStream(std::ofstream& file, const std::filesystem::path& path, std::ios_base::openmode mode) |
| { |
| #ifdef _WIN32 |
| file.open(dhtnet::to_wstring(path.string()), mode); |
| #else |
| file.open(path, mode); |
| #endif |
| } |
| |
| std::ifstream |
| ifstream(const std::filesystem::path& path, std::ios_base::openmode mode) |
| { |
| #ifdef _WIN32 |
| return std::ifstream(dhtnet::to_wstring(path.string()), mode); |
| #else |
| return std::ifstream(path, mode); |
| #endif |
| } |
| |
| std::ofstream |
| ofstream(const std::filesystem::path& path, std::ios_base::openmode mode) |
| { |
| #ifdef _WIN32 |
| return std::ofstream(dhtnet::to_wstring(path.string()), mode); |
| #else |
| return std::ofstream(path, mode); |
| #endif |
| } |
| |
| int |
| accessFile(const std::filesystem::path& file, int mode) |
| { |
| #ifdef _WIN32 |
| return _waccess(dhtnet::to_wstring(file.string()).c_str(), mode); |
| #else |
| return access(file.c_str(), mode); |
| #endif |
| } |
| |
| constexpr auto ID_TIMEOUT = std::chrono::hours(24); |
| |
| void |
| IdList::load() |
| { |
| size_t pruned = 0; |
| auto now = std::chrono::system_clock::now(); |
| try { |
| std::ifstream file(path, std::ios::binary); |
| msgpack::unpacker unp; |
| auto timeout = now - ID_TIMEOUT; |
| while (file.is_open() && !file.eof()) { |
| unp.reserve_buffer(8 * 1024); |
| file.read(unp.buffer(), unp.buffer_capacity()); |
| unp.buffer_consumed(file.gcount()); |
| msgpack::unpacked result; |
| while (unp.next(result)) { |
| auto kv = result.get().as<std::pair<uint64_t, std::chrono::system_clock::time_point>>(); |
| if (kv.second > timeout) |
| ids.insert(std::move(kv)); |
| else |
| pruned++; |
| } |
| } |
| } catch (const std::exception& e) { |
| // discard corrupted files |
| std::error_code ec; |
| std::filesystem::remove(path, ec); |
| } |
| last_maintain = now; |
| if (pruned) { |
| std::ofstream file(path, std::ios::trunc | std::ios::binary); |
| for (auto& kv : ids) |
| msgpack::pack(file, kv); |
| } |
| } |
| |
| bool |
| IdList::add(uint64_t id) |
| { |
| auto now = std::chrono::system_clock::now(); |
| auto r = ids.emplace(id, now); |
| if (r.second) { |
| auto timeout = now - ID_TIMEOUT; |
| if (last_maintain > timeout) { |
| // append |
| std::ofstream file(path, std::ios::app | std::ios::binary); |
| if (file.is_open()) { |
| msgpack::pack(file, *r.first); |
| } |
| } else { |
| // maintain and save |
| std::ofstream file(path, std::ios::trunc | std::ios::binary); |
| for (auto it = ids.begin(); it != ids.end();) { |
| if (it->second < timeout) { |
| it = ids.erase(it); |
| } else { |
| msgpack::pack(file, *it); |
| ++it; |
| } |
| } |
| last_maintain = now; |
| } |
| return true; |
| } |
| return false; |
| } |
| |
| } // namespace fileutils |
| } // namespace dhtnet |