mirror of
https://github.com/nestriness/cdc-file-transfer.git
synced 2026-01-30 12:25:35 +02:00
Adds support for local syncs of files and folders on the same Windows machine, e.g. cdc_rsync C:\source C:\dest. The two main changes are - Skip the check whether the port is available remotely with PortManager. - Do not deploy cdc_rsync_server. - Run cdc_rsync_server directly, not through an SSH tunnel. The current implementation is not optimal as it starts cdc_rsync_server as a separate process and communicates to it via a TCP port.
1188 lines
37 KiB
C++
1188 lines
37 KiB
C++
// Copyright 2022 Google LLC
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
#include "common/path.h"
|
|
|
|
#include <errno.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
|
|
#include <algorithm>
|
|
#include <cassert>
|
|
#include <filesystem> // NOLINT [We are not in google3]
|
|
|
|
#include "absl/strings/ascii.h"
|
|
#include "absl/strings/str_format.h"
|
|
#include "absl/strings/str_split.h"
|
|
#include "common/buffer.h"
|
|
#include "common/errno_mapping.h"
|
|
#include "common/log.h"
|
|
#include "common/status.h"
|
|
#include "common/status_macros.h"
|
|
#include "common/util.h"
|
|
|
|
#if PLATFORM_LINUX
|
|
#include <ftw.h> // nftw
|
|
#include <stdlib.h> // putenv
|
|
#include <unistd.h> // readlink
|
|
#include <utime.h> // struct utimbuf
|
|
#include <wordexp.h>
|
|
#define __stat64 stat64
|
|
#define _chmod chmod
|
|
#endif
|
|
|
|
#if PLATFORM_WINDOWS
|
|
#include <corecrt_io.h> // chmod
|
|
#include <direct.h> // _rmdir
|
|
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#include <windows.h>
|
|
#undef ReplaceFile
|
|
|
|
#include <shlobj_core.h> // SHGetKnownFolderPath
|
|
#endif
|
|
|
|
namespace cdc_ft {
|
|
namespace {
|
|
|
|
constexpr char kPathSeparators[] = "\\/";
|
|
|
|
#if PLATFORM_WINDOWS
|
|
// Jan 1, 1970 (begin of Unix epoch) in 100 ns ticks since Jan 1, 1601 (begin
|
|
// of Windows epoch).
|
|
constexpr int64_t kUnixToWindowsEpochOffset = 0x019DB1DED53E8000;
|
|
|
|
// A Windows tick is 100 ns.
|
|
constexpr int64_t kWinTicksPerSecond = 10000000;
|
|
#endif
|
|
|
|
// Checks if the given path needs a path separator appended before the next
|
|
// component can be added.
|
|
bool NeedsPathSep(const std::string* path) {
|
|
if (path->empty()) return false;
|
|
return path->back() != path::PathSeparator() &&
|
|
path->back() != path::OtherPathSeparator();
|
|
}
|
|
|
|
void JoinImpl(std::string* dest, absl::string_view path,
|
|
absl::string_view to_append, char separator) {
|
|
assert(dest != nullptr);
|
|
dest->clear();
|
|
dest->reserve(to_append.length() + path.length() + 1);
|
|
if (!path.empty()) absl::StrAppend(dest, path);
|
|
if (!to_append.empty()) {
|
|
if (NeedsPathSep(dest)) dest->push_back(separator);
|
|
absl::StrAppend(dest, to_append);
|
|
}
|
|
}
|
|
|
|
void AppendImpl(std::string* dest, absl::string_view to_append,
|
|
char separator) {
|
|
if (to_append.empty()) return;
|
|
assert(dest != nullptr);
|
|
dest->reserve(dest->length() + to_append.length() + 1);
|
|
if (NeedsPathSep(dest)) dest->push_back(separator);
|
|
absl::StrAppend(dest, to_append);
|
|
}
|
|
|
|
#if PLATFORM_WINDOWS
|
|
FILETIME UnixTimeToWindowsFileTime(time_t unix_timestamp) {
|
|
int64_t winTicks = static_cast<int64_t>(unix_timestamp) * kWinTicksPerSecond +
|
|
kUnixToWindowsEpochOffset;
|
|
if (winTicks < 0) {
|
|
// FILETIME does not support dates before 1601. Ah well, we'll get over it.
|
|
winTicks = 0;
|
|
}
|
|
|
|
FILETIME ft;
|
|
ft.dwLowDateTime = winTicks & 0xffffffff;
|
|
ft.dwHighDateTime = winTicks >> 32;
|
|
return ft;
|
|
}
|
|
#endif
|
|
|
|
} // namespace
|
|
|
|
namespace path {
|
|
namespace {
|
|
|
|
// absl::StrSplit delimiter that handles both "\r\n" and "\n" newlines.
|
|
class ByNewline {
|
|
public:
|
|
absl::string_view Find(absl::string_view text, size_t pos) const {
|
|
size_t found_pos = text.find("\n", pos);
|
|
if (found_pos == absl::string_view::npos) {
|
|
return absl::string_view(text.data() + text.size(), 0);
|
|
}
|
|
if (found_pos > 0 && text[found_pos - 1] == '\r') {
|
|
return text.substr(found_pos - 1, 2);
|
|
}
|
|
return text.substr(found_pos, 1);
|
|
}
|
|
};
|
|
|
|
#if PLATFORM_WINDOWS
|
|
uint64_t ToUint64(uint32_t high, uint32_t low) {
|
|
return (((uint64_t)high) << 32) | low;
|
|
}
|
|
|
|
// Converts Windows ticks to Unix epoch. Adapted from
|
|
// https://developers.google.com/protocol-buffers/docs/reference/csharp/class/google/protobuf/well-known-types/timestamp
|
|
int64_t FileTimeToTimeT(FILETIME ft) {
|
|
uint64_t ticks = ToUint64(ft.dwHighDateTime, ft.dwLowDateTime);
|
|
// Divide by kWinTicksPerSecond first to avoid overlfow when
|
|
// ticks < kUnixToWindowsEpochOffset.
|
|
return static_cast<int64_t>(ticks / kWinTicksPerSecond) -
|
|
static_cast<int64_t>(kUnixToWindowsEpochOffset / kWinTicksPerSecond);
|
|
}
|
|
|
|
KNOWNFOLDERID ToWinFolderId(FolderId folder_id) {
|
|
switch (folder_id) {
|
|
case FolderId::kRoamingAppData:
|
|
return FOLDERID_RoamingAppData;
|
|
case FolderId::kProgramFiles:
|
|
return FOLDERID_ProgramFiles;
|
|
}
|
|
return {0};
|
|
}
|
|
|
|
#endif
|
|
|
|
} // namespace
|
|
|
|
absl::Status GetExeDir(std::string* dir) {
|
|
dir->clear();
|
|
#if PLATFORM_WINDOWS
|
|
wchar_t wide_exe_path[MAX_PATH];
|
|
if (FAILED(GetModuleFileName(nullptr, wide_exe_path, MAX_PATH))) {
|
|
return MakeStatus("Failed to get module file name");
|
|
}
|
|
std::string exe_path = Util::WideToUtf8Str(wide_exe_path);
|
|
#elif PLATFORM_LINUX
|
|
char exe_path[PATH_MAX];
|
|
ssize_t count = readlink("/proc/self/exe", exe_path, PATH_MAX);
|
|
if (count == -1) {
|
|
return MakeStatus("readlink() failed");
|
|
}
|
|
#endif
|
|
|
|
*dir = DirName(exe_path);
|
|
return absl::OkStatus();
|
|
}
|
|
|
|
std::string GetTempDir() {
|
|
return std::filesystem::temp_directory_path().u8string();
|
|
}
|
|
|
|
#if PLATFORM_WINDOWS
|
|
absl::Status GetKnownFolderPath(FolderId folder_id, std::string* path) {
|
|
path->clear();
|
|
wchar_t* wchar_path = nullptr;
|
|
KNOWNFOLDERID win_id = ToWinFolderId(folder_id);
|
|
if (!SUCCEEDED(SHGetKnownFolderPath(win_id, 0, NULL, &wchar_path))) {
|
|
return MakeStatus("Failed to get known folder path");
|
|
}
|
|
*path = Util::WideToUtf8Str(wchar_path);
|
|
CoTaskMemFree(wchar_path);
|
|
return absl::OkStatus();
|
|
}
|
|
#endif
|
|
|
|
absl::Status ExpandPathVariables(std::string* path) {
|
|
#if PLATFORM_WINDOWS
|
|
std::wstring wchar_path = Util::Utf8ToWideStr(*path);
|
|
|
|
DWORD size = ::ExpandEnvironmentStrings(wchar_path.c_str(), nullptr, 0);
|
|
if (size == 0) {
|
|
return MakeStatus("Failed to get length of expanded path");
|
|
}
|
|
|
|
std::wstring wchar_expanded(size, 0);
|
|
wchar_t* out_ptr = const_cast<wchar_t*>(wchar_expanded.c_str());
|
|
if (ExpandEnvironmentStrings(wchar_path.c_str(), out_ptr, size) != size) {
|
|
return MakeStatus("Failed to expand path");
|
|
}
|
|
|
|
// wchar_expanded has two null terminators at this point. Pop one.
|
|
wchar_expanded.pop_back();
|
|
*path = Util::WideToUtf8Str(wchar_expanded);
|
|
return absl::OkStatus();
|
|
#else
|
|
wordexp_t res;
|
|
wordexp(path->c_str(), &res, 0);
|
|
if (res.we_wordc > 1) {
|
|
return absl::InvalidArgumentError(
|
|
"Path expands to multiple results (did you use * etc. ?");
|
|
}
|
|
*path = res.we_wordv[0];
|
|
wordfree(&res);
|
|
return absl::OkStatus();
|
|
#endif
|
|
}
|
|
|
|
absl::Status GetEnv(const std::string& name, std::string* value) {
|
|
value->clear();
|
|
#if PLATFORM_WINDOWS
|
|
// The first call returns the size INCLUDING the null terminator.
|
|
std::wstring wchar_name = Util::Utf8ToWideStr(name);
|
|
DWORD size = ::GetEnvironmentVariable(wchar_name.c_str(), NULL, 0);
|
|
if (size == 0) {
|
|
return absl::NotFoundError(
|
|
absl::StrFormat("Environment variable '%s' not found", name.c_str()));
|
|
}
|
|
|
|
// The first call returns the bytes written EXCLUDING the null terminator,
|
|
// hence the '+ 1'.
|
|
std::wstring wstr_value(size, 0);
|
|
wchar_t* val_ptr = const_cast<wchar_t*>(wstr_value.c_str());
|
|
if (::GetEnvironmentVariable(wchar_name.c_str(), val_ptr, size) + 1 != size) {
|
|
return MakeStatus("Failed to get environment variable '%s'", name);
|
|
}
|
|
|
|
// Pop null terminator.
|
|
wstr_value.pop_back();
|
|
|
|
*value = Util::WideToUtf8Str(wstr_value);
|
|
return absl::OkStatus();
|
|
#elif PLATFORM_LINUX
|
|
char* value_cstr = std::getenv(name.c_str());
|
|
if (!value_cstr) {
|
|
return absl::NotFoundError(
|
|
absl::StrFormat("Environment variable '%s' not found", name.c_str()));
|
|
}
|
|
|
|
*value = value_cstr;
|
|
return absl::OkStatus();
|
|
#endif
|
|
}
|
|
|
|
absl::Status SetEnv(const std::string& name, const std::string& value) {
|
|
#if PLATFORM_WINDOWS
|
|
std::wstring wchar_name = Util::Utf8ToWideStr(name);
|
|
std::wstring wchar_value = Util::Utf8ToWideStr(value);
|
|
if (!SetEnvironmentVariable(wchar_name.c_str(), wchar_value.c_str())) {
|
|
return MakeStatus("Failed to set environment variable '%s'", name);
|
|
}
|
|
return absl::OkStatus();
|
|
#elif PLATFORM_LINUX
|
|
if (setenv(name.c_str(), value.c_str(), /*overwrite=*/1) != 0) {
|
|
return MakeStatus("Failed to set environment variable '%s'", name);
|
|
}
|
|
return absl::OkStatus();
|
|
#endif
|
|
}
|
|
|
|
#if PLATFORM_WINDOWS
|
|
std::string GetDrivePrefix(const std::string& path) {
|
|
if (path.empty()) {
|
|
return std::string();
|
|
}
|
|
|
|
if (path[0] != '\\') {
|
|
size_t pos = path.find(":");
|
|
if (pos != 1) {
|
|
// E.g. "\path\to\file", "path\to\file" or "user@host:file".
|
|
return std::string();
|
|
}
|
|
|
|
// E.g. "C:" or "C:\path\to\file".
|
|
return path.substr(0, pos + 1);
|
|
}
|
|
|
|
// Check if it is a network share like \\computername\sharename\path\to\file.
|
|
if (path.size() == 1 || path[1] != '\\') {
|
|
return std::string();
|
|
}
|
|
|
|
// 3 since the computername should be non-empty.
|
|
size_t third_backslash_pos = path.find('\\', 2);
|
|
if (third_backslash_pos == std::string::npos || third_backslash_pos < 3 ||
|
|
path.size() == third_backslash_pos + 1) {
|
|
// E.g. "\\c" or "\\\" or "\\c\".
|
|
return std::string();
|
|
}
|
|
|
|
size_t fourth_backslash_pos = path.find('\\', third_backslash_pos + 1);
|
|
if (fourth_backslash_pos < third_backslash_pos + 2) {
|
|
// E.g. "\\c\".
|
|
return std::string();
|
|
}
|
|
|
|
// E.g. "\\c\s" or "\\c\s\path\to\file"
|
|
return path.substr(0, fourth_backslash_pos);
|
|
}
|
|
#endif // if PLATFORM_WINDOWS
|
|
|
|
std::string GetCwd() { return std::filesystem::current_path().u8string(); }
|
|
|
|
void SetCwd(const std::string& path) {
|
|
std::filesystem::current_path(std::filesystem::u8path(path));
|
|
}
|
|
|
|
std::string GetFullPath(const std::string& path) {
|
|
if (path.empty()) {
|
|
return std::string();
|
|
}
|
|
std::filesystem::path fspath = std::filesystem::u8path(path);
|
|
// Expand relative paths with the current working directory to match the
|
|
// behavior of the Windows GetFullPathName*() API.
|
|
if (fspath.is_relative()) {
|
|
auto cwd = std::filesystem::current_path();
|
|
// Special case on Windows: paths like "C:" or "C:foo", which are
|
|
// interpreted relative to the drive's cwd. If the drive letter is different
|
|
// from the current drive, then the directory is treated as begin absolute.
|
|
if (fspath.has_root_name()) {
|
|
if (fspath.root_name() != cwd.root_name()) {
|
|
// This matches the behavior of std::filesystem::absolute() for a root
|
|
// other than the cwd.
|
|
fspath = std::filesystem::absolute(fspath.root_name()) / fspath;
|
|
}
|
|
}
|
|
fspath = cwd / fspath;
|
|
}
|
|
std::error_code ec;
|
|
std::filesystem::path cfspath = std::filesystem::weakly_canonical(fspath, ec);
|
|
if (ec) {
|
|
// Fall back to creating a normalized absolute path.
|
|
cfspath = std::filesystem::absolute(fspath, ec).lexically_normal();
|
|
if (ec) {
|
|
LOG_WARNING("Failed to canonicalize path '%s': %s", path, ec.message());
|
|
return fspath.u8string();
|
|
}
|
|
}
|
|
return cfspath.u8string();
|
|
}
|
|
|
|
bool IsAbsolute(const std::string& path) {
|
|
return std::filesystem::u8path(path).is_absolute();
|
|
}
|
|
|
|
std::string DirName(const std::string& path) {
|
|
if (path.empty()) return std::string();
|
|
#if PLATFORM_WINDOWS
|
|
// Keep the drive prefix.
|
|
const std::string drive = GetDrivePrefix(path);
|
|
std::string dirpart = path.substr(drive.size());
|
|
#else
|
|
const std::string drive;
|
|
const std::string& dirpart = path;
|
|
#endif
|
|
size_t non_sep_pos = dirpart.find_last_not_of(kPathSeparators);
|
|
size_t sep_pos = dirpart.find_last_of(kPathSeparators, non_sep_pos);
|
|
if (sep_pos == std::string::npos) {
|
|
// Handle the cases "foo", "C:", "\\computer\share".
|
|
return drive;
|
|
}
|
|
// Handle the cases "/", "\\\\", "C:\\".
|
|
if (non_sep_pos == std::string::npos) return path;
|
|
// Handle the cases "/foo", "///foo", "C:\\foo", or "\\\\foo".
|
|
size_t dir_end_pos = dirpart.find_last_not_of(kPathSeparators, sep_pos);
|
|
if (dir_end_pos == std::string::npos)
|
|
return path.substr(0, drive.size() + sep_pos + 1);
|
|
// Handle the cases "/foo/bar", "foo/bar", "C:\\foo", "\\\\foo\\bar".
|
|
return path.substr(0, drive.size() + dir_end_pos + 1);
|
|
}
|
|
|
|
std::string BaseName(const std::string& path) {
|
|
if (path.empty()) return std::string();
|
|
size_t non_sep_pos = path.find_last_not_of(kPathSeparators);
|
|
size_t sep_pos = path.find_last_of(kPathSeparators, non_sep_pos);
|
|
if (non_sep_pos == std::string::npos) non_sep_pos = path.size() - 1;
|
|
if (sep_pos != std::string::npos)
|
|
return path.substr(sep_pos + 1, non_sep_pos - sep_pos);
|
|
return path.substr(0, non_sep_pos + 1);
|
|
}
|
|
|
|
std::string Join(absl::string_view path, absl::string_view to_append) {
|
|
std::string dest;
|
|
Join(&dest, path, to_append);
|
|
return dest;
|
|
}
|
|
|
|
std::string Join(absl::string_view path, absl::string_view to_append1,
|
|
absl::string_view to_append2) {
|
|
return Join(Join(path, to_append1), to_append2);
|
|
}
|
|
|
|
std::string Join(absl::string_view path, absl::string_view to_append1,
|
|
absl::string_view to_append2, absl::string_view to_append3) {
|
|
return Join(Join(path, to_append1, to_append2), to_append3);
|
|
}
|
|
|
|
void Join(std::string* dest, absl::string_view path,
|
|
absl::string_view to_append) {
|
|
JoinImpl(dest, path, to_append, path::PathSeparator());
|
|
}
|
|
|
|
std::string JoinUnix(absl::string_view path, absl::string_view to_append) {
|
|
std::string dest;
|
|
JoinImpl(&dest, path, to_append, '/');
|
|
return dest;
|
|
}
|
|
|
|
void Append(std::string* dest, absl::string_view to_append) {
|
|
AppendImpl(dest, to_append, path::PathSeparator());
|
|
}
|
|
|
|
void AppendUnix(std::string* dest, absl::string_view to_append) {
|
|
AppendImpl(dest, to_append, '/');
|
|
}
|
|
|
|
bool EndsWithPathSeparator(const std::string& path) {
|
|
return !path.empty() && (path.back() == PathSeparator() ||
|
|
path.back() == OtherPathSeparator());
|
|
}
|
|
|
|
void EnsureEndsWithPathSeparator(std::string* path) {
|
|
if (!EndsWithPathSeparator(*path)) {
|
|
path->push_back(PathSeparator());
|
|
}
|
|
}
|
|
|
|
void EnsureDoesNotEndWithPathSeparator(std::string* path) {
|
|
while (EndsWithPathSeparator(*path)) {
|
|
path->pop_back();
|
|
}
|
|
}
|
|
|
|
void FixPathSeparators(std::string* path) {
|
|
std::replace(path->begin(), path->end(), OtherPathSeparator(),
|
|
PathSeparator());
|
|
}
|
|
|
|
std::string ToUnix(std::string path) {
|
|
std::replace(path.begin(), path.end(), '\\', '/');
|
|
return path;
|
|
}
|
|
|
|
std::string ToNative(std::string path) {
|
|
std::replace(path.begin(), path.end(), OtherPathSeparator(), PathSeparator());
|
|
return path;
|
|
}
|
|
|
|
absl::StatusOr<FILE*> OpenFile(const std::string& path, const char* mode) {
|
|
FILE* file;
|
|
#if PLATFORM_WINDOWS
|
|
std::wstring wchar_path = Util::Utf8ToWideStr(path);
|
|
std::wstring wchar_mode = Util::Utf8ToWideStr(mode);
|
|
|
|
file = _wfsopen(wchar_path.c_str(), wchar_mode.c_str(), _SH_DENYNO);
|
|
#else
|
|
// In Linux, _SH_DENYNO is set by default.
|
|
// Locking might be realized by lockf or fcntl.
|
|
file = fopen(path.c_str(), mode);
|
|
#endif
|
|
if (!file) {
|
|
int err = errno;
|
|
return ErrnoToCanonicalStatus(err, "Failed to open file '%s'", path);
|
|
}
|
|
return file;
|
|
}
|
|
|
|
#if PLATFORM_WINDOWS
|
|
|
|
void LogSearchError(const std::wstring& path) {
|
|
uint32_t last_error = GetLastError();
|
|
if (last_error == ERROR_FILE_NOT_FOUND || last_error == ERROR_NO_MORE_FILES) {
|
|
// This is fine, could be an empty directory or pattern didn't find any
|
|
// files in this directory.
|
|
return;
|
|
}
|
|
|
|
std::string utf8_path = Util::WideToUtf8Str(path);
|
|
|
|
if (last_error == ERROR_ACCESS_DENIED) {
|
|
// Access denied is somewhat expected, so just warn and return true.
|
|
LOG_WARNING("Failed to search '%s': Access denied", utf8_path.c_str());
|
|
return;
|
|
}
|
|
|
|
LOG_ERROR("Failed to search '%s': %s", utf8_path.c_str(),
|
|
Util::GetLastWin32Error().c_str());
|
|
return;
|
|
}
|
|
|
|
bool IsRegularDir(const WIN32_FIND_DATA& data) {
|
|
// Not a directory? Note that FindExSearchLimitToDirectories is just a hint.
|
|
if (!(data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) {
|
|
return false;
|
|
}
|
|
|
|
// Ignore "." and "..".
|
|
if (wcscmp(data.cFileName, L".") == 0 || wcscmp(data.cFileName, L"..") == 0) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
absl::Status SearchFilesWin(const std::wstring& dir,
|
|
const std::wstring& pattern, bool recursive,
|
|
SearchHandler handler, bool reset_pattern) {
|
|
if (!recursive && pattern.empty()) {
|
|
return absl::OkStatus();
|
|
}
|
|
WIN32_FIND_DATA data;
|
|
HANDLE hFind;
|
|
// For non-trivial patterns (!= "*"), we need a second pass to recurse into
|
|
// subdirectories. For instance "*.txt" wouldn't find "subdir\text.txt" since
|
|
// "subdir" doesn't match "*.txt".
|
|
// The condition with pattern.find('*') guarantees that the same directory
|
|
// is traversed only once, which is necessary for example for the first-level
|
|
// directory: for sync \source\dir_to_copy /destination: dir = source, pattern
|
|
// = dir_co_copy.
|
|
bool need_separate_dir_pass =
|
|
pattern != L"*" && pattern.find('*') != std::wstring::npos;
|
|
//
|
|
// Find all files. Also recurse into subdirs if !need_separate_dir_pass.
|
|
//
|
|
std::wstring path = dir + pattern;
|
|
hFind = FindFirstFileEx(path.c_str(), FindExInfoBasic, &data,
|
|
FindExSearchNameMatch, nullptr,
|
|
FIND_FIRST_EX_LARGE_FETCH);
|
|
|
|
if (hFind == INVALID_HANDLE_VALUE) {
|
|
LogSearchError(path);
|
|
} else {
|
|
do {
|
|
if (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
|
|
if (IsRegularDir(data)) {
|
|
// Send information about the directory to the server, then
|
|
// continue on processing its files recursively.
|
|
std::string utf8_filename = Util::WideToUtf8Str(data.cFileName);
|
|
std::string utf8_dir = Util::WideToUtf8Str(dir);
|
|
int64_t modified_time = FileTimeToTimeT(data.ftLastWriteTime);
|
|
absl::Status status =
|
|
handler(utf8_dir, utf8_filename, modified_time, 0, true);
|
|
if (!status.ok()) {
|
|
return status;
|
|
}
|
|
if (recursive) {
|
|
// Recurse into subdirectories.
|
|
std::wstring subdir = dir + data.cFileName + L"\\";
|
|
std::wstring new_pattern = pattern;
|
|
if (reset_pattern) {
|
|
new_pattern = L"*";
|
|
}
|
|
status =
|
|
SearchFilesWin(subdir, new_pattern, recursive, handler, false);
|
|
if (!status.ok()) {
|
|
return status;
|
|
}
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
// Send filename, write time and file size to the handler.
|
|
std::string utf8_dir = Util::WideToUtf8Str(dir);
|
|
std::string utf8_filename = Util::WideToUtf8Str(data.cFileName);
|
|
int64_t modified_time = FileTimeToTimeT(data.ftLastWriteTime);
|
|
uint64_t size = ToUint64(data.nFileSizeHigh, data.nFileSizeLow);
|
|
absl::Status status =
|
|
handler(utf8_dir, utf8_filename, modified_time, size, false);
|
|
if (!status.ok()) {
|
|
FindClose(hFind);
|
|
return WrapStatus(status, "Failed to search '%s': Handler failed",
|
|
utf8_dir + utf8_filename);
|
|
}
|
|
} while (FindNextFile(hFind, &data) != 0);
|
|
LogSearchError(dir);
|
|
FindClose(hFind);
|
|
}
|
|
|
|
//
|
|
// Do separate directory pass to recurse into subdirectories.
|
|
//
|
|
|
|
if (recursive && need_separate_dir_pass) {
|
|
hFind = FindFirstFileEx((dir + L"*").c_str(), FindExInfoBasic, &data,
|
|
FindExSearchLimitToDirectories, nullptr,
|
|
FIND_FIRST_EX_LARGE_FETCH);
|
|
if (hFind == INVALID_HANDLE_VALUE) {
|
|
LogSearchError(dir);
|
|
} else {
|
|
do {
|
|
if (IsRegularDir(data)) {
|
|
std::wstring subdir = dir + data.cFileName + L"\\";
|
|
std::wstring new_pattern = pattern;
|
|
if (reset_pattern) {
|
|
new_pattern = L"*";
|
|
}
|
|
absl::Status status =
|
|
SearchFilesWin(subdir, new_pattern, recursive, handler, false);
|
|
if (!status.ok()) {
|
|
return status;
|
|
}
|
|
}
|
|
} while (FindNextFile(hFind, &data) != 0);
|
|
|
|
LogSearchError(dir);
|
|
FindClose(hFind);
|
|
}
|
|
}
|
|
return absl::OkStatus();
|
|
}
|
|
|
|
#elif PLATFORM_LINUX
|
|
|
|
namespace {
|
|
|
|
// nftw() does not allow currying its handler function. Capturing lambdas don't
|
|
// cast to a plain function pointer. This is an (ugly) workaround. See e.g.
|
|
// https://stackoverflow.com/questions/7852101/c-lambda-with-captures-as-a-function-pointer
|
|
thread_local struct GlobalFtwData {
|
|
SearchHandler handler;
|
|
bool recursive = false;
|
|
absl::Status status;
|
|
} global_ftw_data;
|
|
|
|
int ftw_handler(const char* filepath, const struct stat* info, int typeflag,
|
|
struct FTW* pathinfo) {
|
|
// Send files to the handler.
|
|
if (typeflag == FTW_F) {
|
|
std::string dir(filepath, pathinfo->base);
|
|
std::string filename = filepath + pathinfo->base;
|
|
int64_t modified_time = info->st_mtime;
|
|
uint64_t size = info->st_size;
|
|
|
|
// Make nftw stop iterating the file tree if the handler fails.
|
|
global_ftw_data.status =
|
|
global_ftw_data.handler(dir, filename, modified_time, size, false);
|
|
if (!global_ftw_data.status.ok()) {
|
|
return FTW_STOP;
|
|
}
|
|
}
|
|
if (typeflag == FTW_D && pathinfo->level > 0) {
|
|
std::string dir(filepath, pathinfo->base);
|
|
std::string dirname = filepath + pathinfo->base;
|
|
if (dirname != "." && dirname != "..") {
|
|
int64_t modified_time = info->st_mtime;
|
|
|
|
// Make nftw stop iterating the tree if the handler fails.
|
|
global_ftw_data.status =
|
|
global_ftw_data.handler(dir, dirname, modified_time, 0, true);
|
|
if (!global_ftw_data.status.ok()) {
|
|
return FTW_STOP;
|
|
}
|
|
if (!global_ftw_data.recursive) {
|
|
return FTW_SKIP_SUBTREE;
|
|
}
|
|
}
|
|
}
|
|
return FTW_CONTINUE;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
absl::Status SearchFilesLinux(const std::string& dir, bool recursive,
|
|
SearchHandler handler) {
|
|
// FTW_PHYS means "do not follow symlinks", i.e. symlinks will be overwritten
|
|
// by client files if they have the same name. 32 is the max directory depth
|
|
// until things get slower.
|
|
global_ftw_data = {std::move(handler), recursive};
|
|
int result = nftw(dir.c_str(), ftw_handler, 32, FTW_PHYS | FTW_ACTIONRETVAL);
|
|
absl::Status status = global_ftw_data.status;
|
|
global_ftw_data = GlobalFtwData();
|
|
|
|
// ENOENT means the directory does not exist. This is fine.
|
|
if (result < 0 && errno != ENOENT) {
|
|
return MakeStatus("Failed to search '%s': nftw() failed: %s.", dir,
|
|
strerror(errno));
|
|
}
|
|
if (result > 0) {
|
|
// Should only happen if ftw_handler returns FTW_STOP.
|
|
assert(!status.ok());
|
|
assert(result == FTW_STOP);
|
|
return WrapStatus(status, "Failed to search '%s': Search handler failed",
|
|
dir);
|
|
}
|
|
|
|
return absl::OkStatus();
|
|
}
|
|
#endif
|
|
|
|
absl::Status SearchFiles(const std::string& pattern, bool recursive,
|
|
SearchHandler handler) {
|
|
#if PLATFORM_WINDOWS
|
|
std::wstring wchar_pattern = Util::Utf8ToWideStr(pattern);
|
|
|
|
// If |pattern| is a directory, search that directory. Otherwise, assume it's
|
|
// a file or some file pattern.
|
|
std::wstring dir = wchar_pattern;
|
|
std::wstring filename_pattern;
|
|
DWORD ftyp = GetFileAttributes(dir.c_str());
|
|
bool reset_pattern = false;
|
|
|
|
if (ftyp != INVALID_FILE_ATTRIBUTES && (ftyp & FILE_ATTRIBUTE_DIRECTORY)) {
|
|
// It's a directory.
|
|
if (!recursive) {
|
|
return SearchFilesWin(dir, filename_pattern, recursive, handler,
|
|
reset_pattern);
|
|
}
|
|
if (dir.back() == L'\\') {
|
|
filename_pattern = L"*";
|
|
} else {
|
|
// On Linux, source directory without trailing \\ is traversed.
|
|
// To enable the same behavior on windows: pattern=dir_to_copy
|
|
// and it will be reset after the first-level traversal.
|
|
// Otherwise, server_dirs and client_dirs would differ.
|
|
reset_pattern = true;
|
|
while (!dir.empty() && dir.back() != L'\\') {
|
|
dir.pop_back();
|
|
}
|
|
filename_pattern = wchar_pattern.substr(dir.size());
|
|
}
|
|
} else {
|
|
// Assume it's a file or file pattern.
|
|
while (!dir.empty() && dir.back() != L'\\') {
|
|
dir.pop_back();
|
|
}
|
|
filename_pattern = wchar_pattern.substr(dir.size());
|
|
}
|
|
return SearchFilesWin(dir, filename_pattern, recursive, handler,
|
|
reset_pattern);
|
|
#else
|
|
return SearchFilesLinux(pattern, recursive, handler);
|
|
#endif
|
|
}
|
|
|
|
absl::Status StreamReadFileContents(FILE* file, size_t buffer_size,
|
|
StreamReadFileHandler handler) {
|
|
Buffer buffer(buffer_size);
|
|
return StreamReadFileContents(file, &buffer, handler);
|
|
}
|
|
|
|
absl::Status StreamReadFileContents(FILE* file, Buffer* buffer,
|
|
StreamReadFileHandler handler) {
|
|
if (!buffer->size()) {
|
|
return absl::FailedPreconditionError("Given buffer is empty");
|
|
}
|
|
|
|
for (;;) {
|
|
const size_t num_read =
|
|
fread_nolock(buffer->data(), 1, buffer->size(), file);
|
|
|
|
// Note: The handler might read from file, so check feof_nolock now.
|
|
bool eof = num_read != buffer->size() && feof_nolock(file);
|
|
if (num_read > 0) {
|
|
absl::Status status = handler(buffer->data(), num_read);
|
|
if (!status.ok()) {
|
|
return WrapStatus(status, "Handler failed");
|
|
}
|
|
}
|
|
if (num_read != buffer->size()) {
|
|
if (eof) {
|
|
// Indicate eof.
|
|
absl::Status status = handler(nullptr, 0);
|
|
if (!status.ok()) {
|
|
return WrapStatus(status, "Handler failed");
|
|
}
|
|
return absl::OkStatus();
|
|
}
|
|
|
|
return MakeStatus("fread_nolock() failed");
|
|
}
|
|
}
|
|
}
|
|
|
|
absl::Status StreamWriteFileContents(FILE* file,
|
|
StreamWriteFileHandler handler) {
|
|
for (;;) {
|
|
const void* data;
|
|
size_t data_size;
|
|
absl::Status status = handler(&data, &data_size);
|
|
if (!status.ok()) {
|
|
return WrapStatus(status, "Handler failed");
|
|
}
|
|
// By returning (nullptr, 0), the handler indicates EOF.
|
|
if (data_size == 0) {
|
|
assert(!data);
|
|
return absl::OkStatus();
|
|
}
|
|
|
|
const size_t num_written = fwrite_nolock(data, 1, data_size, file);
|
|
if (num_written != data_size) {
|
|
return MakeStatus("fwrite_nolock() failed");
|
|
}
|
|
}
|
|
}
|
|
|
|
absl::Status ReadFile(const std::string& path, Buffer* data) {
|
|
assert(data);
|
|
data->clear();
|
|
|
|
absl::StatusOr<FILE*> file = OpenFile(path, "rb");
|
|
if (!file.ok()) {
|
|
return file.status();
|
|
}
|
|
|
|
fseek64_nolock(*file, 0, SEEK_END);
|
|
int64_t size = ftell64(*file);
|
|
data->resize(size);
|
|
fseek64_nolock(*file, 0, SEEK_SET);
|
|
|
|
if (fread_nolock(data->data(), 1, data->size(), *file) != data->size()) {
|
|
data->clear();
|
|
return MakeStatus("Failed to read data of size %u from file '%s'",
|
|
data->size(), path);
|
|
}
|
|
|
|
fclose(*file);
|
|
return absl::OkStatus();
|
|
}
|
|
|
|
absl::StatusOr<size_t> ReadFile(const std::string& path, void* data,
|
|
size_t offset, size_t len) {
|
|
assert(data);
|
|
if (len == 0) {
|
|
return 0;
|
|
}
|
|
|
|
absl::StatusOr<FILE*> file = OpenFile(path, "rb");
|
|
if (!file.ok()) {
|
|
return file.status();
|
|
}
|
|
if (offset != 0) {
|
|
fseek64_nolock(*file, offset, SEEK_SET);
|
|
}
|
|
size_t read_len = fread_nolock(data, 1, len, *file);
|
|
|
|
if (read_len != len && !feof_nolock(*file)) {
|
|
fclose(*file);
|
|
return MakeStatus("fread_nolock failed: %s", Util::GetLastStrError());
|
|
}
|
|
fclose(*file);
|
|
return read_len;
|
|
}
|
|
|
|
absl::StatusOr<std::string> ReadFile(const std::string& path) {
|
|
absl::StatusOr<FILE*> file = OpenFile(path, "rb");
|
|
if (!file.ok()) {
|
|
return file.status();
|
|
}
|
|
|
|
fseek64_nolock(*file, 0, SEEK_END);
|
|
int64_t size = ftell64(*file);
|
|
std::string data;
|
|
data.resize(size);
|
|
fseek64_nolock(*file, 0, SEEK_SET);
|
|
|
|
if (fread_nolock(data.data(), 1, data.size(), *file) != data.size()) {
|
|
data.clear();
|
|
return MakeStatus("Failed to read data of size %u from file '%s'",
|
|
data.size(), path);
|
|
}
|
|
|
|
fclose(*file);
|
|
return data;
|
|
}
|
|
|
|
absl::Status ReadAllLines(const std::string& path,
|
|
std::vector<std::string>* lines, ReadFlags flags) {
|
|
Buffer data;
|
|
absl::Status status = ReadFile(path, &data);
|
|
if (!status.ok()) {
|
|
return status;
|
|
}
|
|
|
|
std::string all_lines =
|
|
std::string(reinterpret_cast<char*>(data.data()), data.size());
|
|
|
|
*lines = absl::StrSplit(all_lines, ByNewline());
|
|
|
|
if ((flags & ReadFlags::kTrimWhitespace) != ReadFlags::kNone) {
|
|
for (std::string& line : *lines) {
|
|
absl::StripAsciiWhitespace(&line);
|
|
}
|
|
}
|
|
|
|
if ((flags & ReadFlags::kRemoveEmpty) != ReadFlags::kNone) {
|
|
lines->erase(
|
|
std::remove_if(lines->begin(), lines->end(),
|
|
[](const std::string& line) { return line.empty(); }),
|
|
lines->end());
|
|
}
|
|
|
|
return absl::OkStatus();
|
|
}
|
|
|
|
absl::Status GetStats(const std::string& path, Stats* stats) {
|
|
struct __stat64 os_stats;
|
|
bool result;
|
|
#if PLATFORM_WINDOWS
|
|
result = _wstat64(Util::Utf8ToWideStr(path).c_str(), &os_stats) == 0;
|
|
#elif PLATFORM_LINUX
|
|
result = stat64(path.c_str(), &os_stats) == 0;
|
|
#endif
|
|
if (!result) {
|
|
int err = errno;
|
|
*stats = Stats();
|
|
return ErrnoToCanonicalStatus(err, "Failed to stat '%s'", path);
|
|
}
|
|
|
|
stats->mode = os_stats.st_mode;
|
|
stats->size = os_stats.st_size;
|
|
stats->modified_time = os_stats.st_mtime;
|
|
return absl::OkStatus();
|
|
}
|
|
|
|
absl::Status FileSize(const std::string& path, uint64_t* size) {
|
|
Stats stats;
|
|
absl::Status status = GetStats(path, &stats);
|
|
if (!status.ok()) return status;
|
|
*size = stats.size;
|
|
return absl::OkStatus();
|
|
}
|
|
|
|
absl::Status ChangeMode(const std::string& path, uint16_t mode) {
|
|
if (_chmod(path.c_str(), mode) != 0) {
|
|
return MakeStatus("chmod() failed: %s", Util::GetLastStrError());
|
|
}
|
|
return absl::OkStatus();
|
|
}
|
|
|
|
absl::Status WriteFile(const std::string& path, const Buffer& data) {
|
|
return WriteFile(path, data.data(), data.size());
|
|
}
|
|
|
|
absl::Status WriteFile(const std::string& path, const std::string& data) {
|
|
return WriteFile(path, data.data(), data.size());
|
|
}
|
|
|
|
absl::Status WriteFile(const std::string& path, const void* data, size_t len) {
|
|
absl::StatusOr<FILE*> file = OpenFile(path, "wb");
|
|
if (!file.ok()) {
|
|
return file.status();
|
|
}
|
|
|
|
if (fwrite_nolock(data, 1, len, *file) != len) {
|
|
return MakeStatus("Failed to write data of size %u to file '%s'", len,
|
|
path);
|
|
}
|
|
|
|
fclose(*file);
|
|
return absl::OkStatus();
|
|
}
|
|
|
|
absl::Status CreateSymlink(const std::string& target,
|
|
const std::string& link_path, bool is_dir) {
|
|
std::error_code error_code;
|
|
std::filesystem::path target_u8 = std::filesystem::u8path(target);
|
|
std::filesystem::path link_path_u8 = std::filesystem::u8path(link_path);
|
|
if (is_dir) {
|
|
std::filesystem::create_symlink(target_u8, link_path_u8, error_code);
|
|
} else {
|
|
std::filesystem::create_directory_symlink(target_u8, link_path_u8,
|
|
error_code);
|
|
}
|
|
return ErrorCodeToCanonicalStatus(
|
|
error_code, "Failed to create symlink '%s' with target '%s'", link_path,
|
|
target);
|
|
}
|
|
|
|
absl::StatusOr<std::string> GetSymlinkTarget(const std::string& link_path) {
|
|
std::error_code error_code;
|
|
std::filesystem::path link_path_u8 = std::filesystem::u8path(link_path);
|
|
std::filesystem::path symlink_target =
|
|
std::filesystem::read_symlink(link_path_u8, error_code);
|
|
if (error_code) {
|
|
return ErrorCodeToCanonicalStatus(error_code, "Failed to read symlink '%s'",
|
|
link_path);
|
|
}
|
|
return symlink_target.u8string();
|
|
}
|
|
|
|
bool DirExists(const std::string& path) {
|
|
Stats stats;
|
|
return GetStats(path, &stats).ok() && (stats.mode & MODE_IFMT) == MODE_IFDIR;
|
|
}
|
|
|
|
bool FileExists(const std::string& path) {
|
|
Stats stats;
|
|
return GetStats(path, &stats).ok() && (stats.mode & MODE_IFMT) == MODE_IFREG;
|
|
}
|
|
|
|
bool Exists(const std::string& path) {
|
|
Stats stats;
|
|
return GetStats(path, &stats).ok();
|
|
}
|
|
|
|
absl::Status CreateDir(const std::string& path) {
|
|
std::error_code error_code;
|
|
std::filesystem::create_directory(std::filesystem::u8path(path), error_code);
|
|
return ErrorCodeToCanonicalStatus(error_code,
|
|
"Failed to create directory '%s'", path);
|
|
}
|
|
|
|
absl::Status CreateDirRec(const std::string& path) {
|
|
std::error_code error_code;
|
|
std::filesystem::create_directories(std::filesystem::u8path(path),
|
|
error_code);
|
|
return ErrorCodeToCanonicalStatus(error_code,
|
|
"Failed to create directory '%s'", path);
|
|
}
|
|
|
|
absl::Status RenameFile(const std::string& from_path,
|
|
const std::string& to_path) {
|
|
if (rename(from_path.c_str(), to_path.c_str()) != 0) {
|
|
return MakeStatus("rename() failed: %s.", Util::GetLastStrError());
|
|
}
|
|
return absl::OkStatus();
|
|
}
|
|
|
|
absl::Status CopyFileRec(const std::string& from_path,
|
|
const std::string& to_path) {
|
|
std::error_code error;
|
|
std::filesystem::copy(std::filesystem::u8path(from_path),
|
|
std::filesystem::u8path(to_path),
|
|
std::filesystem::copy_options::recursive |
|
|
std::filesystem::copy_options::overwrite_existing,
|
|
error);
|
|
if (error) {
|
|
return MakeStatus("Failed to copy '%s' to '%s': '%s'", from_path, to_path,
|
|
error.message());
|
|
}
|
|
return absl::OkStatus();
|
|
}
|
|
|
|
absl::Status RemoveDirRec(const std::string& path) {
|
|
std::error_code error;
|
|
std::filesystem::remove_all(std::filesystem::u8path(path), error);
|
|
if (error) {
|
|
return MakeStatus("Failed to recursively remove directory %s: %s", path,
|
|
error.message());
|
|
}
|
|
return absl::OkStatus();
|
|
}
|
|
|
|
absl::Status RemoveFile(const std::string& path) {
|
|
#if PLATFORM_WINDOWS
|
|
// On Linux, remove() removes also empty directories.
|
|
// On Windows, special handling of empty-directory deletion is required.
|
|
if (DirExists(path)) {
|
|
if (_rmdir(path.c_str()) != 0 && errno != ENOENT) {
|
|
return MakeStatus("_rmdir() failed: %s.", Util::GetLastStrError());
|
|
}
|
|
return absl::OkStatus();
|
|
}
|
|
#endif
|
|
|
|
if (remove(path.c_str()) != 0 && errno != ENOENT) {
|
|
return MakeStatus("remove() failed: %s.", Util::GetLastStrError());
|
|
}
|
|
return absl::OkStatus();
|
|
}
|
|
|
|
absl::Status ReplaceFile(const std::string& path,
|
|
const std::string& replacement_path) {
|
|
absl::Status status;
|
|
|
|
// Delete old file.
|
|
status = RemoveFile(path);
|
|
if (!status.ok()) {
|
|
return WrapStatus(status, "RemoveFile() failed for '%s'", path);
|
|
}
|
|
|
|
// Rename new file to old file.
|
|
status = RenameFile(replacement_path, path);
|
|
if (!status.ok()) {
|
|
return WrapStatus(status, "Rename() failed from '%s' to '%s'",
|
|
replacement_path, path);
|
|
}
|
|
|
|
return absl::OkStatus();
|
|
}
|
|
|
|
absl::Status GetFileTime(const std::string& path, time_t* timestamp) {
|
|
#if PLATFORM_WINDOWS
|
|
std::wstring wchar_path = Util::Utf8ToWideStr(path);
|
|
HANDLE handle = CreateFileW(
|
|
wchar_path.c_str(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING,
|
|
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS, NULL);
|
|
if (handle == INVALID_HANDLE_VALUE) {
|
|
return MakeStatus("Failed to open '%s': %s", path,
|
|
Util::GetLastWin32Error());
|
|
}
|
|
|
|
FILETIME ft;
|
|
int res = GetFileTime(handle, (LPFILETIME)NULL, (LPFILETIME)NULL, &ft);
|
|
CloseHandle(handle);
|
|
if (res == 0) {
|
|
return MakeStatus("Failed to get file time for '%s': %s", path,
|
|
Util::GetLastWin32Error());
|
|
}
|
|
*timestamp = FileTimeToTimeT(ft);
|
|
#elif PLATFORM_LINUX
|
|
Stats st;
|
|
RETURN_IF_ERROR(GetStats(path, &st));
|
|
*timestamp = static_cast<time_t>(st.modified_time);
|
|
#endif
|
|
return absl::OkStatus();
|
|
}
|
|
|
|
absl::Status SetFileTime(const std::string& path, time_t timestamp) {
|
|
// Would be great to use std::filesystem::last_write_time here, but
|
|
// apparently there's no reliable conversion from time_t available in C++17,
|
|
// see
|
|
// https://developercommunity.visualstudio.com/t/stdfilesystemfile-time-type-does-not-allow-easy-co/251213
|
|
#if PLATFORM_WINDOWS
|
|
HANDLE handle =
|
|
CreateFileW(Util::Utf8ToWideStr(path).c_str(), FILE_WRITE_ATTRIBUTES,
|
|
FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING,
|
|
FILE_ATTRIBUTE_NORMAL, NULL);
|
|
if (handle == INVALID_HANDLE_VALUE) {
|
|
return MakeStatus("Failed to open '%s': %s", path,
|
|
Util::GetLastWin32Error());
|
|
}
|
|
|
|
FILETIME ft = UnixTimeToWindowsFileTime(timestamp);
|
|
int res = SetFileTime(handle, (LPFILETIME)NULL, &ft, &ft);
|
|
CloseHandle(handle);
|
|
if (res == 0) {
|
|
return MakeStatus("Failed to set file time for '%s': %s", path,
|
|
Util::GetLastWin32Error());
|
|
}
|
|
#elif PLATFORM_LINUX
|
|
// Set both actime and modtime to save a stat operation to get the actime.
|
|
struct utimbuf utb;
|
|
utb.actime = timestamp;
|
|
utb.modtime = timestamp;
|
|
if (utime(path.c_str(), &utb) != 0) {
|
|
return MakeStatus("Failed to set file time for '%s': %s", path,
|
|
Util::GetLastStrError());
|
|
}
|
|
#endif
|
|
|
|
return absl::OkStatus();
|
|
}
|
|
|
|
std::filesystem::path ToCanonical(const std::string& path) {
|
|
std::string str_path = GetFullPath(path);
|
|
#if PLATFORM_WINDOWS
|
|
// Normalize drive prefix if present.
|
|
std::string prefix = GetDrivePrefix(str_path);
|
|
for (auto& c : prefix) {
|
|
c = toupper(c);
|
|
}
|
|
str_path = prefix + str_path.substr(prefix.length());
|
|
#endif // if PLATFORM_WINDOWS
|
|
return std::filesystem::u8path(str_path);
|
|
}
|
|
|
|
bool AreEqual(std::string path1, std::string path2) {
|
|
return ToCanonical(path1).compare(ToCanonical(path2)) == 0;
|
|
}
|
|
} // namespace path
|
|
} // namespace cdc_ft
|