diff --git a/README.md b/README.md index 17c0f72f..f9661a91 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,25 @@ Downloading model: Download succeeded. ``` +**Private access tokens** +Private models and worlds can be downloaded using access tokens. +Access tokens are generated on `app.gazebosim.org`. After logging in, +go to `Settings->Access Tokens`. + +An access token can be used with CLI commands via the `--header` option: + +```bash +$ gz fuel download -u https://fuel.gazebosim.org/1.0/openrobotics/models/ambulance --header 'Private-Token: ' +``` + +Or, an access token can be stored in a `~/.gz/fuel/config.yaml` file. The token is then +automatically used by the command line tool and API calls. Use the `configure` helper +tool create your `~/.gz/fuel/config.yaml` file. + +```bash +$ gz fuel configure +``` + **C++ Get List models** ```cpp // Create a client (uses https://fuel.gazebosim.org by default) diff --git a/conf/CMakeLists.txt b/conf/CMakeLists.txt index 6dd6a39f..2cffca0e 100644 --- a/conf/CMakeLists.txt +++ b/conf/CMakeLists.txt @@ -18,7 +18,3 @@ configure_file( # Install the yaml configuration files in an unversioned location. install(FILES ${CMAKE_CURRENT_BINARY_DIR}/fuel${PROJECT_VERSION_MAJOR}.yaml DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/gz/) - -# Install config.yaml -install (FILES config.yaml DESTINATION - ${CMAKE_INSTALL_DATAROOTDIR}/gz/${GZ_DESIGNATION}${PROJECT_VERSION_MAJOR}/) diff --git a/conf/config.yaml b/conf/config.yaml deleted file mode 100644 index bf70a5dd..00000000 --- a/conf/config.yaml +++ /dev/null @@ -1,14 +0,0 @@ ---- -# The list of servers. -servers: - - - name: osrf - url: https://fuel.gazebosim.org - - # - - # name: another_server - # url: https://myserver - -# Where are the assets stored in disk. -# cache: -# path: /tmp/gz/fuel diff --git a/include/gz/fuel_tools/WorldIdentifier.hh b/include/gz/fuel_tools/WorldIdentifier.hh index 9be30374..62c8d738 100644 --- a/include/gz/fuel_tools/WorldIdentifier.hh +++ b/include/gz/fuel_tools/WorldIdentifier.hh @@ -148,6 +148,16 @@ namespace gz::fuel_tools /// \return World information string public: std::string AsPrettyString(const std::string &_prefix = "") const; + /// \brief Returns the privacy setting of the world. + /// \return True if the world is private, false if the world is + /// public. + public: bool Private() const; + + /// \brief Set the privacy setting of the world. + /// \param[in] _private True indicates the world is private, + /// false indicates the world is public. + public: void SetPrivate(bool _private); + /// \brief PIMPL private: std::unique_ptr dataPtr; }; diff --git a/src/ClientConfig.cc b/src/ClientConfig.cc index dd9b7466..d85300e7 100644 --- a/src/ClientConfig.cc +++ b/src/ClientConfig.cc @@ -82,18 +82,24 @@ ClientConfig::ClientConfig() : dataPtr(new ClientConfigPrivate) << "to set cache path. Please use [GZ_FUEL_CACHE_PATH] instead." << std::endl; } - else - { - return; - } } - if (!gz::common::isDirectory(gzFuelPath)) + if (!gzFuelPath.empty()) { - gzerr << "[" << gzFuelPath << "] is not a directory" << std::endl; - return; + if (!gz::common::isDirectory(gzFuelPath)) + gzerr << "[" << gzFuelPath << "] is not a directory" << std::endl; + else + this->SetCacheLocation(gzFuelPath); } - this->SetCacheLocation(gzFuelPath); + + std::string configYamlFile = common::joinPaths(this->CacheLocation(), + "config.yaml"); + std::string configYmlFile = common::joinPaths(this->CacheLocation(), + "config.yml"); + if (gz::common::exists(configYamlFile)) + this->LoadConfig(configYamlFile); + else if (gz::common::exists(configYmlFile)) + this->LoadConfig(configYmlFile); } ////////////////////////////////////////////////// diff --git a/src/WorldIdentifier.cc b/src/WorldIdentifier.cc index fd07e8fd..02d36313 100644 --- a/src/WorldIdentifier.cc +++ b/src/WorldIdentifier.cc @@ -44,8 +44,12 @@ class WorldIdentifierPrivate /// \brief World version. Valid versions start from 1, 0 means the tip. public: unsigned int version{0}; - /// \brief Path of this model in the local cache + /// \brief Path of this world in the local cache public: std::string localPath; + + /// \brief True indicates the world is private, false indicates the + /// world is public. + public: bool privacy{false}; }; ////////////////////////////////////////////////// @@ -229,4 +233,15 @@ std::string WorldIdentifier::AsPrettyString(const std::string &_prefix) const << this->Server().AsPrettyString(_prefix + " "); return out.str(); } +////////////////////////////////////////////////// +bool WorldIdentifier::Private() const +{ + return this->dataPtr->privacy; +} + +////////////////////////////////////////////////// +void WorldIdentifier::SetPrivate(bool _private) +{ + this->dataPtr->privacy = _private; +} } // namespace gz::fuel_tools diff --git a/src/WorldIdentifier_TEST.cc b/src/WorldIdentifier_TEST.cc index 91c2d963..4e293f41 100644 --- a/src/WorldIdentifier_TEST.cc +++ b/src/WorldIdentifier_TEST.cc @@ -37,6 +37,12 @@ TEST(WorldIdentifier, SetFields) EXPECT_EQ(std::string("hello"), id.Name()); EXPECT_EQ(std::string("acai"), id.Owner()); EXPECT_EQ(6u, id.Version()); + + EXPECT_FALSE(id.Private()); + id.SetPrivate(true); + EXPECT_TRUE(id.Private()); + id.SetPrivate(false); + EXPECT_FALSE(id.Private()); } ///////////////////////////////////////////////// diff --git a/src/cmd/cmdfuel.rb.in b/src/cmd/cmdfuel.rb.in index 632d9443..23e89a67 100755 --- a/src/cmd/cmdfuel.rb.in +++ b/src/cmd/cmdfuel.rb.in @@ -25,7 +25,10 @@ else include Fiddle end +require 'fileutils' require 'optparse' +require 'uri' +require 'yaml' # Constants. LIBRARY_NAME = '@library_location@' @@ -61,6 +64,7 @@ COMMANDS = { 'fuel' => " gz fuel [action] [options] \n"\ " \n"\ "Available Actions: \n"\ + " configure Create config.yaml configuration file \n"\ " delete Delete resources \n"\ " download Download resources \n"\ " edit Edit a resource \n"\ @@ -80,6 +84,20 @@ COMMANDS = { 'fuel' => } SUBCOMMANDS = { + 'configure' => + "Create `~/.gz/fuel/config.yaml` to hold Fuel server configurations. \n"\ + " \n"\ + " gz fuel configure [options] \n"\ + " \n"\ + " --defaults Use all the defaults and save. \n"\ + " This will overwrite ~/.gz/fuel/config.yaml. \n"\ + " --console Output to the console instead of to a file. \n"\ + " -h [--help] Print this help message. \n"\ + " \n"\ + " --force-version Use a specific library version. \n"\ + " \n"\ + " --versions Show the available versions. \n", + 'delete' => "Delete simulation resources \n"\ " \n"\ @@ -208,7 +226,9 @@ class Cmd 'pbtxt2config' => '', 'private' => '', 'onlymodels' => '0', - 'onlyworlds' => '0' + 'onlyworlds' => '0', + 'defaults' => false, + 'console' => false } usage = COMMANDS[args[0]] @@ -276,6 +296,12 @@ class Cmd opts.on('--onlyworlds', 'Only update worlds') do options['onlyworlds'] = '1' end + opts.on('--defaults', 'Use default values') do + options['defaults'] = true + end + opts.on('--console', 'Output to console') do + options['console'] = true + end end # opt_parser do opt_parser.parse!(args) @@ -347,6 +373,12 @@ class Cmd end # parse() def execute(args) + # Graceful exit on ctrl-c + Signal.trap("SIGINT") do + puts "\nExiting" + exit + end + options = parse(args) # Read the plugin that handles the command. @@ -394,6 +426,8 @@ class Cmd end case options['subcommand'] + when 'configure' + configure(options['defaults'], options['console']) when 'delete' Importer.extern 'int deleteUrl(const char *, const char *)' if not Importer.deleteUrl(options['url'], options['header']) @@ -463,4 +497,118 @@ class Cmd "from #{plugin}." end # begin end # execute + + # Runs `gz fuel configure` + def configure(defaults, console) + # Default fuel directory and configuration path + local_fuel_dir = File.join(Dir.home, '.gz', 'fuel') + config_path = File.join(local_fuel_dir, 'config.yaml') + + # The default Fuel server URL + default_url = 'https://fuel.gazebosim.org' + + # A lambda function that prompts the user to enter Fuel server information + get_server_info = lambda { + server_url = default_url + default_name = 'Fuel' + + # Prompt the user for a server name with a default value + # Repeat until the URL is valid, or the user hits ctrl-c + until defaults + print "Fuel server URL [#{default_url}]: " + server_url = STDIN.gets.chomp + server_url = default_url if server_url.empty? + begin + uri = URI.parse(server_url) + default_name = !uri.host || uri.host.empty? ? uri.to_s : uri.host + break + rescue URI::InvalidURIError + puts 'Invalid URL.\n' + end + end + + # Prompt the user for an access token with a default value of an + # empty string + access_token = '' + unless defaults + print 'Optional access token [None]: ' + access_token = STDIN.gets.chomp + end + + # Get the cache location + cache = local_fuel_dir + unless defaults + print "Local cache path [#{local_fuel_dir}]: " + cache = STDIN.gets.chomp + cache = local_fuel_dir if cache.empty? + end + + # Get a name to associate with this server + server_name = default_name + unless defaults + print "Name this server [#{default_name}]: " + server_name = STDIN.gets.chomp + server_name = default_name if server_name.empty? + end + + [server_name, server_url, access_token, cache] + } + + unless console + puts '# Set Fuel server configurations.' + puts "# This will create or replace `#{config_path}`.\n\n" + end + + servers = [] + confirmation = 'n' + # Allow the user to enter multiple Fuel servers + begin + server_name, server_url, access_token, cache = get_server_info.call + servers << { 'name' => server_name, + 'url' => server_url, + 'private-token' => access_token, + 'cache' => { 'path' => cache } } + unless defaults + print "\nAdd another Fuel server? [y/N]:" + confirmation = STDIN.gets.chomp.downcase + confirmation = 'n' if confirmation.empty? + end + end while confirmation == 'y' + + unless defaults || console + puts "\nReview:\n" + servers.each do |server| + puts " Name: #{server['name']}" + puts " URL: #{server['url']}" + puts " Cache: #{server['cache']['path']}" + puts " Access token: #{server['private-token']}" + puts "\n" if servers.size > 1 + end + end + + confirmation = 'y' + unless defaults || console + print 'Save? [Y/n]:' + confirmation = STDIN.gets.chomp.downcase + confirmation = 'y' if confirmation.empty? + end + + # Save settings + if confirmation == 'y' + config = {'servers' => servers} + + if console + puts config.to_yaml + else + # Make sure the ~/.gz/fuel directory exists. + FileUtils.mkdir_p(local_fuel_dir) + + # Write to the config.yaml file + File.open(config_path, 'w') { |file| file.write(config.to_yaml) } + puts 'Settings saved to ~/.gz/fuel/config.yaml.' + end + else + puts 'Settings not saved.' + end + end end # class diff --git a/src/gz_TEST.cc b/src/gz_TEST.cc index 7485e648..eb6ab82b 100644 --- a/src/gz_TEST.cc +++ b/src/gz_TEST.cc @@ -100,8 +100,9 @@ TEST(CmdLine, GZ_UTILS_TEST_ENABLED_ONLY_ON_LINUX(ListFail)) TEST(CmdLine, GZ_UTILS_TEST_ENABLED_ONLY_ON_LINUX(ModelListConfigServerUgly)) { - auto output = custom_exec_str(g_listCmd + " -t model --raw"); - EXPECT_NE(output.find("https://fuel.gazebosim.org/1.0/"), + auto output = custom_exec_str(g_listCmd + + " -t model --raw -u 'https://fuel.gazebosim.org' -o openrobotics"); + EXPECT_NE(output.find("https://fuel.gazebosim.org"), std::string::npos) << output; EXPECT_EQ(output.find("owners"), std::string::npos) << output; } @@ -151,3 +152,19 @@ TEST(CmdLine, EXPECT_NE(output.find("owners"), std::string::npos) << output; EXPECT_NE(output.find("worlds"), std::string::npos) << output; } + +TEST(CmdLine, + GZ_UTILS_TEST_ENABLED_ONLY_ON_LINUX(ConfigureDefaultsConsole)) +{ + std::string output = custom_exec_str( + g_exec + " fuel configure --console --defaults"); + + std::string expected = + "---\n" + "servers:\n" + "- name: Fuel\n" + " url: https://fuel.gazebosim.org\n" + " private-token: ''\n"; + + EXPECT_EQ(output.find(expected), 0); +} diff --git a/src/gz_src_TEST.cc b/src/gz_src_TEST.cc index 28d0de76..e3ab3a7e 100644 --- a/src/gz_src_TEST.cc +++ b/src/gz_src_TEST.cc @@ -95,7 +95,8 @@ TEST_F(CmdLine, ModelListFail) // https://github.com/gazebosim/gz-fuel-tools/issues/105 TEST_F(CmdLine, ModelListConfigServerUgly) { - EXPECT_TRUE(listModels("", "openroboticstest", "true")); + EXPECT_TRUE(listModels("https://fuel.gazebosim.org", + "openroboticstest", "true")); EXPECT_NE(this->stdOutBuffer.str().find("https://fuel.gazebosim.org"), std::string::npos) << this->stdOutBuffer.str(); diff --git a/tutorials/02_configuration.md b/tutorials/02_configuration.md index 8983a45c..1aa97067 100644 --- a/tutorials/02_configuration.md +++ b/tutorials/02_configuration.md @@ -35,6 +35,17 @@ The `cache` section captures options related with the local storage of the assets. `path` specifies the local directory where all assets will be downloaded. If not used, all assets are stored under `$HOME/.gz/fuel`. +## Guided Configuration + +The `gz fuel configure` CLI will walk you through the process of creating a +`~/.gz/fuel/config.yaml` file. Just run the following command, and answer +the prompts. Note that this command will replace your existing `~/.gz/fuel/config.yaml` +if you choose to save on the last step. + +```bash +gz fuel configure +``` + ## Custom configuration file path Gazebo Fuel's default configuration file is stored under