Releasing the former Stadia file transfer tools

The tools allow efficient and fast synchronization of large directory
trees from a Windows workstation to a Linux target machine.

cdc_rsync* support efficient copy of files by using content-defined
chunking (CDC) to identify chunks within files that can be reused.

asset_stream_manager + cdc_fuse_fs support efficient streaming of a
local directory to a remote virtual file system based on FUSE. It also
employs CDC to identify and reuse unchanged data chunks.
This commit is contained in:
Christian Schneider
2022-10-07 10:47:04 +02:00
commit 4326e972ac
364 changed files with 49410 additions and 0 deletions

3
cdc_rsync_cli/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
x64/*
*.log
*.user

44
cdc_rsync_cli/BUILD Normal file
View File

@@ -0,0 +1,44 @@
package(default_visibility = [
"//:__subpackages__",
])
cc_binary(
name = "cdc_rsync",
srcs = ["main.cc"],
deps = [
":params",
"//cdc_rsync",
],
)
cc_library(
name = "params",
srcs = ["params.cc"],
hdrs = ["params.h"],
deps = [
"//cdc_rsync",
"@com_github_zstd//:zstd",
"@com_google_absl//absl/status",
],
)
cc_test(
name = "params_test",
srcs = ["params_test.cc"],
data = ["testdata/root.txt"] + glob(["testdata/params/**"]),
deps = [
":params",
"//common:test_main",
"@com_google_googletest//:gtest",
],
)
filegroup(
name = "all_test_sources",
srcs = glob(["*_test.cc"]),
)
filegroup(
name = "all_test_data",
srcs = glob(["testdata/**"]),
)

View File

@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>15.0</VCProjectVersion>
<ProjectGuid>{3FAC852A-00A8-4CFB-9160-07EFF2B73562}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>cdc_rsync</RootNamespace>
<WindowsTargetPlatformVersion Condition="$(VisualStudioVersion) == 15">$([Microsoft.Build.Utilities.ToolLocationHelper]::GetLatestSDKTargetPlatformVersion('Windows', '10.0'))</WindowsTargetPlatformVersion>
<WindowsTargetPlatformVersion Condition="$(VisualStudioVersion) == 16">10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Makefile</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset Condition="$(VisualStudioVersion) == 15">v141</PlatformToolset>
<PlatformToolset Condition="$(VisualStudioVersion) == 16">v142</PlatformToolset>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Makefile</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset Condition="$(VisualStudioVersion) == 15">v141</PlatformToolset>
<PlatformToolset Condition="$(VisualStudioVersion) == 16">v142</PlatformToolset>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="Shared">
<Import Project="..\all_files.vcxitems" Label="Shared" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<OutDir>$(SolutionDir)bazel-out\x64_windows-dbg\bin\cdc_rsync_cli\</OutDir>
<AdditionalOptions>/std:c++17</AdditionalOptions>
<NMakePreprocessorDefinitions>UNICODE</NMakePreprocessorDefinitions>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<OutDir>$(SolutionDir)bazel-out\x64_windows-opt\bin\cdc_rsync_cli\</OutDir>
<NMakePreprocessorDefinitions>UNICODE</NMakePreprocessorDefinitions>
<AdditionalOptions>/std:c++17</AdditionalOptions>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\cdc_rsync_server\cdc_rsync_server.vcxproj">
<Project>{4ece65e0-d950-4b96-8ad5-0313261b8c8d}</Project>
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
<LinkLibraryDependencies>false</LinkLibraryDependencies>
</ProjectReference>
</ItemGroup>
<!-- Prevent console from being closed -->
<ItemDefinitionGroup>
<Link>
<SubSystem>Console</SubSystem>
</Link>
</ItemDefinitionGroup>
<!-- Bazel setup -->
<PropertyGroup>
<BazelTargets>//cdc_rsync_cli:cdc_rsync</BazelTargets>
<BazelOutputFile>cdc_rsync.exe</BazelOutputFile>
<BazelIncludePaths>..\;..\third_party\absl;..\third_party\blake3\c;..\bazel-stadia-file-transfer\external\com_github_zstd\lib;..\third_party\googletest\googletest\include;..\third_party\protobuf\src;$(VC_IncludePath);$(WindowsSDK_IncludePath)</BazelIncludePaths>
<BazelSourcePathPrefix>..\/</BazelSourcePathPrefix>
</PropertyGroup>
<Import Project="..\NMakeBazelProject.targets" />
<!-- For some reason, msbuild doesn't include this file, so copy it explicitly. -->
<!-- TODO: Reenable once we can cross-compile these.
<PropertyGroup>
<GgpRsyncServerFile>$(SolutionDir)bazel-out\k8-$(BazelCompilationMode)\bin\cdc_rsync_server\cdc_rsync_server</GgpRsyncServerFile>
</PropertyGroup>
<Target Name="CopyServer" Inputs="$(GgpRsyncServerFile)" Outputs="$(OutDir)cdc_rsync_server" AfterTargets="Build">
<Copy SourceFiles="$(GgpRsyncServerFile)" DestinationFiles="$(OutDir)cdc_rsync_server" />
</Target>
-->
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" />

72
cdc_rsync_cli/main.cc Normal file
View File

@@ -0,0 +1,72 @@
// 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.
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <string>
#include <vector>
#include "cdc_rsync/cdc_rsync.h"
#include "cdc_rsync_cli/params.h"
#include "common/util.h"
int wmain(int argc, wchar_t* argv[]) {
cdc_ft::params::Parameters parameters;
// Convert args from wide to UTF8 strings.
std::vector<std::string> utf8_str_args;
utf8_str_args.reserve(argc);
for (int i = 0; i < argc; i++) {
utf8_str_args.push_back(cdc_ft::Util::WideToUtf8Str(argv[i]));
}
// Convert args from UTF8 strings to UTF8 c-strings.
std::vector<const char*> utf8_args;
utf8_args.reserve(argc);
for (const auto& utf8_str_arg : utf8_str_args) {
utf8_args.push_back(utf8_str_arg.c_str());
}
if (!cdc_ft::params::Parse(argc, utf8_args.data(), &parameters)) {
return 1;
}
// Convert sources from string-vec to c-str-vec.
std::vector<const char*> sources_ptr;
sources_ptr.reserve(parameters.sources.size());
for (const std::string& source : parameters.sources) {
sources_ptr.push_back(source.c_str());
}
// Convert filter rules from string-structs to c-str-structs.
std::vector<cdc_ft::FilterRule> filter_rules;
filter_rules.reserve(parameters.filter_rules.size());
for (const cdc_ft::params::Parameters::FilterRule& rule :
parameters.filter_rules) {
filter_rules.emplace_back(rule.type, rule.pattern.c_str());
}
const char* error_message = nullptr;
cdc_ft::ReturnCode code = cdc_ft::Sync(
&parameters.options, filter_rules.data(), parameters.filter_rules.size(),
parameters.sources_dir.c_str(), sources_ptr.data(),
parameters.sources.size(), parameters.destination.c_str(),
&error_message);
if (error_message) {
fprintf(stderr, "Error: %s\n", error_message);
}
return static_cast<int>(code);
}

442
cdc_rsync_cli/params.cc Normal file
View File

@@ -0,0 +1,442 @@
// 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 "cdc_rsync_cli/params.h"
#include <cassert>
#include "absl/status/status.h"
#include "absl/strings/str_format.h"
#include "common/path.h"
#include "lib/zstd.h"
namespace cdc_ft {
namespace params {
namespace {
template <typename... Args>
void PrintError(const absl::FormatSpec<Args...>& format, Args... args) {
std::cerr << "Error: " << absl::StrFormat(format, args...) << std::endl;
}
enum class OptionResult { kConsumedKey, kConsumedKeyValue, kError };
const char kHelpText[] =
R"(Copy local files to a gamelet
Synchronizes local files and files on a gamelet. Matching files are skipped.
For partially matching files only the deltas are transferred.
Usage:
cdc_rsync [options] source [source]... destination
Parameters:
source Local file or folder to be copied
destination Destination folder on the gamelet
Options:
--ip string Gamelet IP. Required.
--port number SSH port to use. Required.
--contimeout sec Gamelet connection timeout in seconds (default: 10)
-q, --quiet Quiet mode, only print errors
-v, --verbose Increase output verbosity
--json Print JSON progress
-n, --dry-run Perform a trial run with no changes made
-r, --recursive Recurse into directories
--delete Delete extraneous files from destination folder
-z, --compress Compress file data during the transfer
--compress-level num Explicitly set compression level (default: 6)
-c, --checksum Skip files based on checksum, not mod-time & size
-W, --whole-file Always copy files whole,
do not apply delta-transfer algorithm
--exclude pattern Exclude files matching pattern
--exclude-from file Read exclude patterns from file
--include pattern Don't exclude files matching pattern
--include-from file Read include patterns from file
--files-from file Read list of source files from file
-R, --relative Use relative path names
--existing Skip creating new files on instance
--copy-dest dir Use files from dir as sync base if files are missing
from destination folder
-h --help Help for cdc_rsync
)";
// Handles the --exclude-from and --include-from options.
OptionResult HandleFilterRuleFile(const std::string& option_name,
const char* path, FilterRule::Type type,
Parameters* params) {
if (!path) {
PrintError("Option '%s' needs a value", option_name);
return OptionResult::kError;
}
std::vector<std::string> patterns;
absl::Status status = path::ReadAllLines(
path, &patterns,
path::ReadFlags::kRemoveEmpty | path::ReadFlags::kTrimWhitespace);
if (!status.ok()) {
PrintError("Failed to read file '%s' for %s option: %s", path, option_name,
status.message());
return OptionResult::kError;
}
for (std::string& pattern : patterns) {
params->filter_rules.emplace_back(type, std::move(pattern));
}
return OptionResult::kConsumedKeyValue;
}
// Loads sources for --files-from option. |sources| must contain at most one
// path and that path must be an existing directory. This directory is returned
// in |sources_dir|. The method then loads all sources line-by-line from
// |sources_file| and stores them into |sources|.
bool LoadFilesFrom(const std::string& files_from,
std::vector<std::string>* sources,
std::string* sources_dir) {
if (sources->size() > 1) {
PrintError(
"Expected at most 1 source for the --files-from option, but %u "
"provided",
sources->size());
return false;
}
if (sources->size() == 1 && !path::DirExists(sources->at(0))) {
PrintError(
"The source '%s' must be an existing directory for the --files-from "
"option",
sources->at(0));
return false;
}
*sources_dir = sources->empty() ? std::string() : sources->at(0);
if (!sources_dir->empty()) {
path::EnsureEndsWithPathSeparator(sources_dir);
}
sources->clear();
absl::Status status = path::ReadAllLines(
files_from, sources,
path::ReadFlags::kRemoveEmpty | path::ReadFlags::kTrimWhitespace);
if (!status.ok()) {
PrintError("Failed to read sources file '%s' for files-from option: %s",
files_from, status.message());
return false;
}
if (sources->empty()) {
PrintError("The file '%s' specified in the --files-from option is empty",
files_from);
}
return true;
}
OptionResult HandleParameter(const std::string& key, const char* value,
Parameters* params, bool* help) {
if (key == "ip") {
params->options.ip = value;
return OptionResult::kConsumedKeyValue;
}
if (key == "port") {
if (value) {
params->options.port = atoi(value);
}
return OptionResult::kConsumedKeyValue;
}
if (key == "delete") {
params->options.delete_ = true;
return OptionResult::kConsumedKey;
}
if (key == "r" || key == "recursive") {
params->options.recursive = true;
return OptionResult::kConsumedKey;
}
if (key == "v" || key == "verbosity") {
params->options.verbosity++;
return OptionResult::kConsumedKey;
}
if (key == "q" || key == "quiet") {
params->options.quiet = true;
return OptionResult::kConsumedKey;
}
if (key == "W" || key == "whole-file") {
params->options.whole_file = true;
return OptionResult::kConsumedKey;
}
if (key == "include") {
params->filter_rules.emplace_back(FilterRule::Type::kInclude, value);
return OptionResult::kConsumedKeyValue;
}
if (key == "include-from") {
return HandleFilterRuleFile(key, value, FilterRule::Type::kInclude, params);
}
if (key == "exclude") {
params->filter_rules.emplace_back(FilterRule::Type::kExclude, value);
return OptionResult::kConsumedKeyValue;
}
if (key == "exclude-from") {
return HandleFilterRuleFile(key, value, FilterRule::Type::kExclude, params);
}
if (key == "files-from") {
// Implies -R.
params->options.relative = true;
params->files_from = value;
return OptionResult::kConsumedKeyValue;
}
if (key == "R" || key == "relative") {
params->options.relative = true;
return OptionResult::kConsumedKey;
}
if (key == "z" || key == "compress") {
params->options.compress = true;
return OptionResult::kConsumedKey;
}
if (key == "compress-level") {
if (value) {
params->options.compress_level = atoi(value);
}
return OptionResult::kConsumedKeyValue;
}
if (key == "contimeout") {
if (value) {
params->options.connection_timeout_sec = atoi(value);
}
return OptionResult::kConsumedKeyValue;
}
if (key == "h" || key == "help") {
*help = true;
return OptionResult::kConsumedKey;
}
if (key == "c" || key == "checksum") {
params->options.checksum = true;
return OptionResult::kConsumedKey;
}
if (key == "n" || key == "dry-run") {
params->options.dry_run = true;
return OptionResult::kConsumedKey;
}
if (key == "existing") {
params->options.existing = true;
return OptionResult::kConsumedKey;
}
if (key == "copy-dest") {
params->options.copy_dest = value;
return OptionResult::kConsumedKeyValue;
}
if (key == "json") {
params->options.json = true;
return OptionResult::kConsumedKey;
}
PrintError("Unknown option: '%s'", key);
return OptionResult::kError;
}
bool CheckParameters(const Parameters& params, bool help) {
if (help) {
printf("%s", kHelpText);
return false;
}
if (params.options.delete_ && !params.options.recursive) {
PrintError("--delete does not work without --recursive (-r).");
return false;
}
if (!params.options.ip || params.options.ip[0] == '\0') {
PrintError("--ip must specify a valid IP address");
return false;
}
if (!params.options.port || params.options.port <= 0 ||
params.options.port > UINT16_MAX) {
PrintError("--port must specify a valid port");
return false;
}
// Note: ZSTD_minCLevel() is ridiculously small (-131072), so use a
// reasonable value.
assert(ZSTD_minCLevel() <= Options::kMinCompressLevel);
assert(ZSTD_maxCLevel() == Options::kMaxCompressLevel);
static_assert(Options::kMinCompressLevel < 0);
static_assert(Options::kMaxCompressLevel > 0);
if (params.options.compress_level < Options::kMinCompressLevel ||
params.options.compress_level > Options::kMaxCompressLevel ||
params.options.compress_level == 0) {
PrintError("--compress_level must be between %i..-1 or 1..%i",
Options::kMinCompressLevel, Options::kMaxCompressLevel);
return false;
}
// Warn that any include rules not followed by an exclude rule are pointless
// as the files would be included, anyway.
for (int n = static_cast<int>(params.filter_rules.size()) - 1; n >= 0; --n) {
const Parameters::FilterRule& rule = params.filter_rules[n];
if (rule.type == FilterRule::Type::kExclude) {
break;
}
std::cout << "Warning: Include pattern '" << rule.pattern
<< "' has no effect, not followed by exclude pattern"
<< std::endl;
}
return true;
}
bool CheckOptionResult(OptionResult result, const std::string& name,
const char* value) {
switch (result) {
case OptionResult::kConsumedKey:
return true;
case OptionResult::kConsumedKeyValue:
if (!value) {
PrintError("Option '%s' needs a value", name);
return false;
}
return true;
case OptionResult::kError:
// Error message was already printed.
return false;
}
return true;
}
} // namespace
const char* HelpText() { return kHelpText; }
// Note that abseil has a flags library, but the C++ version doesn't support
// short names ("-q"), see https://abseil.io/docs/cpp/guides/flags. However, we
// aim to be roughly compatible with vanilla rsync, which does have short flag
// names like "-q".
bool Parse(int argc, const char* const* argv, Parameters* parameters) {
if (argc <= 1) {
std::cout << kHelpText;
return false;
}
bool help = false;
for (int index = 1; index < argc; ++index) {
// Handle '--key [value]' and '--key=value' options.
bool equality_used = false;
if (strncmp(argv[index], "--", 2) == 0) {
std::string key(argv[index] + 2);
const char* value = nullptr;
size_t equality_pos = key.find("=");
if (equality_pos != std::string::npos) {
if (equality_pos + 1 < key.size()) {
value = argv[index] + 2 + equality_pos + 1;
}
key = key.substr(0, equality_pos);
equality_used = true;
} else {
value = index + 1 < argc && argv[index + 1][0] != '-' ? argv[index + 1]
: nullptr;
}
OptionResult result = HandleParameter(key, value, parameters, &help);
if (!CheckOptionResult(result, key, value)) {
return false;
}
if (!equality_used && result == OptionResult::kConsumedKeyValue) {
++index;
}
continue;
}
// Handle '-abc' options.
if (strncmp(argv[index], "-", 1) == 0) {
char key[] = "x";
char name[] = "-x";
for (const char* c = argv[index] + 1; *c != 0; ++c) {
key[0] = *c;
name[1] = *c;
OptionResult result = HandleParameter(key, nullptr, parameters, &help);
// These args shouldn't try to consume values.
assert(result != OptionResult::kConsumedKeyValue);
if (!CheckOptionResult(result, name, nullptr)) {
return false;
}
}
continue;
}
// The last added option is the destination. Move previously added options
// to the sources.
if (!parameters->destination.empty()) {
parameters->sources.push_back(std::move(parameters->destination));
}
parameters->destination = argv[index];
}
// Load files-from file (can't do it when --files-from is handled since not
// all sources might have been read at that point.
if (parameters->files_from &&
!LoadFilesFrom(parameters->files_from, &parameters->sources,
&parameters->sources_dir)) {
return false;
}
if (!CheckParameters(*parameters, help)) {
return false;
}
if (parameters->sources.empty() && parameters->destination.empty()) {
PrintError("Missing source and destination");
return false;
}
if (parameters->destination.empty()) {
PrintError("Missing destination");
return false;
}
if (parameters->sources.empty()) {
// If one arg was passed on the command line, it is not clear whether it
// was supposed to be a source or destination. Try to infer that, e.g.
// cdc_rsync *.txt -> Missing destination
// cdc_rsync /mnt/developer -> Missing source
bool missing_src = parameters->destination[0] == '/';
PrintError("Missing %s", missing_src ? "source" : "destination");
return false;
}
return true;
}
} // namespace params
} // namespace cdc_ft

54
cdc_rsync_cli/params.h Normal file
View File

@@ -0,0 +1,54 @@
/*
* 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.
*/
#ifndef CDC_RSYNC_CLI_PARAMS_H_
#define CDC_RSYNC_CLI_PARAMS_H_
#include <string>
#include <vector>
#include "cdc_rsync/cdc_rsync.h"
namespace cdc_ft {
namespace params {
// All cdc_rsync command line parameters.
struct Parameters {
// Copy of cdc_ft::FilterRule with std::string instead of const char*.
struct FilterRule {
using Type = ::cdc_ft::FilterRule::Type;
FilterRule(Type type, std::string pattern)
: type(type), pattern(std::move(pattern)) {}
Type type;
std::string pattern;
};
Options options;
std::vector<FilterRule> filter_rules;
std::vector<std::string> sources;
std::string destination;
const char* files_from = nullptr;
std::string sources_dir; // Base directory for files loaded for --files-from.
};
// Parses sources, destination and options from the command line args.
// Prints a help text if not enough arguments were given or -h/--help was given.
bool Parse(int argc, const char* const* argv, Parameters* parameters);
} // namespace params
} // namespace cdc_ft
#endif // CDC_RSYNC_CLI_PARAMS_H_

View File

@@ -0,0 +1,512 @@
// 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 "cdc_rsync_cli/params.h"
#include "absl/strings/match.h"
#include "common/log.h"
#include "common/path.h"
#include "common/test_main.h"
#include "gtest/gtest.h"
namespace cdc_ft {
namespace params {
namespace {
class TestLog : public Log {
public:
explicit TestLog() : Log(LogLevel::kInfo) {}
protected:
void WriteLogMessage(LogLevel level, const char* file, int line,
const char* func, const char* message) override {
errors_ += message;
}
private:
std::string errors_;
};
std::string NeedsValueError(const char* option_name) {
return absl::StrFormat("Option '%s' needs a value", option_name);
}
class ParamsTest : public ::testing::Test {
public:
void SetUp() override { prev_stderr_ = std::cerr.rdbuf(errors_.rdbuf()); }
void TearDown() override { std::cerr.rdbuf(prev_stderr_); }
protected:
void ExpectNoError() const {
EXPECT_TRUE(errors_.str().empty())
<< "Expected empty stderr but got\n'" << errors_.str() << "'";
}
void ExpectError(const std::string& expected) const {
EXPECT_TRUE(absl::StrContains(errors_.str(), expected))
<< "Expected stderr to contain '" << expected << "' but got\n'"
<< errors_.str() << "'";
}
void ClearErrors() { errors_.str(std::string()); }
std::string base_dir_ = GetTestDataDir("params");
std::string sources_file_ = path::Join(base_dir_, "source_files.txt");
std::string empty_sources_file_ =
path::Join(base_dir_, "empty_source_files.txt");
Parameters parameters_;
std::stringstream errors_;
std::streambuf* prev_stderr_;
};
TEST_F(ParamsTest, ParseSucceedsDefaults) {
const char* argv[] = {"cdc_rsync.exe", "--ip=1.2.3.4", "--port=1234",
"source", "destination", NULL};
EXPECT_TRUE(Parse(static_cast<int>(std::size(argv)) - 1, argv, &parameters_));
EXPECT_STREQ("1.2.3.4", parameters_.options.ip);
EXPECT_EQ(1234, parameters_.options.port);
EXPECT_FALSE(parameters_.options.delete_);
EXPECT_FALSE(parameters_.options.recursive);
EXPECT_EQ(0, parameters_.options.verbosity);
EXPECT_FALSE(parameters_.options.quiet);
EXPECT_FALSE(parameters_.options.whole_file);
EXPECT_FALSE(parameters_.options.compress);
EXPECT_FALSE(parameters_.options.checksum);
EXPECT_FALSE(parameters_.options.dry_run);
EXPECT_EQ(parameters_.options.copy_dest, nullptr);
EXPECT_EQ(6, parameters_.options.compress_level);
EXPECT_EQ(10, parameters_.options.connection_timeout_sec);
EXPECT_EQ(1, parameters_.sources.size());
EXPECT_EQ(parameters_.sources[0], "source");
EXPECT_EQ(parameters_.destination, "destination");
ExpectNoError();
}
TEST_F(ParamsTest, ParseSucceedsWithOptionFromTwoArguments) {
const char* argv[] = {
"cdc_rsync.exe", "--ip=1.2.3.4", "--port=1234", "--compress-level", "2",
"source", "destination", NULL};
EXPECT_TRUE(Parse(static_cast<int>(std::size(argv)) - 1, argv, &parameters_));
EXPECT_EQ(parameters_.options.compress_level, 2);
ExpectNoError();
}
TEST_F(ParamsTest,
ParseSucceedsWithOptionFromOneArgumentWithEqualityWithValue) {
const char* argv[] = {
"cdc_rsync.exe", "--ip=1.2.3.4", "--port=1234", "--compress-level=2",
"source", "destination", NULL};
EXPECT_TRUE(Parse(static_cast<int>(std::size(argv)) - 1, argv, &parameters_));
ASSERT_EQ(parameters_.sources.size(), 1);
EXPECT_EQ(parameters_.options.compress_level, 2);
EXPECT_EQ(parameters_.sources[0], "source");
EXPECT_EQ(parameters_.destination, "destination");
ExpectNoError();
}
TEST_F(ParamsTest, ParseFailsOnCompressLevelEqualsNoValue) {
const char* argv[] = {"cdc_rsync.exe", "--compress-level=", "source",
"destination", NULL};
EXPECT_FALSE(
Parse(static_cast<int>(std::size(argv)) - 1, argv, &parameters_));
ExpectError(NeedsValueError("compress-level"));
}
TEST_F(ParamsTest, ParseFailsOnPortEqualsNoValue) {
const char* argv[] = {"cdc_rsync.exe", "--port=", "source", "destination",
NULL};
EXPECT_FALSE(
Parse(static_cast<int>(std::size(argv)) - 1, argv, &parameters_));
ExpectError(NeedsValueError("port"));
}
TEST_F(ParamsTest, ParseFailsOnContimeoutEqualsNoValue) {
const char* argv[] = {"cdc_rsync.exe", "--contimeout=", "source",
"destination", NULL};
EXPECT_FALSE(
Parse(static_cast<int>(std::size(argv)) - 1, argv, &parameters_));
ExpectError(NeedsValueError("contimeout"));
}
TEST_F(ParamsTest, ParseFailsOnIpEqualsNoValue) {
const char* argv[] = {"cdc_rsync.exe", "--ip=", "source", "destination",
NULL};
EXPECT_FALSE(
Parse(static_cast<int>(std::size(argv)) - 1, argv, &parameters_));
ExpectError(NeedsValueError("ip"));
}
TEST_F(ParamsTest, ParseWithoutParametersFailsOnMissingSourceAndDestination) {
const char* argv[] = {"cdc_rsync.exe", "--ip=1.2.3.4", "--port=1234", NULL};
EXPECT_FALSE(
Parse(static_cast<int>(std::size(argv)) - 1, argv, &parameters_));
ExpectError("Missing source");
}
TEST_F(ParamsTest, ParseWithSingleParameterFailsOnMissingDestination) {
const char* argv[] = {"cdc_rsync.exe", "--ip=1.2.3.4", "--port=1234",
"source", NULL};
EXPECT_FALSE(
Parse(static_cast<int>(std::size(argv)) - 1, argv, &parameters_));
ExpectError("Missing destination");
}
TEST_F(ParamsTest, ParseSuccessedsWithMultipleLetterKeyConsumed) {
const char* argv[] = {
"cdc_rsync.exe", "--ip=1.2.3.4", "--port=1234", "-rvqWRzcn",
"source", "destination", NULL};
EXPECT_TRUE(Parse(static_cast<int>(std::size(argv)) - 1, argv, &parameters_));
EXPECT_TRUE(parameters_.options.recursive);
EXPECT_EQ(parameters_.options.verbosity, 1);
EXPECT_TRUE(parameters_.options.quiet);
EXPECT_TRUE(parameters_.options.whole_file);
EXPECT_TRUE(parameters_.options.relative);
EXPECT_TRUE(parameters_.options.compress);
EXPECT_TRUE(parameters_.options.checksum);
EXPECT_TRUE(parameters_.options.dry_run);
ExpectNoError();
}
TEST_F(ParamsTest,
ParseFailsOnMultipleLetterKeyConsumedOptionsWithUnsupportedOne) {
const char* argv[] = {"cdc_rsync.exe", "-rvqaWRzcn", "source", "destination",
NULL};
EXPECT_FALSE(
Parse(static_cast<int>(std::size(argv)) - 1, argv, &parameters_));
ExpectError("Unknown option: 'a'");
}
TEST_F(ParamsTest, ParseSuccessedsWithMultipleLongKeyConsumedOptions) {
const char* argv[] = {"cdc_rsync.exe",
"--ip=1.2.3.4",
"--port=1234",
"--recursive",
"--verbosity",
"--quiet",
"--whole-file",
"--compress",
"--relative",
"--delete",
"--checksum",
"--dry-run",
"--existing",
"--json",
"source",
"destination",
NULL};
EXPECT_TRUE(Parse(static_cast<int>(std::size(argv)) - 1, argv, &parameters_));
EXPECT_TRUE(parameters_.options.recursive);
EXPECT_EQ(parameters_.options.verbosity, 1);
EXPECT_TRUE(parameters_.options.quiet);
EXPECT_TRUE(parameters_.options.whole_file);
EXPECT_TRUE(parameters_.options.relative);
EXPECT_TRUE(parameters_.options.compress);
EXPECT_TRUE(parameters_.options.delete_);
EXPECT_TRUE(parameters_.options.checksum);
EXPECT_TRUE(parameters_.options.dry_run);
EXPECT_TRUE(parameters_.options.existing);
EXPECT_TRUE(parameters_.options.json);
ExpectNoError();
}
TEST_F(ParamsTest, ParseFailsOnUnknownKey) {
const char* argv[] = {"cdc_rsync.exe", "-unknownKey", "source", "destination",
NULL};
EXPECT_FALSE(
Parse(static_cast<int>(std::size(argv)) - 1, argv, &parameters_));
ExpectError("Unknown option: 'u'");
}
TEST_F(ParamsTest, ParseSuccessedsWithSupportedKeyValue) {
const char* argv[] = {
"cdc_rsync.exe", "--compress-level", "11", "--port=4086",
"--ip=127.0.0.1", "--contimeout", "99", "--copy-dest=dest",
"source", "destination", NULL};
EXPECT_TRUE(Parse(static_cast<int>(std::size(argv)) - 1, argv, &parameters_));
EXPECT_EQ(parameters_.options.compress_level, 11);
EXPECT_EQ(parameters_.options.connection_timeout_sec, 99);
EXPECT_EQ(parameters_.options.port, 4086);
EXPECT_STREQ(parameters_.options.ip, "127.0.0.1");
EXPECT_STREQ(parameters_.options.copy_dest, "dest");
ExpectNoError();
}
TEST_F(ParamsTest,
ParseSuccessedsWithSupportedKeyValueWithoutEqualityForChars) {
const char* argv[] = {"cdc_rsync.exe", "--port", "4086", "--ip",
"127.0.0.1", "--copy-dest", "dest", "source",
"destination", NULL};
EXPECT_TRUE(Parse(static_cast<int>(std::size(argv)) - 1, argv, &parameters_));
EXPECT_EQ(parameters_.options.port, 4086);
EXPECT_STREQ(parameters_.options.ip, "127.0.0.1");
EXPECT_STREQ(parameters_.options.copy_dest, "dest");
ExpectNoError();
}
TEST_F(ParamsTest, ParseFailsOnGameletIpNeedsPort) {
const char* argv[] = {"cdc_rsync.exe", "--ip=127.0.0.1", "source",
"destination", NULL};
EXPECT_FALSE(
Parse(static_cast<int>(std::size(argv)) - 1, argv, &parameters_));
ExpectError("--port must specify a valid port");
}
TEST_F(ParamsTest, ParseFailsOnDeleteNeedsRecursive) {
const char* argv[] = {
"cdc_rsync.exe", "--ip=1.2.3.4", "--port=1234", "--delete",
"source", "destination", NULL};
EXPECT_FALSE(
Parse(static_cast<int>(std::size(argv)) - 1, argv, &parameters_));
ExpectError("--delete does not work without --recursive (-r)");
}
TEST_F(ParamsTest, ParseChecksCompressLevel) {
int minLevel = Options::kMinCompressLevel;
int maxLevel = Options::kMaxCompressLevel;
int levels[] = {minLevel - 1, minLevel, 0, maxLevel, maxLevel + 1};
bool valid[] = {false, true, false, true, false};
for (int n = 0; n < std::size(levels); ++n) {
std::string level = "--compress-level=" + std::to_string(levels[n]);
const char* argv[] = {"cdc_rsync.exe", "--ip=1.2.3.4", "--port=1234",
level.c_str(), "source", "destination"};
EXPECT_TRUE(Parse(static_cast<int>(std::size(argv)) - 1, argv,
&parameters_) == valid[n]);
if (valid[n]) {
ExpectNoError();
} else {
ExpectError("--compress_level must be between");
}
ClearErrors();
}
}
TEST_F(ParamsTest, ParseFailsOnUnknownKeyValue) {
const char* argv[] = {"cdc_rsync.exe", "--unknownKey=5", "source",
"destination", NULL};
EXPECT_FALSE(
Parse(static_cast<int>(std::size(argv)) - 1, argv, &parameters_));
ExpectError("unknownKey");
}
TEST_F(ParamsTest, ParseFailsWithHelpOption) {
const char* argv[] = {"cdc_rsync.exe", "--ip=1.2.3.4", "--port=1234",
"source", "destination", NULL};
EXPECT_TRUE(Parse(static_cast<int>(std::size(argv)) - 1, argv, &parameters_));
const char* argv2[] = {
"cdc_rsync.exe", "--ip=1.2.3.4", "--port=1234", "source",
"destination", "--help", NULL};
EXPECT_FALSE(
Parse(static_cast<int>(std::size(argv2)) - 1, argv2, &parameters_));
ExpectNoError();
const char* argv3[] = {
"cdc_rsync.exe", "--ip=1.2.3.4", "--port=1234", "source",
"destination", "-h", NULL};
EXPECT_FALSE(
Parse(static_cast<int>(std::size(argv3)) - 1, argv3, &parameters_));
ExpectNoError();
}
TEST_F(ParamsTest, ParseSucceedsWithIncludeExclude) {
const char* argv[] = {
"cdc_rsync.exe", "--ip=1.2.3.4", "--port=1234", "--include=*.txt",
"--exclude", "*.dat", "--include", "*.exe",
"source", "destination", NULL};
EXPECT_TRUE(Parse(static_cast<int>(std::size(argv)) - 1, argv, &parameters_));
ASSERT_EQ(parameters_.filter_rules.size(), 3);
ASSERT_EQ(parameters_.filter_rules[0].type, FilterRule::Type::kInclude);
ASSERT_EQ(parameters_.filter_rules[0].pattern, "*.txt");
ASSERT_EQ(parameters_.filter_rules[1].type, FilterRule::Type::kExclude);
ASSERT_EQ(parameters_.filter_rules[1].pattern, "*.dat");
ASSERT_EQ(parameters_.filter_rules[2].type, FilterRule::Type::kInclude);
ASSERT_EQ(parameters_.filter_rules[2].pattern, "*.exe");
ExpectNoError();
}
TEST_F(ParamsTest, FilesFrom_NoFile) {
const char* argv[] = {
"cdc_rsync.exe", "--ip=1.2.3.4", "--port=1234", "source",
"destination", "--files-from", NULL};
EXPECT_FALSE(
Parse(static_cast<int>(std::size(argv)) - 1, argv, &parameters_));
ExpectError(NeedsValueError("files-from"));
}
TEST_F(ParamsTest, FilesFrom_ImpliesRelative) {
const char* argv[] = {
"cdc_rsync.exe", "--ip=1.2.3.4", "--port=1234", "--files-from",
sources_file_.c_str(), base_dir_.c_str(), "destination", NULL};
EXPECT_TRUE(Parse(static_cast<int>(std::size(argv)) - 1, argv, &parameters_));
EXPECT_TRUE(parameters_.options.relative);
ExpectNoError();
}
TEST_F(ParamsTest, FilesFrom_WithoutSourceArg) {
const char* argv[] = {
"cdc_rsync.exe", "--ip=1.2.3.4", "--port=1234", "--files-from",
sources_file_.c_str(), "destination", NULL};
EXPECT_TRUE(Parse(static_cast<int>(std::size(argv)) - 1, argv, &parameters_));
EXPECT_TRUE(parameters_.sources_dir.empty());
EXPECT_EQ(parameters_.destination, "destination");
ExpectNoError();
}
TEST_F(ParamsTest, FilesFrom_WithSourceArg) {
const char* argv[] = {
"cdc_rsync.exe", "--ip=1.2.3.4", "--port=1234", "--files-from",
sources_file_.c_str(), base_dir_.c_str(), "destination", NULL};
EXPECT_TRUE(Parse(static_cast<int>(std::size(argv)) - 1, argv, &parameters_));
std::string expected_sources_dir = base_dir_;
path::EnsureEndsWithPathSeparator(&expected_sources_dir);
EXPECT_EQ(parameters_.sources_dir, expected_sources_dir);
EXPECT_EQ(parameters_.destination, "destination");
ExpectNoError();
}
TEST_F(ParamsTest, FilesFrom_ParsesFile) {
const char* argv[] = {
"cdc_rsync.exe", "--ip=1.2.3.4", "--port=1234", "--files-from",
sources_file_.c_str(), "destination", NULL};
EXPECT_TRUE(Parse(static_cast<int>(std::size(argv)) - 1, argv, &parameters_));
std::vector<const char*> expected = {"file1", "file2", "file3"};
ASSERT_EQ(parameters_.sources.size(), expected.size());
for (size_t n = 0; n < expected.size(); ++n) {
EXPECT_EQ(parameters_.sources[n], expected[n]);
}
ExpectNoError();
}
TEST_F(ParamsTest, FilesFrom_EmptyFile_WithoutSourceArg) {
const char* argv[] = {"cdc_rsync.exe",
"--ip=1.2.3.4",
"--port=1234",
"--files-from",
empty_sources_file_.c_str(),
"destination",
NULL};
EXPECT_FALSE(
Parse(static_cast<int>(std::size(argv)) - 1, argv, &parameters_));
ExpectError(empty_sources_file_);
ExpectError("--files-from option is empty");
}
TEST_F(ParamsTest, FilesFrom_EmptyFile_WithSourceArg) {
const char* argv[] = {"cdc_rsync.exe",
"--ip=1.2.3.4",
"--port=1234",
"--files-from",
empty_sources_file_.c_str(),
base_dir_.c_str(),
"destination",
NULL};
EXPECT_FALSE(
Parse(static_cast<int>(std::size(argv)) - 1, argv, &parameters_));
ExpectError(empty_sources_file_);
ExpectError("--files-from option is empty");
}
TEST_F(ParamsTest, FilesFrom_NoDestination) {
const char* argv[] = {"cdc_rsync.exe", "--ip=1.2.3.4", "--port=1234",
"--files-from", sources_file_.c_str(), NULL};
EXPECT_FALSE(
Parse(static_cast<int>(std::size(argv)) - 1, argv, &parameters_));
ExpectError("Missing destination");
}
TEST_F(ParamsTest, IncludeFrom_NoFile) {
const char* argv[] = {
"cdc_rsync.exe", "--ip=1.2.3.4", "--port=1234", "source",
"destination", "--include-from", NULL};
EXPECT_FALSE(
Parse(static_cast<int>(std::size(argv)) - 1, argv, &parameters_));
ExpectError(NeedsValueError("include-from"));
}
TEST_F(ParamsTest, IncludeFrom_ParsesFile) {
std::string file = path::Join(base_dir_, "include_files.txt");
const char* argv[] = {
"cdc_rsync.exe", "--ip=1.2.3.4", "--port=1234", "--include-from",
file.c_str(), "source", "destination", NULL};
EXPECT_TRUE(Parse(static_cast<int>(std::size(argv)) - 1, argv, &parameters_));
ASSERT_EQ(parameters_.filter_rules.size(), 1);
ASSERT_EQ(parameters_.filter_rules[0].type, FilterRule::Type::kInclude);
ASSERT_EQ(parameters_.filter_rules[0].pattern, "file3");
ExpectNoError();
}
TEST_F(ParamsTest, ExcludeFrom_NoFile) {
const char* argv[] = {"cdc_rsync.exe", "source", "destination",
"--exclude-from", NULL};
EXPECT_FALSE(
Parse(static_cast<int>(std::size(argv)) - 1, argv, &parameters_));
ExpectError(NeedsValueError("exclude-from"));
}
TEST_F(ParamsTest, ExcludeFrom_ParsesFile) {
std::string file = path::Join(base_dir_, "exclude_files.txt");
const char* argv[] = {
"cdc_rsync.exe", "--ip=1.2.3.4", "--port=1234", "--exclude-from",
file.c_str(), "source", "destination", NULL};
EXPECT_TRUE(Parse(static_cast<int>(std::size(argv)) - 1, argv, &parameters_));
ASSERT_EQ(parameters_.filter_rules.size(), 2);
EXPECT_EQ(parameters_.filter_rules[0].type, FilterRule::Type::kExclude);
EXPECT_EQ(parameters_.filter_rules[0].pattern, "file1");
EXPECT_EQ(parameters_.filter_rules[1].type, FilterRule::Type::kExclude);
EXPECT_EQ(parameters_.filter_rules[1].pattern, "file2");
ExpectNoError();
}
TEST_F(ParamsTest, IncludeExcludeMixed_ProperOrder) {
std::string exclude_file = path::Join(base_dir_, "exclude_files.txt");
std::string include_file = path::Join(base_dir_, "include_files.txt");
const char* argv[] = {"cdc_rsync.exe",
"--ip=1.2.3.4",
"--port=1234",
"--include-from",
include_file.c_str(),
"--exclude=excl1",
"source",
"--exclude-from",
exclude_file.c_str(),
"destination",
"--include",
"incl1",
NULL};
EXPECT_TRUE(Parse(static_cast<int>(std::size(argv)) - 1, argv, &parameters_));
ASSERT_EQ(parameters_.filter_rules.size(), 5);
EXPECT_EQ(parameters_.filter_rules[0].type, FilterRule::Type::kInclude);
EXPECT_EQ(parameters_.filter_rules[0].pattern, "file3");
EXPECT_EQ(parameters_.filter_rules[1].type, FilterRule::Type::kExclude);
EXPECT_EQ(parameters_.filter_rules[1].pattern, "excl1");
EXPECT_EQ(parameters_.filter_rules[2].type, FilterRule::Type::kExclude);
EXPECT_EQ(parameters_.filter_rules[2].pattern, "file1");
EXPECT_EQ(parameters_.filter_rules[3].type, FilterRule::Type::kExclude);
EXPECT_EQ(parameters_.filter_rules[3].pattern, "file2");
EXPECT_EQ(parameters_.filter_rules[4].type, FilterRule::Type::kInclude);
EXPECT_EQ(parameters_.filter_rules[4].pattern, "incl1");
ExpectNoError();
}
} // namespace
} // namespace params
} // namespace cdc_ft

View File

@@ -0,0 +1,3 @@

View File

@@ -0,0 +1,2 @@
file1
file2

View File

@@ -0,0 +1 @@
file3

View File

@@ -0,0 +1,6 @@
file1
file2
file3

0
cdc_rsync_cli/testdata/root.txt vendored Normal file
View File