mirror of
https://github.com/nestriness/cdc-file-transfer.git
synced 2026-05-01 19:23:06 +03:00
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:
3
cdc_rsync_cli/.gitignore
vendored
Normal file
3
cdc_rsync_cli/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
x64/*
|
||||
*.log
|
||||
*.user
|
||||
44
cdc_rsync_cli/BUILD
Normal file
44
cdc_rsync_cli/BUILD
Normal 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/**"]),
|
||||
)
|
||||
87
cdc_rsync_cli/cdc_rsync_cli.vcxproj
Normal file
87
cdc_rsync_cli/cdc_rsync_cli.vcxproj
Normal 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>
|
||||
2
cdc_rsync_cli/cdc_rsync_cli.vcxproj.filters
Normal file
2
cdc_rsync_cli/cdc_rsync_cli.vcxproj.filters
Normal 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
72
cdc_rsync_cli/main.cc
Normal 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(), ¶meters)) {
|
||||
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(
|
||||
¶meters.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
442
cdc_rsync_cli/params.cc
Normal 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, ¶meters->sources,
|
||||
¶meters->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
54
cdc_rsync_cli/params.h
Normal 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_
|
||||
512
cdc_rsync_cli/params_test.cc
Normal file
512
cdc_rsync_cli/params_test.cc
Normal 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, ¶meters_));
|
||||
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, ¶meters_));
|
||||
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, ¶meters_));
|
||||
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, ¶meters_));
|
||||
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, ¶meters_));
|
||||
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, ¶meters_));
|
||||
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, ¶meters_));
|
||||
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, ¶meters_));
|
||||
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, ¶meters_));
|
||||
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, ¶meters_));
|
||||
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, ¶meters_));
|
||||
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, ¶meters_));
|
||||
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, ¶meters_));
|
||||
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, ¶meters_));
|
||||
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, ¶meters_));
|
||||
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, ¶meters_));
|
||||
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, ¶meters_));
|
||||
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,
|
||||
¶meters_) == 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, ¶meters_));
|
||||
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, ¶meters_));
|
||||
|
||||
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, ¶meters_));
|
||||
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, ¶meters_));
|
||||
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, ¶meters_));
|
||||
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, ¶meters_));
|
||||
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, ¶meters_));
|
||||
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, ¶meters_));
|
||||
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, ¶meters_));
|
||||
|
||||
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, ¶meters_));
|
||||
|
||||
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, ¶meters_));
|
||||
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, ¶meters_));
|
||||
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, ¶meters_));
|
||||
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, ¶meters_));
|
||||
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, ¶meters_));
|
||||
|
||||
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, ¶meters_));
|
||||
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, ¶meters_));
|
||||
|
||||
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, ¶meters_));
|
||||
|
||||
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
|
||||
3
cdc_rsync_cli/testdata/params/empty_source_files.txt
vendored
Normal file
3
cdc_rsync_cli/testdata/params/empty_source_files.txt
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
|
||||
|
||||
|
||||
2
cdc_rsync_cli/testdata/params/exclude_files.txt
vendored
Normal file
2
cdc_rsync_cli/testdata/params/exclude_files.txt
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
file1
|
||||
file2
|
||||
1
cdc_rsync_cli/testdata/params/include_files.txt
vendored
Normal file
1
cdc_rsync_cli/testdata/params/include_files.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
file3
|
||||
6
cdc_rsync_cli/testdata/params/source_files.txt
vendored
Normal file
6
cdc_rsync_cli/testdata/params/source_files.txt
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
file1
|
||||
|
||||
file2
|
||||
file3
|
||||
|
||||
|
||||
0
cdc_rsync_cli/testdata/root.txt
vendored
Normal file
0
cdc_rsync_cli/testdata/root.txt
vendored
Normal file
Reference in New Issue
Block a user