├── .circleci └── config.yml ├── .coveralls.yml ├── .github ├── CODEOWNERS └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── ChangeLog ├── Gemfile ├── README.rdoc ├── Rakefile ├── SECURITY.md ├── appveyor.yml ├── bin └── td ├── contrib └── completion │ ├── _td │ └── td-completion.bash ├── data ├── sample_apache.json └── sample_apache_gen.rb ├── dist ├── build_osx.sh ├── build_win81.sh ├── exe.rake ├── pkg.rake └── resources │ ├── exe │ ├── td │ ├── td-cmd.bat │ ├── td.bat │ └── td.iss │ └── pkg │ ├── Distribution.erb │ ├── PackageInfo.erb │ ├── postinstall │ └── td ├── java └── logging.properties ├── lib ├── td.rb └── td │ ├── command │ ├── account.rb │ ├── apikey.rb │ ├── bulk_import.rb │ ├── common.rb │ ├── connector.rb │ ├── db.rb │ ├── export.rb │ ├── help.rb │ ├── import.rb │ ├── job.rb │ ├── list.rb │ ├── options.rb │ ├── password.rb │ ├── query.rb │ ├── result.rb │ ├── runner.rb │ ├── sample.rb │ ├── sched.rb │ ├── schema.rb │ ├── server.rb │ ├── status.rb │ ├── table.rb │ ├── update.rb │ ├── user.rb │ └── workflow.rb │ ├── compact_format_yamler.rb │ ├── compat_core.rb │ ├── compat_gzip_reader.rb │ ├── config.rb │ ├── connector_config_normalizer.rb │ ├── distribution.rb │ ├── file_reader.rb │ ├── helpers.rb │ ├── updater.rb │ └── version.rb ├── spec ├── file_reader │ ├── filter_spec.rb │ ├── io_filter_spec.rb │ ├── line_reader_spec.rb │ ├── parsing_reader_spec.rb │ └── shared_context.rb ├── file_reader_spec.rb ├── spec_helper.rb └── td │ ├── command │ ├── account_spec.rb │ ├── connector_spec.rb │ ├── export_spec.rb │ ├── import_spec.rb │ ├── job_spec.rb │ ├── query_spec.rb │ ├── sched_spec.rb │ ├── schema_spec.rb │ ├── table_spec.rb │ └── workflow_spec.rb │ ├── common_spec.rb │ ├── compact_format_yamler_spec.rb │ ├── config_spec.rb │ ├── connector_config_normalizer_spec.rb │ ├── fixture │ ├── bulk_load.yml │ ├── ca.cert │ ├── server.cert │ ├── server.key │ ├── testRootCA.crt │ ├── testServer.crt │ ├── testServer.key │ └── tmp.zip │ ├── helpers_spec.rb │ ├── updater_spec.rb │ └── version_spec.rb └── td.gemspec /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | 4 | orbs: 5 | ruby: circleci/ruby@2.0.0 6 | win: circleci/windows@5.0.0 7 | 8 | 9 | commands: 10 | install_windows_requirements: 11 | description: "Install windows requirements" 12 | steps: 13 | - run: 14 | name: "Install MSYS2" 15 | command: choco install msys2 -y 16 | - run: 17 | name: "Install Ruby devkit" 18 | command: ridk install 2 3 19 | bundle-install: 20 | description: "Install dependencies" 21 | steps: 22 | - run: 23 | name: Which bundler? 24 | command: ruby -v; bundle -v 25 | - run: 26 | name: Bundle install 27 | command: bundle install 28 | run-tests: 29 | description: "Run tests" 30 | steps: 31 | - run: 32 | name: Run tests 33 | command: bundle exec rake spec SPEC_OPTS="-fd" 34 | run-tests-flow: 35 | description: "Single flow for running tests" 36 | steps: 37 | - checkout 38 | - bundle-install 39 | - run-tests 40 | 41 | 42 | jobs: 43 | 44 | ruby_27: 45 | docker: 46 | - image: cimg/ruby:2.7-browsers 47 | steps: 48 | - run-tests-flow 49 | 50 | ruby_30: 51 | docker: 52 | - image: cimg/ruby:3.0-browsers 53 | steps: 54 | - run-tests-flow 55 | 56 | ruby_31: 57 | docker: 58 | - image: cimg/ruby:3.1-browsers 59 | steps: 60 | - run-tests-flow 61 | 62 | ruby_32: 63 | docker: 64 | - image: cimg/ruby:3.2-browsers 65 | steps: 66 | - run-tests-flow 67 | 68 | win_ruby: 69 | executor: 70 | name: win/default 71 | shell: powershell.exe 72 | steps: 73 | - install_windows_requirements 74 | - run: 75 | name: "Install bundler" 76 | shell: powershell.exe 77 | command: gem install bundler 78 | - run-tests-flow 79 | 80 | 81 | workflows: 82 | tests: 83 | jobs: 84 | - ruby_27 85 | - ruby_30 86 | - ruby_31 87 | - ruby_32 88 | - win_ruby 89 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @treasure-data/integrations 2 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '29 4 * * 5' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'ruby' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle 2 | .DS_Store 3 | bundler 4 | build/td-import-java 5 | Gemfile.lock 6 | vendor/* 7 | /pkg/ 8 | build/* 9 | coverage/ 10 | dist/resources/pkg/ruby*.pkg 11 | *~ 12 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | #gem 'td-client', :git => 'https://github.com/treasure-data/td-client-ruby.git' 4 | 5 | gemspec 6 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = Treasure Data command line tool 2 | {Build Status}[https://circleci.com/gh/treasure-data/td.svg?style=svg] 3 | {appveyor}[https://ci.appveyor.com/project/treasure-data/td/branch/master] 4 | {Coverage Status}[https://coveralls.io/github/treasure-data/td?branch=master] 5 | 6 | This CUI utility wraps the {Ruby Client Library td-client-ruby}[https://github.com/treasure-data/td-client-ruby] 7 | to interact with the REST API in managing databases and jobs on the Treasure Data Cloud. 8 | 9 | For more info about Treasure Data, see . 10 | 11 | For full documentation see . 12 | 13 | = Getting Started 14 | 15 | Install td command as a gem. 16 | 17 | > gem install td 18 | 19 | See help message for details. 20 | 21 | > td 22 | 23 | You need to authorize the account, before executing any other commands. 24 | 25 | > td account 26 | 27 | = Sample Workflow 28 | 29 | > td account -f # authorize an account 30 | user: k@treasure-data.com 31 | password: ********** 32 | > td database:create mydb # create a database 33 | > td table:create mydb www_access # create a table 34 | 35 | = Packaging 36 | 37 | == Mac OS X 38 | 39 | Disable RVM or rbenv and use ruby.pkg's ruby (/usr/local/td/ruby/bin/ruby). 40 | And then run following commands: 41 | 42 | $ /usr/local/td/ruby/bin/gem install bundler rubyzip 43 | $ /usr/local/td/ruby/bin/bundle install 44 | $ /usr/local/td/ruby/bin/rake pkg:build 45 | 46 | It uses https://github.com/treasure-data/ruby-osx-packager 47 | 48 | == Windows 49 | 50 | Install following binary packages: 51 | 52 | * MinGW with MSYS Basic System and using mingw-get-inst 53 | * Git for Windows, with Windows Command Prompt support 54 | * Ruby ruby-1.9.3p327 using RubyInstaller for Windows, with PATH update 55 | * Inno Setup 5 56 | 57 | Then run following commands on MinGW Shell: 58 | 59 | $ mingw-get install msys-vim 60 | $ mingw-get install msys-wget 61 | $ gem install bundler rubyzip 62 | $ bundle install # don't use "--path" option 63 | $ rake exe:build # don't use "bundle exec" 64 | 65 | == Bulk Import 66 | 67 | Some of the +td+ commands prefixed with +td+ +import+ leverages the {Java Bulk Import CLI td-import-java}[https://github.com/treasure-data/td-import-java] 68 | to process and Bulk load data in the Treasure Data Cloud. 69 | 70 | The Bulk Import CLI is downloaded automatically at the first call to any of the command that require it; the use will 71 | need internet connectivity in order to fetch the Bulk Import CLI JAR file from the 72 | {Central Maven repository}[https://repo1.maven.org/maven2/com/treasuredata/td-import/] 73 | and take advantage of these advanced features. If you need to setup a proxy, please consult this 74 | {documentation}[https://docs.treasuredata.com/display/public/INT/Legacy+Bulk+Import+Tips+and+Tricks#LegacyBulkImportTipsandTricks-UsingaProxyServer] page. 75 | 76 | The log levels and properties of the Bulk Import CLI can be configured in a +logging.properties+ file. A default 77 | configuration is provided in a file within the gem or Toolbelt folder root, in the +java/+ folder. If you wish to 78 | customize it, please make a copy of this file and store it in the: 79 | 80 | ~/.td/java folder on Mac OSX or Linux 81 | %USERPROFILE%\.td\java folder on Windows 82 | 83 | == Testing Hooks 84 | 85 | The CLI implements several hooks to enable/disable/trigger special behaviors. 86 | These hooks are expressed as environment variables and can therefore be provided in several ways: 87 | 88 | === How to Use 89 | 90 | * Unix / Linux / MacOSX 91 | * environment variable export in the shell the command is executed. The setting remains active until the shell is closed. E.g.: 92 | 93 | $ export TD_TOOLBELT_DEBUG=1 94 | 95 | * in the shell configuration file, to be active in any new shell that is opened. E.g.: add 96 | 97 | export TD_TOOLBELT_DEBUG=1 98 | 99 | to ~/.bashrc or equivalent shell configuration file. 100 | To make the setting active in the current shell, source the configuration file, e.g.: 101 | 102 | $ source ~/.bashrc 103 | 104 | * on the command line at runtime (active only for the duration of the command). E.g.: 105 | 106 | $ TD_TOOLBELT_DEBUG=1 td .... 107 | 108 | * as alias on in the current shell. The setting remains active until the shell is closed. E.g.: 109 | 110 | $ alias td='TD_TOOLBELT_DEBUG=1 td' 111 | 112 | * as alias in configuration file, to be active in any new shell that is opened. E.g.: 113 | 114 | alias td='TD_TOOLBELT_DEBUG=1 td'` 115 | 116 | to ~/.bashrc or equivalent shell configuration file. 117 | To make the setting active in the current shell, source the configuration file, e.g.: 118 | 119 | $ source ~/.bashrc 120 | 121 | * Windows 122 | * in the command prompt the command is executed. The setting remains active until the command prompt window is closed. E.g.: 123 | 124 | cmd> set TD_TOOLBELT_DEBUG=1 125 | 126 | * as a global environment variable in the system settings. It will be active for all new command prompt windows. 127 | 128 | These are the available hooks: 129 | 130 | * Enable debugging mode: 131 | 132 | $ TD_TOOLBELT_DEBUG=1 133 | 134 | * JAR auto update (enabled by default is not specified). This setting does not affect import:jar_update: 135 | * Enable: 136 | 137 | $ TD_TOOLBELT_JAR_UPDATE=1 138 | 139 | * Disable: 140 | 141 | $ TD_TOOLBELT_JAR_UPDATE=0 142 | 143 | * Specify an alternative endpoint to use updating the toolbelt (default: http://toolbelt.treasuredata.com): 144 | 145 | $ TD_TOOLBELT_UPDATE_ROOT="http://toolbelt.treasuredata.com" 146 | 147 | * Specify an alternative endpoint to use updating the JAR file (default: https://repo1.maven.org): 148 | 149 | $ TD_TOOLBELT_JARUPDATE_ROOT="https://repo1.maven.org" 150 | 151 | 152 | = Copyright 153 | 154 | Copyright:: Copyright (c) 2015 Treasure Data Inc. 155 | License:: Apache License, Version 2.0 156 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler' 3 | require 'zip/zip' 4 | Bundler::GemHelper.install_tasks 5 | 6 | task :default => :build 7 | 8 | # common methods for package build scripts 9 | require 'fileutils' 10 | require "erb" 11 | 12 | def version 13 | require project_root_path('lib/td/version') 14 | TreasureData::TOOLBELT_VERSION 15 | end 16 | 17 | def project_root_path(path) 18 | "#{PROJECT_ROOT}/#{path}" 19 | end 20 | 21 | PROJECT_ROOT = File.expand_path(File.dirname(__FILE__)) 22 | USE_GEMS = ["#{PROJECT_ROOT}/pkg/td-#{version}.gem"] 23 | 24 | def install_use_gems(target_dir) 25 | unless ENV['GEM_HOME'].to_s.empty? 26 | puts "**" 27 | puts "** WARNING" 28 | puts "**" 29 | puts "** GEM_HOME is already set. Created package might be broken." 30 | puts "** RVM surely breaks the package. Use rbenv instead." 31 | puts "**" 32 | end 33 | 34 | require 'rubygems/gem_runner' 35 | require 'rubygems/rdoc' # avoid `Gem.finish_resolve` raises error 36 | 37 | # system(env, cmd) doesn't work with ruby 1.8 38 | ENV['GEM_HOME'] = target_dir 39 | ENV['GEM_PATH'] = '' 40 | USE_GEMS.each {|gem| 41 | begin 42 | # this is a hack to have the dependency handling for the 'td' gem 43 | # pick up a local gem for 'td-client' so as to be able to build 44 | # and test the 'toolbelt' package without publishing the 'td-client' 45 | # gem on rubygems.com 46 | unless ENV['TD_TOOLBELT_LOCAL_CLIENT_GEM'].nil? 47 | unless File.exists? ENV['TD_TOOLBELT_LOCAL_CLIENT_GEM'] 48 | raise "Cannot find gem file with path #{ENV['TD_TOOLBELT_LOCAL_CLIENT_GEM']}" 49 | end 50 | puts "Copy local gem #{ENV['TD_TOOLBELT_LOCAL_CLIENT_GEM']} to #{Dir.pwd}" 51 | FileUtils.cp File.expand_path(ENV['TD_TOOLBELT_LOCAL_CLIENT_GEM']), Dir.pwd 52 | end 53 | Gem::GemRunner.new.run ["install", gem, "--no-document"] # Use one of two commands bellow. Gem::GemRunner on ruby 3.0.5 has some weird bug 54 | # sh "gem install \"#{gem}" --no-document" 55 | # sh "/usr/local/td/ruby/bin/gem install \"#{gem}\" --no-document" # On OSX 56 | rescue Gem::SystemExitException => e 57 | unless e.exit_code.zero? 58 | puts e, e.backtrace.join("\n") 59 | raise e 60 | end 61 | end 62 | } 63 | FileUtils.mv Dir.glob("#{target_dir}/gems/*"), target_dir 64 | FileUtils.rm_f Dir.glob("#{target_dir}/*.gem") 65 | %W(bin cache doc gems specifications build_info).each { |dir| 66 | FileUtils.remove_dir("#{target_dir}/#{dir}", true) 67 | } 68 | end 69 | 70 | def resource_path(path) 71 | project_root_path("dist/resources/#{path}") 72 | end 73 | 74 | def install_resource(resource_name, target_path, mode) 75 | FileUtils.mkdir_p File.dirname(target_path) 76 | FileUtils.cp resource_path(resource_name), target_path 77 | File.chmod(mode, target_path) 78 | end 79 | 80 | def install_erb_resource(resource_name, target_path, mode, variables) 81 | FileUtils.mkdir_p File.dirname(target_path) 82 | erb_raw = File.read resource_path(resource_name) 83 | 84 | ctx = Object.new 85 | variables.each_pair {|k,v| 86 | # ctx.define_singleton_method(k) { v } doesn't work with ruby 1.8 87 | (class<subcommand' && ret=0 99 | 100 | if (( CURRENT == 1 )); then 101 | _describe -t subcommands "td subcommands" subcommands 102 | return 103 | fi 104 | 105 | local -a _subcommand_args 106 | case "$words[1]" in 107 | account) 108 | _subcommand_args=( 109 | '(-f|--force)'{-f,--force}'[overwrite current account setting]' \ 110 | ) 111 | ;; 112 | apikey:set) 113 | _subcommand_args=( 114 | '(-f|--force)'{-f,--force}'[overwrite current account setting]' \ 115 | ) 116 | ;; 117 | bulk_import:create) 118 | _subcommand_args=( 119 | '(-g|--org)'{-g,--org}'[create the bukl import session under this organization]' \ 120 | ) 121 | ;; 122 | bulk_import:upload_parts) 123 | _subcommand_args=( 124 | '(-P|--prefix)'{-P,--prefix}'[add prefix to parts name]' \ 125 | '(-s|--use-suffix)'{-s,--use-suffix}'[use COUNT number of . (dots) in the source file name to the parts name]' \ 126 | '--auto-perform[perform bulk import job automatically]' \ 127 | '--parallel[perform uploading in parallel (default: 2; max 8)]' \ 128 | ) 129 | ;; 130 | bulk_import:perform) 131 | _subcommand_args=( 132 | '(-w|--wait)'{-w,--wait}'[wait for finishing the job]' \ 133 | '(-f|--force)'{-f,--force}'[force start performing]' \ 134 | ) 135 | ;; 136 | bulk_import:prepare_parts) 137 | _subcommand_args=( 138 | '(-s|--split-size)'{-s,--split-size}'[size of each parts]' \ 139 | '(-o|--output)'{-o,--output}'[output directory]' \ 140 | ) 141 | ;; 142 | db:create) 143 | _subcommand_args=( 144 | '(-g|--org)'{-g,--org}'[create the database under this organization]' \ 145 | ) 146 | ;; 147 | db:delete) 148 | _subcommand_args=( 149 | '(-f|--force)'{-f,--force}'[clear tables and delete the database]' \ 150 | ) 151 | ;; 152 | job:list) 153 | _subcommand_args=( 154 | '(-p|--page)'{-p,--page}'[skip N pages]' \ 155 | '(-s|--skip)'{-s,--skip}'[skip N jobs]' \ 156 | '(-R|--running)'{-R,--running}'[show only running jobs]' \ 157 | '(-S|--success)'{-S,--success}'[show only succeeded jobs]' \ 158 | '(-E|--error)'{-E,--error}'[show only failed jobs]' \ 159 | '--show[show slow queries (default threshold: 3600 seconds)]' \ 160 | ) 161 | ;; 162 | job:show) 163 | _subcommand_args=( 164 | '(-v|--verbose)'{-v,--verbose}'[show logs]' \ 165 | '(-w|--wait)'{-w,--wait}'[wait for finishing the job]' \ 166 | '(-G|--vertical)'{-G,--vertical}'[use vertical table to show results]' \ 167 | '(-o|--output)'{-o,--output}'[write result to the file]' \ 168 | '(-f|--format)'{-f,--format}'[format of the result to write to the file (tsv, csv, json or msgpack)]' 169 | ) 170 | ;; 171 | query) 172 | _subcommand_args=( 173 | '(-g|--org)'{-g,--org}'[issue the query under this organization]' \ 174 | '(-d|--database)'{-d,--database}'[use the database (required)]' \ 175 | '(-w|--wait)'{-w,--wait}'[wait for finishing the job]' \ 176 | '(-G|--vertical)'{-G,--vertical}'[use vertical table to show results]' \ 177 | '(-o|--output)'{-o,--output}'[write result to the file]' \ 178 | '(-f|--format)'{-f,--format}'[format of the result to write to the file (tsv, csv, json or msgpack)]' 179 | '(-r|--result)'{-r,--result}'[write result to the URL (see also result:create subcommand)]' \ 180 | '(-u|--user)'{-u,--user}'[set user name for the result URL]' \ 181 | '(-p|--password)'{-p,--password}'[ask password for the result URL]' \ 182 | '(-P|--priority)'{-P,--priority}'[set priority]' \ 183 | '(-R|--retry)'{-R,--retry}'[automatic retrying count]' \ 184 | '(-q|--query)'{-q,--query}'[use file instead of inline query]' \ 185 | '--sampling[enable random sampling to reduce records 1/DENOMINATOR]' \ 186 | ) 187 | ;; 188 | result:create) 189 | _subcommand_args=( 190 | '(-g|--org)'{-g,--org}'[create the result under this organization]' \ 191 | '(-u|--user)'{-u,--user}'[set user name for authentication]' \ 192 | '(-p|--password)'{-p,--password}'[ask password for authentication]' \ 193 | ) 194 | ;; 195 | sched:create) 196 | _subcommand_args=( 197 | '(-g|--org)'{-g,--org}'[create the schedule under this organization]' \ 198 | '(-d|--database)'{-d,--database}'[use the database (required)]' \ 199 | '(-t|--timezone)'{-t,--timezone}'[name of the timezone (like Asia/Tokyo)]' \ 200 | '(-D|--delay)'{-D,--delay}'[delay time of the schedule]' \ 201 | '(-o|--output)'{-o,--output}'[write result to the file]' \ 202 | '(-r|--result)'{-r,--result}'[write result to the URL (see also result:create subcommand)]' \ 203 | '(-u|--user)'{-u,--user}'[set user name for the result URL]' \ 204 | '(-p|--password)'{-p,--password}'[ask password for the result URL]' \ 205 | '(-P|--priority)'{-P,--priority}'[set priority]' \ 206 | '(-R|--retry)'{-R,--retry}'[automatic retrying count]' \ 207 | ) 208 | ;; 209 | sched:update) 210 | _subcommand_args=( 211 | '(-s|--schedule)'{-s,--schedule}'[change the schedule]' \ 212 | '(-q|--query)'{-q,--query}'[change the query]' \ 213 | '(-d|--database)'{-d,--database}'[change the database]' \ 214 | '(-r|--result)'{-r,--result}'[change the result table]' \ 215 | '(-t|--timezone)'{-t,--timezone}'[change the name of the timezone]' \ 216 | '(-D|--delay)'{-D,--delay}'[change the delay time of the schedule]' \ 217 | '(-P|--priority)'{-P,--priority}'[set priority]' \ 218 | '(-R|--retry)'{-R,--retry}'[automatic retrying count]' \ 219 | ) 220 | ;; 221 | sched:histroy) 222 | _subcommand_args=( 223 | '(-p|--page)'{-p,--page}'[skip N pages]' \ 224 | '(-s|--skip)'{-s,--skip}'[skip N jobs]' \ 225 | ) 226 | ;; 227 | sched:run) 228 | _subcommand_args=( 229 | '(-n|--num)'{-n,--num}'[number of jobs to run]' \ 230 | ) 231 | ;; 232 | table:delete) 233 | _subcommand_args=( 234 | '(-f|--force)'{-f,--force}'[never prompt]' \ 235 | ) 236 | ;; 237 | table:list) 238 | _subcommand_args=( 239 | '(-n|--num_threads)'{-num,--num_threads}'[number of threads to get list in parallel]' \ 240 | '--show-bytes[show estimated table in bytes]' \ 241 | ) 242 | ;; 243 | table:tail) 244 | _subcommand_args=( 245 | '(-t|--to)'{-t,--to}'[end time of logs to get]' \ 246 | '(-f|--from)'{-f,--from}'[start time of logs to get]' \ 247 | '(-c|--count)'{-c,--count}'[number of logs to get]' \ 248 | '(-P|--pretty)'{-P,--pretty}'[pretty print]' \ 249 | ) 250 | ;; 251 | table:import) 252 | _subcommand_args=( 253 | '--format[file format (default: apache)]' \ 254 | '--apache[same as --format apache; apache common log format]' \ 255 | '--syslog[same as --format syslog; syslog]' \ 256 | '--msgpack[same as --format msgpack; msgpack stream format]' \ 257 | '--json[same as --format json; LF-separated json format]' \ 258 | '(-t|--time-key)'{-t,--time-key}'[time key name for json and msgpack format (e.g. created_at)]' \ 259 | "--auto-create-table[create table and database if doesn't exist]" \ 260 | ) 261 | ;; 262 | table:export) 263 | _subcommand_args=( 264 | '(-g|--org)'{-g,--org}'[export the data under this organization]' \ 265 | '(-t|--to)'{-t,--to}'[export data which is older than the TIME]' \ 266 | '(-f|--from)'{-f,--from}'[export data which is newer than or same with the TIME]' \ 267 | '(-b|--bucket)'{-b,--bucket}'[name of the destination S3 bucket (required)]' \ 268 | '(-k|--aws-key-id)'{-k,--aws-key-id}'[AWS access key id to export data (required)]' \ 269 | '(-s|--aws-secret-key)'{-s,--aws-secret-key}'[AWS secret key to export data (required)]' \ 270 | ) 271 | ;; 272 | esac 273 | 274 | _arguments \ 275 | $_subcommand_args \ 276 | && return 0 277 | } 278 | 279 | _td 280 | -------------------------------------------------------------------------------- /contrib/completion/td-completion.bash: -------------------------------------------------------------------------------- 1 | # bash completion for td commands 2 | 3 | _td() 4 | { 5 | COMP_WORDBREAKS=${COMP_WORDBREAKS//:} 6 | local cur="${COMP_WORDS[COMP_CWORD]}" 7 | local prev="${COMP_WORDS[COMP_CWORD - 1]}" 8 | local list="`td help:all | awk '{print $1}' | grep '^[a-z]' | xargs`" 9 | 10 | # echo "cur=$cur, prev=$prev" 11 | 12 | if [[ "$prev" == "td" ]]; then 13 | if [[ "$cur" == "" ]]; then 14 | COMPREPLY=($list) 15 | else 16 | COMPREPLY=($(compgen -W "$list" -- "$cur")) 17 | fi 18 | fi 19 | } 20 | complete -F _td td 21 | 22 | # Local variables: 23 | # # mode: shell-script 24 | # # sh-basic-offset: 4 25 | # # sh-indent-comment: t 26 | # # indent-tabs-mode: nil 27 | # # End: 28 | # # ex: ts=4 sw=4 et filetype=sh 29 | -------------------------------------------------------------------------------- /data/sample_apache_gen.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require 'json' 3 | require 'msgpack' 4 | require 'digest/md5' 5 | require 'cgi' 6 | require 'uuidtools' 7 | require 'fileutils' 8 | require 'zlib' 9 | require 'parallel' 10 | 11 | RECORDS = 5000 12 | HOSTS = RECORDS/4 13 | PAGES = RECORDS/4 14 | 15 | AGENT_LIST_STRING = < host.ip, 182 | 'user' => '-', 183 | 'method' => page.method, 184 | 'path' => page.path, 185 | 'code' => grand(10000) == 0 ? 500 : page.code, 186 | 'referer' => (grand(2) == 0 ? @pages[grand(@pages.size)].path : page.referer) || '-', 187 | 'size' => page.size, 188 | 'agent' => host.agent, 189 | } 190 | puts record.to_json 191 | #puts %[#{record['host']} - #{record['user']} [#{Time.at(now).strftime('%d/%b/%Y:%H:%M:%S %z')}] "#{record['method']} #{record['path']} HTTP/1.1" #{record['code']} #{record['size']} "#{record['referer']}" "#{record['agent']}"] 192 | end 193 | 194 | -------------------------------------------------------------------------------- /dist/build_osx.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -u 2 | set -e 3 | 4 | PACKAGER="/usr/local/td/ruby/bin" 5 | 6 | git pull 7 | ${PACKAGER}/gem install bundler rubyzip --no-document 8 | rbenv rehash 9 | ${PACKAGER}/bundle install 10 | ${PACKAGER}/rake pkg:clean 11 | ${PACKAGER}/rake pkg:build 12 | -------------------------------------------------------------------------------- /dist/build_win81.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash -u 2 | 3 | if [ $# -ne 1 ]; then 4 | echo "$0 TD_TOOLBELT_LOCAL_CLIENT_GEM" 1>&2 5 | exit 1 6 | fi 7 | 8 | bundle install 9 | rake exe:clean 10 | TD_TOOLBELT_LOCAL_CLIENT_GEM=$1 rake exe:build 11 | -------------------------------------------------------------------------------- /dist/exe.rake: -------------------------------------------------------------------------------- 1 | namespace 'exe' do 2 | desc "build Windows exe package" 3 | task 'build' => '^build' do 4 | create_build_dir('exe') do |dir| 5 | install_ruby_version = '3.0.5' 6 | # create ./installers/ 7 | FileUtils.mkdir_p "installers" 8 | installer_path = project_root_path("dist/resources/exe/rubyinstaller-#{install_ruby_version}-1-x64.exe") 9 | FileUtils.cp installer_path, "installers/rubyinstaller.exe" 10 | 11 | variables = { 12 | :version => version, 13 | :basename => "td-#{version}", 14 | :outdir => ".", 15 | :install_ruby_version => install_ruby_version, 16 | } 17 | 18 | # create ./td/ 19 | mkchdir("td") do 20 | mkchdir('vendor/gems') do 21 | install_use_gems(Dir.pwd) 22 | end 23 | install_resource 'exe/td', 'bin/td', 0755 24 | install_erb_resource 'exe/td.bat', 'bin/td.bat', 0755, variables 25 | install_resource 'exe/td-cmd.bat', 'td-cmd.bat', 0755 26 | end 27 | 28 | zip_files(project_root_path("pkg/td-update-exe-#{version}.zip"), 'td') 29 | 30 | # create td.iss and run Inno Setup 31 | install_erb_resource 'exe/td.iss', 'td.iss', 0644, variables 32 | 33 | inno_dir = ENV["INNO_DIR"] || 'C:/Program Files (x86)/Inno Setup 5' 34 | inno_bin = ENV["INNO_BIN"] || "#{inno_dir}/Compil32.exe" 35 | puts "INNO_BIN: #{inno_bin}" 36 | 37 | sh "\"#{inno_bin}\" /cc \"td.iss\"" 38 | FileUtils.cp "td-#{version}.exe", project_root_path("pkg/td-#{version}.exe") 39 | end 40 | end 41 | 42 | desc "clean Windows exe package" 43 | task "clean" do 44 | FileUtils.rm_rf build_dir_path('exe') 45 | FileUtils.rm_rf project_root_path("pkg/td-#{version}.exe") 46 | FileUtils.rm_rf project_root_path("pkg/td-update-exe-#{version}.zip") 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /dist/pkg.rake: -------------------------------------------------------------------------------- 1 | namespace 'pkg' do 2 | desc "build Mac OS X pkg package" 3 | task 'build' => '^build' do 4 | create_build_dir('pkg') do |dir| 5 | FileUtils.mkdir_p "bundle" 6 | FileUtils.mkdir_p "bundle/Resources" 7 | FileUtils.mkdir_p "bundle/td-client.pkg" 8 | 9 | # create ./bundle/td-client.pkg/Payload 10 | mkchdir('td-client.build') do 11 | mkchdir('vendor/gems') do 12 | install_use_gems(Dir.pwd) 13 | end 14 | install_resource 'pkg/td', 'bin/td', 0755 15 | sh "pax -wz -x cpio . > ../bundle/td-client.pkg/Payload" 16 | end 17 | 18 | zip_files(project_root_path("pkg/td-update-pkg-#{version}.zip"), 'td-client.build') 19 | 20 | # create ./bundle/td-client.pkg/Bom 21 | sh "mkbom -s td-client.build bundle/td-client.pkg/Bom" 22 | 23 | # create ./bundle/td-client.pkg/Scripts/ 24 | install_resource 'pkg/postinstall', 'bundle/td-client.pkg/Scripts/postinstall', 0755 25 | 26 | variables = { 27 | :version => version, 28 | :kbytes => `du -ks td-client.build | cut -f 1`.strip.to_i, 29 | :num_files => `find td-client.build | wc -l`, 30 | } 31 | 32 | # create ./bundle/td-client.pkg/PackageInfo 33 | install_erb_resource('pkg/PackageInfo.erb', 'bundle/td-client.pkg/PackageInfo', 0644, variables) 34 | 35 | # create ./bundle/Distribution 36 | install_erb_resource('pkg/Distribution.erb', 'bundle/Distribution', 0644, variables) 37 | 38 | ruby_version = '3.0.5' 39 | sh "pkgutil --expand #{project_root_path("dist/resources/pkg/ruby-#{ruby_version}.pkg")} ruby" 40 | mv "ruby/ruby-#{ruby_version}.pkg", "bundle/ruby.pkg" 41 | 42 | # create td-a.b.c.pkg 43 | sh "pkgutil --flatten bundle td-#{version}.pkg" 44 | FileUtils.cp "td-#{version}.pkg", project_root_path("pkg/td-#{version}.pkg") 45 | puts "Finished building pkg/td-#{version}.pkg" 46 | end 47 | end 48 | 49 | desc "clean Mac OS X pkg package" 50 | task "clean" do 51 | FileUtils.rm_rf build_dir_path('pkg') 52 | FileUtils.rm_rf project_root_path("pkg/td-#{version}.pkg") 53 | FileUtils.rm_rf project_root_path("pkg/td-update-pkg-#{version}.zip") 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /dist/resources/exe/td: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # -*- coding: utf-8 -*- 3 | 4 | # avoid conflicts with rvm 5 | ENV.delete 'GEM_HOME' 6 | ENV.delete 'GEM_PATH' 7 | 8 | # attempt to load rubygems 9 | begin 10 | require "rubygems" 11 | rescue LoadError 12 | end 13 | 14 | # resolve bin path, ignoring symlinks 15 | require "pathname" 16 | here = File.dirname(Pathname.new(__FILE__).realpath) 17 | 18 | # add locally installed gems to libpath 19 | gem_dir = File.expand_path("../vendor/gems", here) 20 | Dir["#{gem_dir}/**/lib"].each do |libdir| 21 | $:.unshift libdir 22 | end 23 | 24 | # inject any code in ~/.td/updated/vendor/gems over top 25 | require 'td/updater' 26 | TreasureData::Updater.inject_libpath 27 | 28 | # start up the CLI 29 | require 'td/command/runner' 30 | exit TreasureData::Command::Runner.new.run ARGV 31 | -------------------------------------------------------------------------------- /dist/resources/exe/td-cmd.bat: -------------------------------------------------------------------------------- 1 | @cd %USERPROFILE% 2 | @cmd /k td 3 | -------------------------------------------------------------------------------- /dist/resources/exe/td.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | :: determine if this is an NT operating system 4 | if not "%~f0" == "~f0" goto WinNT 5 | goto Win9x 6 | 7 | :Win9x 8 | @"%~dp0\..\ruby-<%= install_ruby_version %>\bin\ruby.exe" -Eutf-8 "td" %1 %2 %3 %4 %5 %6 %7 %8 %9 9 | goto :EOF 10 | 11 | :WinNT 12 | @"%~dp0\..\ruby-<%= install_ruby_version %>\bin\ruby.exe" -Eutf-8 "%~dpn0" %* 13 | goto :EOF 14 | -------------------------------------------------------------------------------- /dist/resources/exe/td.iss: -------------------------------------------------------------------------------- 1 | [Setup] 2 | AppName=Treasure Data 3 | AppVersion=<%= version %> 4 | DefaultDirName={pf}\Treasure Data 5 | DefaultGroupName=Treasure Data 6 | Compression=lzma2 7 | SolidCompression=yes 8 | OutputBaseFilename=<%= basename %> 9 | OutputDir=<%= outdir %> 10 | ChangesEnvironment=yes 11 | UsePreviousSetupType=no 12 | AlwaysShowComponentsList=no 13 | 14 | ; For Ruby expansion ~ 32MB (installed) - 12MB (installer) 15 | ExtraDiskSpaceRequired=20971520 16 | 17 | [Types] 18 | Name: client; Description: "Full Installation"; 19 | Name: custom; Description: "Custom Installation"; flags: iscustom 20 | 21 | [Components] 22 | Name: "toolbelt"; Description: "Treasure Data Toolbelt"; Types: "client custom" 23 | Name: "toolbelt/client"; Description: "Treasure Data Client"; Types: "client custom"; Flags: fixed 24 | 25 | [InstallDelete] 26 | Type: filesandordirs; Name: "{app}\vendor" 27 | 28 | [Files] 29 | Source: "td\*.*"; DestDir: "{app}"; Flags: recursesubdirs; Components: "toolbelt/client" 30 | Source: "installers\rubyinstaller.exe"; DestDir: "{tmp}"; Components: "toolbelt/client" 31 | 32 | [Icons] 33 | Name: "{group}\Treasure Data command prompt"; Filename: "{app}\td-cmd.bat" 34 | 35 | [Registry] 36 | Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"; ValueType: "expandsz"; ValueName: "Path"; \ 37 | ValueData: "{olddata};{app}\bin"; Check: NeedsAddPath(ExpandConstant('{app}\bin')) 38 | Root: HKCU; Subkey: "Environment"; ValueType: "expandsz"; ValueName: "HOME"; \ 39 | ValueData: "%USERPROFILE%"; Flags: createvalueifdoesntexist 40 | 41 | [Run] 42 | Filename: "{tmp}\rubyinstaller.exe"; Parameters: "/verysilent /noreboot /nocancel /noicons /dir=""{app}/ruby-<%= install_ruby_version %>"""; \ 43 | Flags: shellexec waituntilterminated; StatusMsg: "Installing Ruby"; Components: "toolbelt/client" 44 | ; Filename: "{app}\td-cmd.bat"; Description: "Run command prompt"; Flags: postinstall 45 | 46 | [Code] 47 | 48 | function NeedsAddPath(Param: string): boolean; 49 | var 50 | OrigPath: string; 51 | begin 52 | if not RegQueryStringValue(HKEY_LOCAL_MACHINE, 53 | 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', 54 | 'Path', OrigPath) 55 | then begin 56 | Result := True; 57 | exit; 58 | end; 59 | // look for the path with leading and trailing semicolon 60 | // Pos() returns 0 if not found 61 | Result := Pos(';' + Param + ';', ';' + OrigPath + ';') = 0; 62 | end; 63 | 64 | function IsProgramInstalled(Name: string): boolean; 65 | var 66 | ResultCode: integer; 67 | begin 68 | Result := Exec(Name, 'version', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); 69 | end; 70 | -------------------------------------------------------------------------------- /dist/resources/pkg/Distribution.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | TreasureData Client 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | #td-client.pkg 14 | #ruby.pkg 15 | 16 | -------------------------------------------------------------------------------- /dist/resources/pkg/PackageInfo.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /dist/resources/pkg/postinstall: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | mkdir -p /usr/local/bin 3 | ln -sf /usr/local/td/bin/td /usr/local/bin/td 4 | osascript -e ' 5 | tell application "Terminal" 6 | activate 7 | do script "td" 8 | end tell' 9 | -------------------------------------------------------------------------------- /dist/resources/pkg/td: -------------------------------------------------------------------------------- 1 | #!/usr/local/td/ruby/bin/ruby 2 | # -*- coding: utf-8 -*- 3 | 4 | # avoid conflicts with rvm 5 | ENV.delete 'GEM_HOME' 6 | ENV.delete 'GEM_PATH' 7 | 8 | # attempt to load rubygems 9 | begin 10 | require "rubygems" 11 | rescue LoadError 12 | end 13 | 14 | # resolve bin path, ignoring symlinks 15 | require "pathname" 16 | here = File.dirname(Pathname.new(__FILE__).realpath) 17 | 18 | # add locally installed gems to libpath 19 | gem_dir = File.expand_path("../vendor/gems", here) 20 | Dir["#{gem_dir}/**/lib"].each do |libdir| 21 | $:.unshift libdir 22 | end 23 | 24 | # inject any code in ~/.td/updated/vendor/gems over top 25 | require 'td/updater' 26 | TreasureData::Updater.inject_libpath 27 | 28 | # start up the CLI 29 | require 'td/command/runner' 30 | exit TreasureData::Command::Runner.new.run ARGV 31 | -------------------------------------------------------------------------------- /java/logging.properties: -------------------------------------------------------------------------------- 1 | ############################################################ 2 | # Treasure Data BulkImport Logging Configuration File 3 | # 4 | # You can use a different file by specifying a filename 5 | # with the java.util.logging.config.file system property. 6 | # For example java -Djava.util.logging.config.file=myfile 7 | ############################################################ 8 | 9 | ############################################################ 10 | # Global properties 11 | ############################################################ 12 | 13 | # "handlers" specifies a comma separated list of log Handler 14 | # classes. These handlers will be installed during VM startup. 15 | # Note that these classes must be on the system classpath. 16 | # By default we only configure a ConsoleHandler, which will only 17 | # show messages at the INFO and above levels. 18 | handlers= java.util.logging.FileHandler 19 | 20 | # To also add the FileHandler, use the following line instead. 21 | #handlers= java.util.logging.FileHandler, java.util.logging.ConsoleHandler 22 | 23 | # Default global logging level. 24 | # This specifies which kinds of events are logged across 25 | # all loggers. For any given facility this global level 26 | # can be overriden by a facility specific level 27 | # Note that the ConsoleHandler also has a separate level 28 | # setting to limit messages printed to the console. 29 | .level= INFO 30 | 31 | ############################################################ 32 | # Handler specific properties. 33 | # Describes specific configuration info for Handlers. 34 | ############################################################ 35 | 36 | java.util.logging.FileHandler.level = INFO 37 | java.util.logging.FileHandler.pattern=td-bulk-import.log 38 | java.util.logging.FileHandler.append=true 39 | java.util.logging.FileHandler.limit = 50000 40 | java.util.logging.FileHandler.count = 1 41 | java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter 42 | 43 | # Limit the message that are printed on the console to INFO and above. 44 | java.util.logging.ConsoleHandler.level = INFO 45 | java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter 46 | 47 | ############################################################ 48 | # Facility specific properties. 49 | # Provides extra control for each logger. 50 | ############################################################ 51 | 52 | # For example, set the com.xyz.foo logger to only log SEVERE 53 | # messages: 54 | com.xyz.foo.level = SEVERE 55 | -------------------------------------------------------------------------------- /lib/td.rb: -------------------------------------------------------------------------------- 1 | require 'td-logger' 2 | -------------------------------------------------------------------------------- /lib/td/command/account.rb: -------------------------------------------------------------------------------- 1 | require 'td/helpers' 2 | 3 | module TreasureData 4 | module Command 5 | 6 | def account(op) 7 | op.banner << "\noptions:\n" 8 | 9 | force = false 10 | op.on('-f', '--force', 'overwrite current account setting', TrueClass) {|b| 11 | force = true 12 | } 13 | 14 | user_name = op.cmd_parse 15 | 16 | endpoint = nil 17 | # user may be calling 'td account' with the -e / --endpoint 18 | # option, which we want to preserve and save 19 | begin 20 | endpoint = Config.endpoint 21 | rescue ConfigNotFoundError => e 22 | # the endpoint is neither stored in the config file 23 | # nor passed as option on the command line 24 | end 25 | 26 | conf = nil 27 | begin 28 | conf = Config.read 29 | rescue ConfigError 30 | end 31 | 32 | if conf && conf['account.apikey'] 33 | unless force 34 | if conf['account.user'] 35 | $stderr.puts "Account is already configured with '#{conf['account.user']}' account." 36 | else 37 | $stderr.puts "Account is already configured." 38 | end 39 | $stderr.puts "Add '-f' option to overwrite." 40 | exit 0 41 | end 42 | end 43 | 44 | $stdout.puts "Enter your Treasure Data credentials. For Google SSO user, please see https://docs.treasuredata.com/display/public/PD/Configuring+Authentication+for+TD+Using+the+TD+Toolbelt#ConfiguringAuthenticationforTDUsingtheTDToolbelt-SettingUpGoogleSSOUsers" 45 | unless user_name 46 | begin 47 | $stdout.print "Email: " 48 | line = STDIN.gets || "" 49 | user_name = line.strip 50 | rescue Interrupt 51 | $stderr.puts "\ncanceled." 52 | exit 1 53 | end 54 | end 55 | 56 | if user_name.empty? 57 | $stderr.puts "canceled." 58 | exit 0 59 | end 60 | 61 | client = nil 62 | 63 | 3.times do 64 | begin 65 | $stdout.print "Password (typing will be hidden): " 66 | password = get_password 67 | rescue Interrupt 68 | $stderr.print "\ncanceled." 69 | exit 1 70 | ensure 71 | system "stty echo" # TODO termios 72 | $stdout.print "\n" 73 | end 74 | 75 | if password.empty? 76 | $stderr.puts "canceled." 77 | exit 0 78 | end 79 | 80 | begin 81 | # enalbe SSL for the authentication 82 | opts = {} 83 | opts[:ssl] = true 84 | opts[:endpoint] = endpoint if endpoint 85 | client = Client.authenticate(user_name, password, opts) 86 | rescue TreasureData::AuthError 87 | $stderr.puts "User name or password mismatched." 88 | end 89 | 90 | break if client 91 | end 92 | return unless client 93 | 94 | $stdout.puts "Authenticated successfully." 95 | 96 | conf ||= Config.new 97 | conf["account.user"] = user_name 98 | conf["account.apikey"] = client.apikey 99 | conf['account.endpoint'] = endpoint if endpoint 100 | conf.save 101 | 102 | $stderr.puts "Use '#{$prog} " + Config.cl_options_string + "db:create ' to create a database." 103 | end 104 | 105 | def account_usage(op) 106 | op.cmd_parse 107 | 108 | client = get_client 109 | a = client.account 110 | 111 | $stderr.puts "Storage: #{a.storage_size_string}" 112 | end 113 | 114 | private 115 | if Helpers.on_windows? 116 | require 'fiddle' 117 | 118 | def get_char 119 | msvcrt = Fiddle.dlopen('msvcrt') 120 | getch = Fiddle::Function.new(msvcrt['_getch'], [], Fiddle::TYPE_CHAR) 121 | getch.call() 122 | rescue Exception 123 | crtdll = Fiddle.dlopen('crtdll') 124 | getch = Fiddle::Function.new(crtdll['_getch'], [], Fiddle::TYPE_CHAR) 125 | getch.call() 126 | end 127 | 128 | def get_password 129 | password = '' 130 | 131 | while c = get_char 132 | break if c == 13 || c == 10 # CR or NL 133 | if c == 127 || c == 8 # 128: backspace, 8: delete 134 | password.slice!(-1, 1) 135 | else 136 | password << c.chr 137 | end 138 | end 139 | 140 | password 141 | end 142 | else 143 | def get_password 144 | system "stty -echo" # TODO termios 145 | password = STDIN.gets || "" 146 | password[0..-2] # strip \n 147 | end 148 | end 149 | end 150 | end 151 | 152 | -------------------------------------------------------------------------------- /lib/td/command/apikey.rb: -------------------------------------------------------------------------------- 1 | 2 | module TreasureData 3 | module Command 4 | 5 | def apikey_show(op) 6 | if Config.apikey 7 | $stdout.puts Config.apikey 8 | return 9 | end 10 | 11 | conf = nil 12 | begin 13 | conf = Config.read 14 | rescue ConfigError 15 | end 16 | 17 | if !conf || !conf['account.apikey'] 18 | $stderr.puts "Account is not configured yet." 19 | $stderr.puts "Use '#{$prog} apikey:set' or '#{$prog} account' first." 20 | exit 1 21 | end 22 | 23 | $stdout.puts conf['account.apikey'] 24 | end 25 | 26 | def apikey_set(op) 27 | op.banner << "\noptions:\n" 28 | 29 | force = false 30 | op.on('-f', '--force', 'overwrite current account setting', TrueClass) {|b| 31 | force = true 32 | } 33 | 34 | apikey = op.cmd_parse 35 | 36 | conf = nil 37 | begin 38 | conf = Config.read 39 | rescue ConfigError 40 | end 41 | if conf && conf['account.apikey'] 42 | unless force 43 | if conf['account.user'] 44 | $stderr.puts "Account is already configured with '#{conf['account.user']}' account." 45 | else 46 | $stderr.puts "Account is already configured." 47 | end 48 | $stderr.puts "Add '-f' option to overwrite." 49 | exit 0 50 | end 51 | end 52 | 53 | conf ||= Config.new 54 | conf.delete("account.user") 55 | conf["account.apikey"] = apikey 56 | conf.save 57 | 58 | $stdout.puts "API key is set." 59 | $stdout.puts "Use '#{$prog} db:create ' to create a database." 60 | end 61 | 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/td/command/db.rb: -------------------------------------------------------------------------------- 1 | 2 | module TreasureData 3 | module Command 4 | 5 | def db_show(op) 6 | set_render_format_option(op) 7 | 8 | db_name = op.cmd_parse 9 | 10 | client = get_client 11 | 12 | db = get_database(client, db_name) 13 | 14 | rows = [] 15 | db.tables.each {|table| 16 | pschema = table.schema.fields.map {|f| 17 | "#{f.name}:#{f.type}" 18 | }.join(', ') 19 | rows << {:Table => table.name, :Type => table.type.to_s, :Count => table.count.to_s, :Schema=>pschema.to_s} 20 | } 21 | rows = rows.sort_by {|map| 22 | [map[:Type].size, map[:Table]] 23 | } 24 | 25 | $stdout.puts cmd_render_table(rows, :fields => [:Table, :Type, :Count, :Schema], :render_format => op.render_format) 26 | end 27 | 28 | def db_list(op) 29 | set_render_format_option(op) 30 | 31 | op.cmd_parse 32 | 33 | client = get_client 34 | dbs = client.databases 35 | 36 | rows = [] 37 | dbs.each {|db| 38 | rows << {:Name=>db.name, :Count=>db.count} 39 | } 40 | $stdout.puts cmd_render_table(rows, :fields => [:Name, :Count], :render_format => op.render_format) 41 | 42 | if dbs.empty? 43 | $stderr.puts "There are no databases." 44 | $stderr.puts "Use '#{$prog} " + Config.cl_options_string + "db:create ' to create a database." 45 | end 46 | end 47 | 48 | def db_create(op) 49 | db_name = op.cmd_parse 50 | 51 | API.validate_database_name(db_name) 52 | 53 | client = get_client 54 | 55 | opts = {} 56 | begin 57 | client.create_database(db_name, opts) 58 | rescue AlreadyExistsError 59 | $stderr.puts "Database '#{db_name}' already exists." 60 | exit 1 61 | end 62 | 63 | $stderr.puts "Database '#{db_name}' is created." 64 | $stderr.puts "Use '#{$prog} " + Config.cl_options_string + "table:create #{db_name} ' to create a table." 65 | end 66 | 67 | def db_delete(op) 68 | force = false 69 | op.on('-f', '--force', 'clear tables and delete the database', TrueClass) {|b| 70 | force = true 71 | } 72 | 73 | db_name = op.cmd_parse 74 | 75 | client = get_client 76 | 77 | begin 78 | db = client.database(db_name) 79 | 80 | if !force && !db.tables.empty? 81 | $stderr.puts "Database '#{db_name}' is not empty. Use '-f' option or drop tables first." 82 | exit 1 83 | end 84 | 85 | db.delete 86 | rescue NotFoundError 87 | $stderr.puts "Database '#{db_name}' does not exist." 88 | exit 1 89 | end 90 | 91 | $stderr.puts "Database '#{db_name}' is deleted." 92 | end 93 | 94 | end 95 | end 96 | 97 | -------------------------------------------------------------------------------- /lib/td/command/export.rb: -------------------------------------------------------------------------------- 1 | require 'td/command/job' 2 | 3 | module TreasureData 4 | module Command 5 | SUPPORTED_FORMATS = %W[json.gz line-json.gz tsv.gz jsonl.gz] 6 | SUPPORTED_ENCRYPT_METHOD = %W[s3] 7 | 8 | def export_result(op) 9 | wait = false 10 | priority = nil 11 | retry_limit = nil 12 | 13 | op.on('-w', '--wait', 'wait until the job is completed', TrueClass) {|b| 14 | wait = b 15 | } 16 | op.on('-P', '--priority PRIORITY', 'set priority') {|s| 17 | priority = job_priority_id_of(s) 18 | unless priority 19 | raise "unknown priority #{s.inspect} should be -2 (very-low), -1 (low), 0 (normal), 1 (high) or 2 (very-high)" 20 | end 21 | } 22 | op.on('-R', '--retry COUNT', 'automatic retrying count', Integer) {|i| 23 | retry_limit = i 24 | } 25 | 26 | target_job_id, result = op.cmd_parse 27 | 28 | client = get_ssl_client 29 | 30 | opts = { 31 | result: result, 32 | retry_limit: retry_limit, 33 | priority: priority, 34 | } 35 | if wait 36 | job = client.job(target_job_id) 37 | if !job.finished? 38 | $stderr.puts "target job #{target_job_id} is still running..." 39 | wait_job(job) 40 | end 41 | end 42 | 43 | job = client.result_export(target_job_id, opts) 44 | $stderr.puts "result export job #{job.job_id} is queued." 45 | $stderr.puts "Use '#{$prog} " + Config.cl_options_string + "job:show #{job.job_id}' to show the status." 46 | 47 | if wait 48 | wait_job(job) 49 | $stdout.puts "status : #{job.status}" 50 | end 51 | end 52 | 53 | def export_table(op) 54 | table_export(op) 55 | end 56 | 57 | def table_export(op) 58 | from = nil 59 | to = nil 60 | s3_bucket = nil 61 | wait = false 62 | aws_access_key_id = nil 63 | aws_secret_access_key = nil 64 | file_prefix = nil 65 | file_format = "json.gz" # default 66 | pool_name = nil 67 | encryption = nil 68 | assume_role = nil 69 | 70 | # Increase the summary_width value from default 32 into 36, to show the options help message properly. 71 | op.summary_width = 36 72 | 73 | op.on('-w', '--wait', 'wait until the job is completed', TrueClass) {|b| 74 | wait = b 75 | } 76 | op.on('-f', '--from TIME', 'export data which is newer than or same with the TIME') {|s| 77 | from = export_parse_time(s) 78 | } 79 | op.on('-t', '--to TIME', 'export data which is older than the TIME') {|s| 80 | to = export_parse_time(s) 81 | } 82 | op.on('-b', '--s3-bucket NAME', 'name of the destination S3 bucket (required)') {|s| 83 | s3_bucket = s 84 | } 85 | op.on('-p', '--prefix PATH', 'path prefix of the file on S3') {|s| 86 | file_prefix = s 87 | } 88 | op.on('-k', '--aws-key-id KEY_ID', 'AWS access key id to export data (required)') {|s| 89 | aws_access_key_id = s 90 | } 91 | op.on('-s', '--aws-secret-key SECRET_KEY', 'AWS secret access key to export data (required)') {|s| 92 | aws_secret_access_key = s 93 | } 94 | op.on('-F', '--file-format FILE_FORMAT', 95 | 'file format for exported data.', 96 | 'Available formats are tsv.gz (tab-separated values per line) and jsonl.gz (JSON record per line).', 97 | 'The json.gz and line-json.gz formats are default and still available but only for backward compatibility purpose;', 98 | ' use is discouraged because they have far lower performance.') { |s| 99 | raise ArgumentError, "#{s} is not a supported file format" unless SUPPORTED_FORMATS.include?(s) 100 | file_format = s 101 | } 102 | op.on('-O', '--pool-name NAME', 'specify resource pool by name') {|s| 103 | pool_name = s 104 | } 105 | op.on('-e', '--encryption ENCRYPT_METHOD', 'export with server side encryption with the ENCRYPT_METHOD') {|s| 106 | raise ArgumentError, "#{s} is not a supported encryption method" unless SUPPORTED_ENCRYPT_METHOD.include?(s) 107 | encryption = s 108 | } 109 | op.on('-a', '--assume-role ASSUME_ROLE_ARN', 'export with assume role with ASSUME_ROLE_ARN as role arn') {|s| 110 | assume_role = s 111 | } 112 | 113 | db_name, table_name = op.cmd_parse 114 | 115 | unless s3_bucket 116 | $stderr.puts "-b, --s3-bucket NAME option is required." 117 | exit 1 118 | end 119 | 120 | unless aws_access_key_id 121 | $stderr.puts "-k, --aws-key-id KEY_ID option is required." 122 | exit 1 123 | end 124 | 125 | unless aws_secret_access_key 126 | $stderr.puts "-s, --aws-secret-key SECRET_KEY option is required." 127 | exit 1 128 | end 129 | 130 | client = get_client 131 | 132 | get_table(client, db_name, table_name) 133 | 134 | client = get_ssl_client 135 | 136 | s3_opts = {} 137 | s3_opts['from'] = from.to_s if from 138 | s3_opts['to'] = to.to_s if to 139 | s3_opts['file_prefix'] = file_prefix if file_prefix 140 | s3_opts['file_format'] = file_format 141 | s3_opts['bucket'] = s3_bucket 142 | s3_opts['access_key_id'] = aws_access_key_id 143 | s3_opts['secret_access_key'] = aws_secret_access_key 144 | s3_opts['pool_name'] = pool_name if pool_name 145 | s3_opts['encryption'] = encryption if encryption 146 | s3_opts['assume_role'] = assume_role if assume_role 147 | 148 | job = client.export(db_name, table_name, "s3", s3_opts) 149 | 150 | $stderr.puts "Export job #{job.job_id} is queued." 151 | $stderr.puts "Use '#{$prog} " + Config.cl_options_string + "job:show #{job.job_id}' to show the status." 152 | 153 | if wait 154 | wait_job(job) 155 | $stdout.puts "Status : #{job.status}" 156 | end 157 | end 158 | 159 | private 160 | def export_parse_time(time) 161 | if time.to_i.to_s == time.to_s 162 | # UNIX time 163 | return time.to_i 164 | else 165 | require 'time' 166 | begin 167 | return Time.parse(time).to_i 168 | rescue 169 | $stderr.puts "invalid time format: #{time}" 170 | exit 1 171 | end 172 | end 173 | end 174 | 175 | end 176 | end 177 | 178 | -------------------------------------------------------------------------------- /lib/td/command/help.rb: -------------------------------------------------------------------------------- 1 | 2 | module TreasureData 3 | module Command 4 | 5 | def help(op) 6 | cmd = op.cmd_parse 7 | 8 | c = List.get_option(cmd) 9 | if c == nil 10 | $stderr.puts "'#{cmd}' is not a td command. Run '#{$prog}' to show the list." 11 | List.show_guess(cmd) 12 | exit 1 13 | 14 | elsif c.name != cmd && c.group == cmd 15 | # group command 16 | $stdout.puts List.cmd_usage(cmd) 17 | exit 1 18 | 19 | else 20 | method, cmd_req_connectivity = List.get_method(cmd) 21 | method.call(['--help']) 22 | end 23 | end 24 | 25 | def help_all(op) 26 | cmd = op.cmd_parse 27 | 28 | TreasureData::Command::List.show_help(op.summary_indent) 29 | $stdout.puts "" 30 | $stdout.puts "Type '#{$prog} help COMMAND' for more information on a specific command." 31 | end 32 | 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/td/command/options.rb: -------------------------------------------------------------------------------- 1 | module TreasureData 2 | module Command 3 | module Options 4 | 5 | def job_show_options(op) 6 | opts = {} 7 | opts[:verbose] = nil 8 | opts[:wait] = false 9 | opts[:output] = nil 10 | opts[:format] = nil 11 | opts[:render_opts] = {:header => false} 12 | opts[:limit] = nil 13 | opts[:exclude] = false 14 | 15 | op.on('-v', '--verbose', 'show logs', TrueClass) {|b| 16 | opts[:verbose] = b 17 | } 18 | op.on('-w', '--wait', 'wait for finishing the job', TrueClass) {|b| 19 | opts[:wait] = b 20 | } 21 | op.on('-G', '--vertical', 'use vertical table to show results', TrueClass) {|b| 22 | opts[:render_opts][:vertical] = b 23 | } 24 | op.on('-o', '--output PATH', 'write result to the file') {|s| 25 | unless Dir.exist?(File.dirname(s)) 26 | s = File.expand_path(s) 27 | end 28 | opts[:output] = s 29 | opts[:format] ||= 'tsv' 30 | } 31 | op.on('-l', '--limit ROWS', 'limit the number of result rows shown when not outputting to file') {|s| 32 | unless s.to_i > 0 33 | raise "Invalid limit number. Must be a positive integer" 34 | end 35 | opts[:limit] = s.to_i 36 | } 37 | op.on('-c', '--column-header', 'output of the columns\' header when the schema is available', 38 | ' for the table (only applies to tsv and csv formats)', TrueClass) {|b| 39 | opts[:render_opts][:header] = b; 40 | } 41 | op.on('-x', '--exclude', 'do not automatically retrieve the job result', TrueClass) {|b| 42 | opts[:exclude] = b 43 | } 44 | 45 | op.on('--null STRING', "null expression in csv or tsv") {|s| 46 | opts[:render_opts][:null_expr] = s.to_s 47 | } 48 | 49 | write_format_option(op) {|s| opts[:format] = s } 50 | 51 | # CAUTION: this opts is filled by op.cmd_parse 52 | opts 53 | end 54 | 55 | def write_format_option(op) 56 | op.on('-f', '--format FORMAT', 'format of the result to write to the file (tsv, csv, json, msgpack, and msgpack.gz)') {|s| 57 | unless ['tsv', 'csv', 'json', 'msgpack', 'msgpack.gz'].include?(s) 58 | raise "Unknown format #{s.dump}. Supported formats are: tsv, csv, json, msgpack, and msgpack.gz" 59 | end 60 | 61 | yield(s) 62 | } 63 | end 64 | module_function :write_format_option 65 | end # module Options 66 | end # module Command 67 | end # module TreasureData 68 | -------------------------------------------------------------------------------- /lib/td/command/password.rb: -------------------------------------------------------------------------------- 1 | 2 | module TreasureData 3 | module Command 4 | 5 | def password_change(op) 6 | op.cmd_parse 7 | 8 | old_password = nil 9 | password = nil 10 | 11 | begin 12 | system "stty -echo" # TODO termios 13 | $stdout.print "Old password (typing will be hidden): " 14 | old_password = STDIN.gets || "" 15 | old_password = old_password[0..-2] # strip \n 16 | rescue Interrupt 17 | $stderr.print "\ncanceled." 18 | exit 1 19 | ensure 20 | system "stty echo" # TODO termios 21 | $stdout.print "\n" 22 | end 23 | 24 | if old_password.empty? 25 | $stderr.puts "canceled." 26 | exit 0 27 | end 28 | 29 | 3.times do 30 | begin 31 | system "stty -echo" # TODO termios 32 | $stdout.print "New password (typing will be hidden): " 33 | password = STDIN.gets || "" 34 | password = password[0..-2] # strip \n 35 | rescue Interrupt 36 | $stderr.print "\ncanceled." 37 | exit 1 38 | ensure 39 | system "stty echo" # TODO termios 40 | $stdout.print "\n" 41 | end 42 | 43 | if password.empty? 44 | $stderr.puts "canceled." 45 | exit 0 46 | end 47 | 48 | begin 49 | system "stty -echo" # TODO termios 50 | $stdout.print "Retype new password: " 51 | password2 = STDIN.gets || "" 52 | password2 = password2[0..-2] # strip \n 53 | rescue Interrupt 54 | $stderr.print "\ncanceled." 55 | exit 1 56 | ensure 57 | system "stty echo" # TODO termios 58 | $stdout.print "\n" 59 | end 60 | 61 | if password == password2 62 | break 63 | end 64 | 65 | $stdout.puts "Doesn't match." 66 | end 67 | 68 | client = get_client(:ssl => true) 69 | 70 | client.change_my_password(old_password, password) 71 | 72 | $stderr.puts "Password changed." 73 | end 74 | 75 | end 76 | end 77 | 78 | -------------------------------------------------------------------------------- /lib/td/command/query.rb: -------------------------------------------------------------------------------- 1 | require 'td/command/job' 2 | require 'td/command/options' 3 | 4 | module TreasureData 5 | module Command 6 | 7 | def query(op) 8 | db_name = nil 9 | wait = false 10 | output = nil 11 | format = nil 12 | render_opts = {:header => false} 13 | result_url = nil 14 | result_user = nil 15 | result_ask_password = false 16 | priority = nil 17 | retry_limit = nil 18 | query = nil 19 | type = nil 20 | limit = nil 21 | exclude = false 22 | pool_name = nil 23 | domain_key = nil 24 | engine_version = nil 25 | 26 | op.on('-d', '--database DB_NAME', 'use the database (required)') {|s| 27 | db_name = s 28 | } 29 | op.on('-w', '--wait[=SECONDS]', 'wait for finishing the job (for seconds)', Integer) {|b| 30 | wait = b 31 | } 32 | op.on('-G', '--vertical', 'use vertical table to show results', TrueClass) {|b| 33 | render_opts[:vertical] = b 34 | } 35 | op.on('-o', '--output PATH', 'write result to the file') {|s| 36 | unless Dir.exist?(File.dirname(s)) 37 | s = File.expand_path(s) 38 | end 39 | output = s 40 | format = 'tsv' if format.nil? 41 | } 42 | 43 | TreasureData::Command::Options.write_format_option(op) {|s| format = s } 44 | 45 | op.on('-r', '--result RESULT_URL', 'write result to the URL (see also result:create subcommand)', 46 | ' It is suggested for this option to be used with the -x / --exclude option to suppress printing', 47 | ' of the query result to stdout or -o / --output to dump the query result into a file.') {|s| 48 | result_url = s 49 | } 50 | op.on('-u', '--user NAME', 'set user name for the result URL') {|s| 51 | result_user = s 52 | } 53 | op.on('-p', '--password', 'ask password for the result URL') {|s| 54 | result_ask_password = true 55 | } 56 | op.on('-P', '--priority PRIORITY', 'set priority') {|s| 57 | priority = job_priority_id_of(s) 58 | unless priority 59 | raise "unknown priority #{s.inspect} should be -2 (very-low), -1 (low), 0 (normal), 1 (high) or 2 (very-high)" 60 | end 61 | } 62 | op.on('-R', '--retry COUNT', 'automatic retrying count', Integer) {|i| 63 | retry_limit = i 64 | } 65 | op.on('-q', '--query PATH', 'use file instead of inline query') {|s| 66 | query = File.open(s) { |f| f.read.strip } 67 | } 68 | op.on('-T', '--type TYPE', 'set query type (hive, presto)') {|s| 69 | type = s.to_sym 70 | } 71 | op.on('--sampling DENOMINATOR', 'OBSOLETE - enable random sampling to reduce records 1/DENOMINATOR', Integer) {|i| 72 | $stdout.puts "WARNING: the random sampling feature enabled through the '--sampling' option was removed and does no longer" 73 | $stdout.puts " have any effect. It is left for backwards compatibility with older scripts using 'td'." 74 | $stdout.puts 75 | } 76 | op.on('-l', '--limit ROWS', 'limit the number of result rows shown when not outputting to file') {|s| 77 | unless s.to_i > 0 78 | raise "Invalid limit number. Must be a positive integer" 79 | end 80 | limit = s.to_i 81 | } 82 | op.on('-c', '--column-header', 'output of the columns\' header when the schema is available for the table (only applies to json, tsv and csv formats)', TrueClass) {|b| 83 | render_opts[:header] = b 84 | } 85 | op.on('-x', '--exclude', 'do not automatically retrieve the job result', TrueClass) {|b| 86 | exclude = b 87 | } 88 | op.on('-O', '--pool-name NAME', 'specify resource pool by name') {|s| 89 | pool_name = s 90 | } 91 | op.on('--domain-key DOMAIN_KEY', 'optional user-provided unique ID. You can include this ID with your `create` request to ensure idempotence') {|s| 92 | domain_key = s 93 | } 94 | op.on('--engine-version ENGINE_VERSION', 'EXPERIMENTAL: specify query engine version by name') {|s| 95 | engine_version = s 96 | } 97 | 98 | sql = op.cmd_parse 99 | 100 | # required parameters 101 | 102 | unless db_name 103 | raise ParameterConfigurationError, 104 | "-d / --database DB_NAME option is required." 105 | end 106 | 107 | if sql == '-' 108 | sql = STDIN.read 109 | elsif sql.nil? 110 | sql = query 111 | end 112 | unless sql 113 | raise ParameterConfigurationError, 114 | " argument or -q / --query PATH option is required." 115 | end 116 | 117 | # parameter concurrency validation 118 | 119 | if output.nil? && format 120 | unless ['tsv', 'csv', 'json'].include?(format) 121 | raise ParameterConfigurationError, 122 | "Supported formats are only tsv, csv and json without --output option" 123 | end 124 | end 125 | 126 | if render_opts[:header] 127 | unless ['tsv', 'csv', 'json'].include?(format) 128 | raise ParameterConfigurationError, 129 | "Option -c / --column-header is only supported with json, tsv and csv formats" 130 | end 131 | end 132 | 133 | if result_url 134 | require 'td/command/result' 135 | result_url = build_result_url(result_url, result_user, result_ask_password) 136 | end 137 | 138 | client = get_client 139 | 140 | # local existence check 141 | get_database(client, db_name) 142 | 143 | opts = {} 144 | opts['type'] = type if type 145 | opts['pool_name'] = pool_name if pool_name 146 | opts['domain_key'] = domain_key if domain_key 147 | opts['engine_version'] = engine_version if engine_version 148 | job = client.query(db_name, sql, result_url, priority, retry_limit, opts) 149 | 150 | $stdout.puts "Job #{job.job_id} is queued." 151 | $stdout.puts "Use '#{$prog} " + Config.cl_options_string + "job:show #{job.job_id}' to show the status." 152 | #puts "See #{job.url} to see the progress." 153 | 154 | if wait != false # `wait==nil` means `--wait` is specified 155 | wait_job(job, true, wait) 156 | $stdout.puts "Status : #{job.status}" 157 | if job.success? && !exclude 158 | begin 159 | show_result_with_retry(job, output, limit, format, render_opts) 160 | rescue TreasureData::NotFoundError => e 161 | end 162 | end 163 | elsif output 164 | $stdout.puts "The output file won't be generated without additional `-w` or `--wait` option." 165 | end 166 | end 167 | 168 | require 'td/command/job' # wait_job, job_priority_id_of 169 | end 170 | end 171 | -------------------------------------------------------------------------------- /lib/td/command/result.rb: -------------------------------------------------------------------------------- 1 | 2 | module TreasureData 3 | module Command 4 | 5 | def result_show(op) 6 | name = op.cmd_parse 7 | client = get_client 8 | 9 | rs = client.results 10 | r = rs.find {|r| name == r.name } 11 | 12 | unless r 13 | $stderr.puts "Result URL '#{name}' does not exist." 14 | $stderr.puts "Use '#{$prog} " + Config.cl_options_string + "result:create #{name} ' to create the URL." 15 | exit 1 16 | end 17 | 18 | $stdout.puts "Name : #{r.name}" 19 | $stdout.puts "URL : #{r.url}" 20 | end 21 | 22 | def result_list(op) 23 | set_render_format_option(op) 24 | 25 | op.cmd_parse 26 | 27 | client = get_client 28 | 29 | rs = client.results 30 | 31 | rows = [] 32 | rs.each {|r| 33 | rows << {:Name => r.name, :URL => r.url} 34 | } 35 | rows = rows.sort_by {|map| 36 | map[:Name] 37 | } 38 | 39 | $stdout.puts cmd_render_table(rows, :fields => [:Name, :URL], :render_format => op.render_format) 40 | 41 | if rs.empty? 42 | $stderr.puts "There are no result URLs." 43 | $stderr.puts "Use '#{$prog} " + Config.cl_options_string + "result:create ' to create a result URL." 44 | end 45 | end 46 | 47 | def result_create(op) 48 | result_user = nil 49 | result_ask_password = false 50 | 51 | op.on('-u', '--user NAME', 'set user name for authentication') {|s| 52 | result_user = s 53 | } 54 | op.on('-p', '--password', 'ask password for authentication') {|s| 55 | result_ask_password = true 56 | } 57 | 58 | name, url = op.cmd_parse 59 | API.validate_result_set_name(name) 60 | 61 | client = get_client 62 | 63 | url = build_result_url(url, result_user, result_ask_password) 64 | 65 | opts = {} 66 | begin 67 | client.create_result(name, url, opts) 68 | rescue AlreadyExistsError 69 | $stderr.puts "Result URL '#{name}' already exists." 70 | exit 1 71 | end 72 | 73 | $stderr.puts "Result URL '#{name}' is created." 74 | end 75 | 76 | def result_delete(op) 77 | name = op.cmd_parse 78 | 79 | client = get_client 80 | 81 | begin 82 | client.delete_result(name) 83 | rescue NotFoundError 84 | $stderr.puts "Result URL '#{name}' does not exist." 85 | exit 1 86 | end 87 | 88 | $stderr.puts "Result URL '#{name}' is deleted." 89 | end 90 | 91 | private 92 | def build_result_url(url, user, ask_password) 93 | if ask_password 94 | begin 95 | system "stty -echo" # TODO termios 96 | $stdout.print "Password (typing will be hidden): " 97 | password = STDIN.gets || "" 98 | password = password[0..-2] # strip \n 99 | rescue Interrupt 100 | $stderr.print "\ncanceled." 101 | exit 1 102 | ensure 103 | system "stty echo" # TODO termios 104 | $stdout.print "\n" 105 | end 106 | end 107 | 108 | ups = nil 109 | if user && password 110 | require 'cgi' 111 | ups = "#{CGI.escape(user)}:#{CGI.escape(password)}@" 112 | elsif user 113 | require 'cgi' 114 | ups = "#{CGI.escape(user)}@" 115 | elsif password 116 | require 'cgi' 117 | ups = ":#{CGI.escape(password)}@" 118 | end 119 | if ups 120 | url = url.sub(/\A([\w]+:(?:\/\/)?)/, "\\1#{ups}") 121 | end 122 | 123 | url 124 | end 125 | 126 | # DEPRECATED: relying on API server side validation which will return 127 | # immediately after query submission with error code 422. 128 | private 129 | def validate_td_result_url(url) 130 | re = /td:\/\/[^@]*@\/(.*)\/([^?]+)/ 131 | match = re.match(url) 132 | if match.nil? 133 | raise ParameterConfigurationError, "Treasure Data result output invalid URL format" 134 | end 135 | dbs = match[1] 136 | tbl = match[2] 137 | begin 138 | API.validate_name("Treasure Data result output destination database", 3, 256, dbs) 139 | API.validate_name("Treasure Data result output destination table", 3, 256, tbl) 140 | rescue ParameterValidationError => e 141 | raise ParameterConfigurationError, e 142 | end 143 | end 144 | end 145 | end 146 | 147 | -------------------------------------------------------------------------------- /lib/td/command/runner.rb: -------------------------------------------------------------------------------- 1 | module TreasureData 2 | module Command 3 | 4 | class Runner 5 | def initialize 6 | @config_path = nil 7 | @apikey = nil 8 | @endpoint = nil 9 | @import_endpoint = nil 10 | @prog_name = nil 11 | @insecure = false 12 | end 13 | 14 | attr_accessor :apikey, :endpoint, :import_endpoint, :config_path, :prog_name, :insecure 15 | 16 | def run(argv=ARGV) 17 | require 'td/version' 18 | require 'td/compat_core' 19 | require 'optparse' 20 | 21 | $prog = @prog_name || File.basename($0) 22 | 23 | op = OptionParser.new 24 | op.version = TOOLBELT_VERSION 25 | op.banner = < e 191 | # known exceptions are rendered as simple error messages unless the 192 | # TD_TOOLBELT_DEBUG variable is set or the -v / --verbose option is used. 193 | # List of known exceptions: 194 | # => ParameterConfigurationError 195 | # => BulkImportExecutionError 196 | # => UpUpdateError 197 | # => ImportError 198 | require 'td/client/api' 199 | # => APIError 200 | # => ForbiddenError 201 | # => NotFoundError 202 | # => AuthError 203 | if ![ParameterConfigurationError, BulkImportExecutionError, UpdateError, ImportError, 204 | APIError, ForbiddenError, NotFoundError, AuthError, AlreadyExistsError, WorkflowError].include?(e.class) || 205 | !ENV['TD_TOOLBELT_DEBUG'].nil? || $verbose 206 | show_backtrace "Error #{$!.class}: backtrace:", $!.backtrace 207 | end 208 | 209 | if $!.respond_to?(:api_backtrace) && $!.api_backtrace 210 | show_backtrace "Error backtrace from server:", $!.api_backtrace.split("\n") 211 | end 212 | 213 | $stdout.print "Error: " 214 | if [ForbiddenError, NotFoundError, AuthError].include?(e.class) 215 | $stdout.print "#{e.class} - " 216 | end 217 | $stdout.puts $!.to_s 218 | 219 | require 'socket' 220 | if e.is_a?(::SocketError) 221 | $stderr.puts < --json #{fname}' to import this file." 29 | end 30 | 31 | end 32 | end 33 | 34 | -------------------------------------------------------------------------------- /lib/td/command/schema.rb: -------------------------------------------------------------------------------- 1 | module TreasureData::Command 2 | 3 | def schema_show(op) 4 | db_name, table_name = op.cmd_parse 5 | 6 | client = get_client 7 | table = get_table(client, db_name, table_name) 8 | 9 | $stdout.puts "#{db_name}.#{table_name} (" 10 | table.schema.fields.each {|f| 11 | if f.sql_alias 12 | $stdout.puts " #{f.name}:#{f.type}@#{f.sql_alias}" 13 | else 14 | $stdout.puts " #{f.name}:#{f.type}" 15 | end 16 | } 17 | $stdout.puts ")" 18 | end 19 | 20 | def schema_set(op) 21 | db_name, table_name, *columns = op.cmd_parse 22 | 23 | client = get_client 24 | table = get_table(client, db_name, table_name) 25 | 26 | schema = TreasureData::Schema.parse(columns) 27 | client.update_schema(db_name, table_name, schema) 28 | 29 | $stderr.puts "Schema is updated on #{db_name}.#{table_name} table." 30 | end 31 | 32 | def schema_add(op) 33 | db_name, table_name, *columns = op.cmd_parse 34 | 35 | client = get_client 36 | table = get_table(client, db_name, table_name) 37 | 38 | schema = table.schema.merge(TreasureData::Schema.parse(columns)) 39 | client.update_schema(db_name, table_name, schema) 40 | 41 | $stderr.puts "Schema is updated on #{db_name}.#{table_name} table." 42 | end 43 | 44 | def schema_remove(op) 45 | db_name, table_name, *columns = op.cmd_parse 46 | 47 | client = get_client 48 | table = get_table(client, db_name, table_name) 49 | 50 | schema = table.schema 51 | 52 | columns.each {|col| 53 | unless schema.fields.reject!{|f| f.name == col } 54 | $stderr.puts "Column name '#{col}' does not exist." 55 | exit 1 56 | end 57 | } 58 | 59 | client.update_schema(db_name, table_name, schema) 60 | 61 | $stderr.puts "Schema is updated on #{db_name}.#{table_name} table." 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/td/command/server.rb: -------------------------------------------------------------------------------- 1 | require 'uri' 2 | 3 | module TreasureData 4 | module Command 5 | 6 | def server_status(op) 7 | op.cmd_parse 8 | 9 | $stdout.puts Client.server_status 10 | end 11 | 12 | def server_endpoint(op) 13 | endpoint = op.cmd_parse 14 | 15 | if Config.cl_endpoint and endpoint != Config.endpoint 16 | raise ParameterConfigurationError, 17 | "You specified the API server endpoint in the command options as well (-e / --endpoint " + 18 | "option) but it does not match the value provided to the 'server:endpoint' command. " + 19 | "Please remove the option or ensure the endpoints URLs match each other." 20 | end 21 | 22 | Command.validate_api_endpoint(endpoint) 23 | Command.test_api_endpoint(endpoint) 24 | 25 | conf = nil 26 | begin 27 | conf = Config.read 28 | rescue ConfigError 29 | conf = Config.new 30 | end 31 | conf["account.endpoint"] = endpoint 32 | conf.save 33 | end 34 | 35 | end 36 | end 37 | 38 | -------------------------------------------------------------------------------- /lib/td/command/status.rb: -------------------------------------------------------------------------------- 1 | 2 | module TreasureData 3 | module Command 4 | 5 | def status(op) 6 | op.cmd_parse 7 | 8 | client = get_client 9 | 10 | # +----------------+ 11 | # | scheds | 12 | # +----------------+ 13 | # +----------------+ 14 | # | jobs | 15 | # +----------------+ 16 | # +------+ +-------+ 17 | # |tables| |results| 18 | # +------+ +-------+ 19 | 20 | scheds = [] 21 | jobs = [] 22 | tables = [] 23 | results = [] 24 | 25 | s = client.schedules 26 | s.each {|sched| 27 | scheds << {:Name => sched.name, :Cron => sched.cron, :Result => sched.result_url, :Query => sched.query} 28 | } 29 | scheds = scheds.sort_by {|map| 30 | map[:Name] 31 | } 32 | x1, y1 = status_render(0, 0, "[Schedules]", scheds, :fields => [:Name, :Cron, :Result, :Query]) 33 | 34 | j = client.jobs(0, 4) 35 | j.each {|job| 36 | start = job.start_at 37 | elapsed = Command.humanize_elapsed_time(start, job.end_at) 38 | jobs << {:JobID => job.job_id, :Status => job.status, :Query => job.query.to_s, :Start => (start ? start.localtime : ''), :Elapsed => elapsed, :Result => job.result_url} 39 | } 40 | x2, y2 = status_render(0, 0, "[Jobs]", jobs, :fields => [:JobID, :Status, :Start, :Elapsed, :Result, :Query]) 41 | 42 | dbs = client.databases 43 | dbs.map {|db| 44 | db.tables.each {|table| 45 | tables << {:Database => db.name, :Table => table.name, :Count => table.count.to_s, :Size => table.estimated_storage_size_string} 46 | } 47 | } 48 | x3, y3 = status_render(0, 0, "[Tables]", tables, :fields => [:Database, :Table, :Count, :Size]) 49 | 50 | rs = client.results 51 | rs.each {|r| 52 | results << {:Name => r.name, :URL => r.url} 53 | } 54 | results = results.sort_by {|map| 55 | map[:Name] 56 | } 57 | x4, y4 = status_render(x3+2, y3, "[Results]", results, :fields => [:Name, :URL]) 58 | 59 | (y3-y4-1).times do 60 | $stdout.print "\eD" 61 | end 62 | $stdout.print "\eE" 63 | end 64 | 65 | private 66 | def status_render(movex, movey, msg, *args) 67 | lines = cmd_render_table(*args).split("\n") 68 | lines.pop # remove 'N rows in set' line 69 | lines.unshift(msg) 70 | #lines.unshift("") 71 | 72 | $stdout.print "\e[#{movey}A" if movey > 0 73 | 74 | max_width = 0 75 | height = 0 76 | lines.each {|line| 77 | $stdout.print "\e[#{movex}C" if movex > 0 78 | $stdout.puts line 79 | width = line.length 80 | max_width = width if max_width < width 81 | height += 1 82 | } 83 | 84 | return movex+max_width, height 85 | end 86 | 87 | end # module Command 88 | end # module TreasureData 89 | 90 | -------------------------------------------------------------------------------- /lib/td/command/update.rb: -------------------------------------------------------------------------------- 1 | require "td/updater" 2 | 3 | module TreasureData 4 | module Command 5 | 6 | def update(op) 7 | # for gem installation, this command is disallowed - 8 | # it only works for the toolbelt. 9 | if Updater.disable? 10 | $stderr.puts Updater.disable_message 11 | exit 12 | end 13 | 14 | start_time = Time.now 15 | $stdout.puts "Updating 'td' from #{TOOLBELT_VERSION}..." 16 | if new_version = Updater.update 17 | $stdout.puts "Successfully updated to #{new_version} in #{Command.humanize_time((Time.now - start_time).to_i)}." 18 | else 19 | $stdout.puts "Nothing to update." 20 | end 21 | end 22 | 23 | end # module Command 24 | end # module TreasureData 25 | 26 | -------------------------------------------------------------------------------- /lib/td/command/user.rb: -------------------------------------------------------------------------------- 1 | 2 | module TreasureData 3 | module Command 4 | 5 | def user_show(op) 6 | name = op.cmd_parse 7 | 8 | client = get_client 9 | 10 | users = client.users 11 | user = users.find {|user| name == user.name } 12 | unless user 13 | $stderr.puts "User '#{name}' does not exist." 14 | $stderr.puts "Use '#{$prog} " + Config.cl_options_string + "user:create ' to create an user." 15 | exit 1 16 | end 17 | 18 | $stderr.puts "Name : #{user.name}" 19 | $stderr.puts "Email : #{user.email}" 20 | end 21 | 22 | def user_list(op) 23 | set_render_format_option(op) 24 | 25 | op.cmd_parse 26 | 27 | client = get_client 28 | 29 | users = client.users 30 | 31 | rows = [] 32 | users.each {|user| 33 | rows << {:Name => user.name, :Email => user.email} 34 | } 35 | 36 | $stdout.puts cmd_render_table(rows, :fields => [:Name, :Email], :render_format => op.render_format) 37 | 38 | if rows.empty? 39 | $stderr.puts "There are no users." 40 | $stderr.puts "Use '#{$prog} " + Config.cl_options_string + "user:create ' to create an users." 41 | end 42 | end 43 | 44 | def user_create(op) 45 | email = nil 46 | random_password = false 47 | 48 | op.on('-e', '--email EMAIL', "Use this email address to identify the user") {|s| 49 | email = s 50 | } 51 | 52 | op.on('-R', '--random-password', "Generate random password", TrueClass) {|b| 53 | random_password = b 54 | } 55 | 56 | name = op.cmd_parse 57 | 58 | unless email 59 | $stderr.puts "-e, --email EMAIL option is required." 60 | exit 1 61 | end 62 | 63 | if random_password 64 | lower = ('a'..'z').to_a 65 | upper = ('A'..'Z').to_a 66 | digit = ('0'..'9').to_a 67 | symbol = %w[! # $ % - _ = + < >] 68 | 69 | r = [] 70 | 3.times { r << lower.sort_by{rand}.first } 71 | 3.times { r << upper.sort_by{rand}.first } 72 | 2.times { r << digit.sort_by{rand}.first } 73 | 1.times { r << symbol.sort_by{rand}.first } 74 | password = r.sort_by{rand}.join 75 | 76 | $stdout.puts "Password: #{password}" 77 | 78 | else 79 | 3.times do 80 | begin 81 | system "stty -echo" # TODO termios 82 | $stdout.print "Password (typing will be hidden): " 83 | password = STDIN.gets || "" 84 | password = password[0..-2] # strip \n 85 | rescue Interrupt 86 | $stderr.print "\ncanceled." 87 | exit 1 88 | ensure 89 | system "stty echo" # TODO termios 90 | $stdout.print "\n" 91 | end 92 | 93 | if password.empty? 94 | $stderr.puts "canceled." 95 | exit 0 96 | end 97 | 98 | begin 99 | system "stty -echo" # TODO termios 100 | $stdout.print "Retype password: " 101 | password2 = STDIN.gets || "" 102 | password2 = password2[0..-2] # strip \n 103 | rescue Interrupt 104 | $stderr.print "\ncanceled." 105 | exit 1 106 | ensure 107 | system "stty echo" # TODO termios 108 | $stdout.print "\n" 109 | end 110 | 111 | if password == password2 112 | break 113 | end 114 | 115 | $stdout.puts "Doesn't match." 116 | end 117 | end 118 | 119 | client = get_client(:ssl => true) 120 | client.add_user(name, nil, email, password) 121 | 122 | $stderr.puts "User '#{name}' is created." 123 | $stderr.puts "Use '#{$prog} " + Config.cl_options_string + "user:apikeys #{name}' to show the API key." 124 | end 125 | 126 | def user_delete(op) 127 | name = op.cmd_parse 128 | 129 | client = get_client 130 | 131 | client.remove_user(name) 132 | 133 | $stderr.puts "User '#{name}' is deleted." 134 | end 135 | 136 | ## TODO user:email:change 137 | #def user_email_change(op) 138 | #end 139 | 140 | def user_apikey_add(op) 141 | name = op.cmd_parse 142 | 143 | client = get_client 144 | 145 | begin 146 | client.add_apikey(name) 147 | rescue TreasureData::NotFoundError 148 | $stderr.puts "User '#{name}' does not exist." 149 | $stderr.puts "Use '#{$prog} " + Config.cl_options_string + "users' to show users." 150 | exit 1 151 | end 152 | 153 | $stderr.puts "Added an API key to user '#{name}'." 154 | $stderr.puts "Use '#{$prog} " + Config.cl_options_string + "user:apikeys #{name}' to show the API key" 155 | end 156 | 157 | def user_apikey_remove(op) 158 | name, key = op.cmd_parse 159 | 160 | client = get_client 161 | 162 | begin 163 | client.remove_apikey(name, key) 164 | rescue TreasureData::NotFoundError 165 | $stderr.puts "User '#{name}' or API key '#{key}' does not exist." 166 | $stderr.puts "Use '#{$prog} " + Config.cl_options_string + "users' to show users." 167 | $stderr.puts "Use '#{$prog} " + Config.cl_options_string + "user:apikeys '#{key}' to show API keys" 168 | exit 1 169 | end 170 | 171 | $stderr.puts "Removed an an API key from user '#{name}'." 172 | end 173 | 174 | def user_apikey_list(op) 175 | set_render_format_option(op) 176 | 177 | name = op.cmd_parse 178 | 179 | client = get_client 180 | 181 | keys = client.list_apikeys(name) 182 | 183 | rows = [] 184 | keys.each {|key| 185 | rows << {:Key => key} 186 | } 187 | 188 | $stdout.puts cmd_render_table(rows, :fields => [:Key], :render_format => op.render_format) 189 | end 190 | 191 | def user_password_change(op) 192 | name = op.cmd_parse 193 | 194 | password = nil 195 | 196 | 3.times do 197 | begin 198 | system "stty -echo" # TODO termios 199 | $stdout.print "New password (typing will be hidden): " 200 | password = STDIN.gets || "" 201 | password = password[0..-2] # strip \n 202 | rescue Interrupt 203 | $stderr.print "\ncanceled." 204 | exit 1 205 | ensure 206 | system "stty echo" # TODO termios 207 | $stdout.print "\n" 208 | end 209 | 210 | if password.empty? 211 | $stderr.puts "canceled." 212 | exit 0 213 | end 214 | 215 | begin 216 | system "stty -echo" # TODO termios 217 | $stdout.print "Retype new password: " 218 | password2 = STDIN.gets || "" 219 | password2 = password2[0..-2] # strip \n 220 | rescue Interrupt 221 | $stderr.print "\ncanceled." 222 | exit 1 223 | ensure 224 | system "stty echo" # TODO termios 225 | $stdout.print "\n" 226 | end 227 | 228 | if password == password2 229 | break 230 | end 231 | 232 | $stdout.puts "Doesn't match." 233 | end 234 | 235 | client = get_client(:ssl => true) 236 | 237 | client.change_password(name, password) 238 | 239 | $stderr.puts "Password of user '#{name}' changed." 240 | end 241 | 242 | end 243 | end 244 | 245 | -------------------------------------------------------------------------------- /lib/td/compact_format_yamler.rb: -------------------------------------------------------------------------------- 1 | require 'psych' 2 | 3 | module TreasureData 4 | module CompactFormatYamler 5 | module Visitors 6 | class YAMLTree < Psych::Visitors::YAMLTree 7 | # NOTE support 2.0 following 8 | unless self.respond_to? :create 9 | class << self 10 | alias :create :new 11 | end 12 | end 13 | 14 | def visit_Hash o 15 | if o.class == ::Hash && o.values.all? {|v| v.kind_of?(Numeric) || v.kind_of?(String) || v.kind_of?(Symbol) } 16 | register(o, @emitter.start_mapping(nil, nil, true, Psych::Nodes::Mapping::FLOW)) 17 | 18 | o.each do |k,v| 19 | accept k 20 | accept v 21 | end 22 | @emitter.end_mapping 23 | else 24 | super 25 | end 26 | end 27 | end 28 | end 29 | 30 | def self.dump(o, io = nil, options = {}) 31 | if Hash === io 32 | options = io 33 | io = nil 34 | end 35 | 36 | visitor = ::TreasureData::CompactFormatYamler::Visitors::YAMLTree.create options 37 | visitor << o 38 | visitor.tree.yaml io, options 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/td/compat_core.rb: -------------------------------------------------------------------------------- 1 | 2 | # File#size 3 | methods = File.public_instance_methods.map {|m| m.to_sym } 4 | if !methods.include?(:size) 5 | class File 6 | def size 7 | lstat.size 8 | end 9 | end 10 | end 11 | 12 | -------------------------------------------------------------------------------- /lib/td/compat_gzip_reader.rb: -------------------------------------------------------------------------------- 1 | 2 | methods = Zlib::GzipReader.public_instance_methods 3 | if !methods.include?(:readpartial) && !methods.include?('readpartial') 4 | class Zlib::GzipReader 5 | def readpartial(size, out=nil) 6 | o = read(size) 7 | if o 8 | if out 9 | out.replace(o) 10 | return out 11 | else 12 | return o 13 | end 14 | end 15 | raise EOFError, "end of file reached" 16 | end 17 | end 18 | end 19 | 20 | -------------------------------------------------------------------------------- /lib/td/config.rb: -------------------------------------------------------------------------------- 1 | 2 | module TreasureData 3 | 4 | 5 | class ConfigError < StandardError 6 | end 7 | 8 | class ConfigNotFoundError < ConfigError 9 | end 10 | 11 | class ConfigParseError < ConfigError 12 | end 13 | 14 | 15 | class Config 16 | # class variables 17 | @@path = ENV['TREASURE_DATA_CONFIG_PATH'] || ENV['TD_CONFIG_PATH'] || File.join(ENV['HOME'], '.td', 'td.conf') 18 | @@apikey = ENV['TREASURE_DATA_API_KEY'] || ENV['TD_API_KEY'] 19 | @@apikey = nil if @@apikey == "" 20 | @@cl_apikey = false # flag to indicate whether an apikey has been provided through the command-line 21 | @@endpoint = ENV['TREASURE_DATA_API_SERVER'] || ENV['TD_API_SERVER'] 22 | @@endpoint = nil if @@endpoint == "" 23 | @@cl_endpoint = false # flag to indicate whether an endpoint has been provided through the command-line 24 | @@import_endpoint = ENV['TREASURE_DATA_API_IMPORT_SERVER'] || ENV['TD_API_IMPORT_SERVER'] 25 | @@import_endpoint = nil if @@endpoint == "" 26 | @@cl_import_endpoint = false # flag to indicate whether an endpoint has been provided through the command-line option 27 | @@secure = true 28 | @@retry_post_requests = false 29 | 30 | def initialize 31 | @path = nil 32 | @conf = {} # section.key = val 33 | end 34 | 35 | def self.read(path=Config.path, create=false) 36 | new.read(path) 37 | end 38 | 39 | def [](cate_key) 40 | @conf[cate_key] 41 | end 42 | 43 | def []=(cate_key, val) 44 | @conf[cate_key] = val 45 | end 46 | 47 | def delete(cate_key) 48 | @conf.delete(cate_key) 49 | end 50 | 51 | def read(path=@path) 52 | @path = path 53 | begin 54 | data = File.read(@path) 55 | rescue 56 | e = ConfigNotFoundError.new($!.to_s) 57 | e.set_backtrace($!.backtrace) 58 | raise e 59 | end 60 | 61 | section = "" 62 | 63 | data.each_line {|line| 64 | line.strip! 65 | case line 66 | when /^#/ 67 | next 68 | when /\[(.+)\]/ 69 | section = $~[1] 70 | when /^(\w+)\s*=\s*(.+?)\s*$/ 71 | key = $~[1] 72 | val = $~[2] 73 | @conf["#{section}.#{key}"] = val 74 | else 75 | raise ConfigParseError, "invalid config line '#{line}' at #{@path}" 76 | end 77 | } 78 | 79 | self 80 | end 81 | 82 | def save(path=@path||Config.path) 83 | @path = path 84 | write 85 | end 86 | 87 | private 88 | def write 89 | require 'fileutils' 90 | FileUtils.mkdir_p File.dirname(@path) 91 | File.open(@path, "w") {|f| 92 | @conf.keys.map {|cate_key| 93 | cate_key.split('.', 2) 94 | }.zip(@conf.values).group_by {|(section,key), val| 95 | section 96 | }.each {|section,cate_key_vals| 97 | f.puts "[#{section}]" 98 | cate_key_vals.each {|(section,key), val| 99 | f.puts " #{key} = #{val}" 100 | } 101 | } 102 | } 103 | end 104 | 105 | 106 | def self.path 107 | @@path 108 | end 109 | 110 | def self.path=(path) 111 | @@path = path 112 | end 113 | 114 | 115 | def self.secure 116 | @@secure 117 | end 118 | 119 | def self.secure=(secure) 120 | @@secure = secure 121 | end 122 | 123 | 124 | def self.retry_post_requests 125 | @@retry_post_requests 126 | end 127 | 128 | def self.retry_post_requests=(retry_post_requests) 129 | @@retry_post_requests = retry_post_requests 130 | end 131 | 132 | 133 | def self.apikey 134 | @@apikey || Config.read['account.apikey'] 135 | end 136 | 137 | def self.apikey=(apikey) 138 | @@apikey = apikey 139 | end 140 | 141 | def self.cl_apikey 142 | @@cl_apikey 143 | end 144 | 145 | def self.cl_apikey=(flag) 146 | @@cl_apikey = flag 147 | end 148 | 149 | 150 | def self.endpoint 151 | endpoint = @@endpoint || Config.read['account.endpoint'] 152 | endpoint.sub(/(\/)+$/, '') if endpoint 153 | end 154 | 155 | def self.endpoint=(endpoint) 156 | @@endpoint = endpoint 157 | end 158 | 159 | def self.endpoint_domain 160 | (self.endpoint || 'api.treasuredata.com').sub(%r[https?://], '') 161 | end 162 | 163 | def self.cl_endpoint 164 | @@cl_endpoint 165 | end 166 | 167 | def self.cl_endpoint=(flag) 168 | @@cl_endpoint = flag 169 | end 170 | 171 | def self.import_endpoint 172 | endpoint = @@import_endpoint || Config.read['account.import_endpoint'] 173 | endpoint.sub(/(\/)+$/, '') if endpoint 174 | end 175 | 176 | def self.import_endpoint=(endpoint) 177 | @@import_endpoint = endpoint 178 | end 179 | 180 | def self.cl_import_endpoint 181 | @@cl_import_endpoint 182 | end 183 | 184 | def self.cl_import_endpoint=(flag) 185 | @@cl_import_endpoint = flag 186 | end 187 | 188 | def self.workflow_endpoint 189 | case self.endpoint_domain 190 | when /\Aapi(-(?:staging|development))?(-[a-z0-9]+)?\.(connect\.)?((?:eu01|ap02|ap03)\.)?treasuredata\.(com|co\.jp)\z/i 191 | "https://api#{$1}-workflow#{$2}.#{$3}#{$4}treasuredata.#{$5}" 192 | else 193 | raise ConfigError, "Workflow is not supported for '#{self.endpoint}'" 194 | end 195 | end 196 | 197 | # renders the apikey and endpoint options as a string for the helper commands 198 | def self.cl_options_string 199 | string = "" 200 | string += "-k #{@@apikey} " if @@cl_apikey 201 | string += "-e #{@@endpoint} " if @@cl_endpoint 202 | string += "--import-endpoint #{@@import_endpoint} " if @@cl_import_endpoint 203 | string 204 | end 205 | 206 | end # class Config 207 | end # module TreasureData 208 | -------------------------------------------------------------------------------- /lib/td/connector_config_normalizer.rb: -------------------------------------------------------------------------------- 1 | module TreasureData 2 | class ConnectorConfigNormalizer 3 | def initialize(config) 4 | @config = config 5 | end 6 | 7 | def normalized_config 8 | case 9 | when @config['in'] 10 | { 11 | 'in' => @config['in'], 12 | 'out' => @config['out'] || {}, 13 | 'exec' => @config['exec'] || {}, 14 | 'filters' => @config['filters'] || [] 15 | } 16 | when @config['config'] 17 | if @config.size != 1 18 | raise "Setting #{(@config.keys - ['config']).inspect} keys in a configuration file is not supported. Please set options to the command line argument." 19 | end 20 | 21 | self.class.new(@config['config']).normalized_config 22 | else 23 | { 24 | 'in' => @config, 25 | 'out' => {}, 26 | 'exec' => {}, 27 | 'filters' => [] 28 | } 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/td/distribution.rb: -------------------------------------------------------------------------------- 1 | module TreasureData 2 | module Distribution 3 | def self.files 4 | fs = Dir[File.expand_path("../../../{bin,data,lib}/**/*", __FILE__)].select do |file| 5 | File.file?(file) 6 | end 7 | fs << File.expand_path("../../../Gemfile", __FILE__) 8 | fs << File.expand_path("../../../td.gemspec", __FILE__) 9 | fs 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/td/helpers.rb: -------------------------------------------------------------------------------- 1 | module TreasureData 2 | module Helpers 3 | module_function 4 | 5 | def format_with_delimiter(number, delimiter = ',') 6 | number.to_s.gsub(/(\d)(?=(?:\d{3})+(?!\d))/, "\\1#{delimiter}") 7 | end 8 | 9 | def home_directory 10 | on_windows? ? ENV['USERPROFILE'].gsub("\\","/") : ENV['HOME'] 11 | end 12 | 13 | def on_windows? 14 | RUBY_PLATFORM =~ /mswin32|mingw32|mingw-ucrt/ 15 | end 16 | 17 | def on_mac? 18 | RUBY_PLATFORM =~ /-darwin\d/ 19 | end 20 | 21 | def on_64bit_os? 22 | if on_windows? 23 | if ENV.fetch('PROCESSOR_ARCHITECTURE', '').downcase.include? 'amd64' 24 | return true 25 | end 26 | return ENV.has_key?('PROCESSOR_ARCHITEW6432') 27 | else 28 | require 'open3' 29 | out, status = Open3.capture2('uname', '-m') 30 | raise 'Failed to detect OS bitness' unless status.success? 31 | return out.downcase.include? 'x86_64' 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/td/version.rb: -------------------------------------------------------------------------------- 1 | module TreasureData 2 | TOOLBELT_VERSION = '0.18.0' 3 | end 4 | -------------------------------------------------------------------------------- /spec/file_reader/filter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'file_reader/shared_context' 3 | 4 | require 'stringio' 5 | require 'td/file_reader' 6 | 7 | include TreasureData 8 | 9 | describe 'FileReader filters' do 10 | include_context 'error_proc' 11 | 12 | let :delimiter do 13 | "\t" 14 | end 15 | 16 | let :dataset do 17 | [ 18 | ['hoge', 12345, true, 'null', Time.now.to_s], 19 | ['foo', 34567, false, 'null', Time.now.to_s], 20 | ['piyo', 56789, true, nil, Time.now.to_s], 21 | ] 22 | end 23 | 24 | let :lines do 25 | dataset.map { |data| data.map(&:to_s).join(delimiter) } 26 | end 27 | 28 | let :parser do 29 | io = StringIO.new(lines.join("\n")) 30 | reader = FileReader::LineReader.new(io, error, {}) 31 | FileReader::DelimiterParser.new(reader, error, :delimiter_expr => delimiter) 32 | end 33 | 34 | describe FileReader::AutoTypeConvertParserFilter do 35 | let :options do 36 | { 37 | :null_expr => /\A(?:nil||\-|\\N)\z/i, 38 | :true_expr => /\A(?:true)\z/i, 39 | :false_expr => /\A(?:false)\z/i, 40 | } 41 | end 42 | 43 | it 'initialize' do 44 | filter = FileReader::AutoTypeConvertParserFilter.new(parser, error, options) 45 | expect(filter).not_to be_nil 46 | end 47 | 48 | context 'after initialization' do 49 | let :filter do 50 | FileReader::AutoTypeConvertParserFilter.new(parser, error, options) 51 | end 52 | 53 | it 'forward returns one converted line' do 54 | expect(filter.forward).to eq(dataset[0]) 55 | end 56 | 57 | it 'feeds all lines' do 58 | begin 59 | i = 0 60 | while line = filter.forward 61 | expect(line).to eq(dataset[i]) 62 | i += 1 63 | end 64 | rescue 65 | end 66 | end 67 | end 68 | end 69 | 70 | describe FileReader::HashBuilder do 71 | let :columns do 72 | ['str', 'num', 'bool', 'null', 'log_at'] 73 | end 74 | 75 | let :built_dataset do 76 | # [{"str" => "hoge", "num" => "12345", "bool" => "true" , "null" =>"null", "log_at" => "2012-12-26 05:14:09 +0900"}, ...] 77 | dataset.map { |data| Hash[columns.zip(data.map(&:to_s))]} 78 | end 79 | 80 | it 'initialize' do 81 | builder = FileReader::HashBuilder.new(parser, error, columns) 82 | expect(builder).not_to be_nil 83 | end 84 | 85 | context 'after initialization' do 86 | let :builder do 87 | FileReader::HashBuilder.new(parser, error, columns) 88 | end 89 | 90 | it 'forward returns one converted line' do 91 | expect(builder.forward).to eq(built_dataset[0]) 92 | end 93 | 94 | it 'feeds all lines' do 95 | begin 96 | i = 0 97 | while line = builder.forward 98 | expect(line).to eq(built_dataset[i]) 99 | i += 1 100 | end 101 | rescue 102 | end 103 | end 104 | 105 | describe FileReader::TimeParserFilter do 106 | it "can't be initialized without :time_column option" do 107 | expect { 108 | FileReader::TimeParserFilter.new(parser, error, {}) 109 | }.to raise_error(Exception, /--time-column/) 110 | end 111 | 112 | it 'initialize' do 113 | filter = FileReader::TimeParserFilter.new(builder, error, :time_column => 'log_at') 114 | expect(filter).not_to be_nil 115 | end 116 | 117 | context 'after initialization' do 118 | let :timed_dataset do 119 | require 'time' 120 | built_dataset.each { |data| data['time'] = Time.parse(data['log_at']).to_i } 121 | end 122 | 123 | let :filter do 124 | FileReader::TimeParserFilter.new(builder, error, :time_column => 'log_at') 125 | end 126 | 127 | it 'forward returns one parse line with parsed log_at' do 128 | expect(filter.forward).to eq(timed_dataset[0]) 129 | end 130 | 131 | it 'feeds all lines' do 132 | begin 133 | i = 0 134 | while line = filter.forward 135 | expect(line).to eq(timed_dataset[i]) 136 | i += 1 137 | end 138 | rescue 139 | end 140 | end 141 | 142 | context 'missing log_at column lines' do 143 | let :columns do 144 | ['str', 'num', 'bool', 'null', 'created_at'] 145 | end 146 | 147 | let :error_pattern do 148 | /^time column 'log_at' is missing/ 149 | end 150 | 151 | it 'feeds all lines' do 152 | i = 0 153 | begin 154 | while line = filter.forward 155 | i += 1 156 | end 157 | rescue RSpec::Expectations::ExpectationNotMetError => e 158 | fail 159 | rescue 160 | expect(i).to eq(0) 161 | end 162 | end 163 | end 164 | 165 | context 'invalid time format' do 166 | let :error_pattern do 167 | /^invalid time format/ 168 | end 169 | 170 | [{:time_column => 'log_at', :time_format => "%d"}, 171 | {:time_column => 'str'}].each { |options| 172 | let :filter do 173 | FileReader::TimeParserFilter.new(builder, error, options) 174 | end 175 | 176 | it 'feeds all lines' do 177 | i = 0 178 | begin 179 | while line = filter.forward 180 | i += 1 181 | end 182 | rescue RSpec::Expectations::ExpectationNotMetError => e 183 | fail 184 | rescue 185 | expect(i).to eq(0) 186 | end 187 | end 188 | } 189 | end 190 | end 191 | end 192 | 193 | describe FileReader::SetTimeParserFilter do 194 | it "can't be initialized without :time_value option" do 195 | expect { 196 | FileReader::SetTimeParserFilter.new(parser, error, {}) 197 | }.to raise_error(Exception, /--time-value/) 198 | end 199 | 200 | it 'initialize' do 201 | filter = FileReader::SetTimeParserFilter.new(builder, error, :time_value => Time.now.to_i) 202 | expect(filter).not_to be_nil 203 | end 204 | 205 | context 'after initialization' do 206 | let :time_value do 207 | Time.now.to_i 208 | end 209 | 210 | let :timed_dataset do 211 | built_dataset.each { |data| data['time'] = time_value } 212 | end 213 | 214 | let :filter do 215 | FileReader::SetTimeParserFilter.new(builder, error, :time_value => time_value) 216 | end 217 | 218 | it 'forward returns one converted line with time' do 219 | expect(filter.forward).to eq(timed_dataset[0]) 220 | end 221 | 222 | it 'feeds all lines' do 223 | begin 224 | i = 0 225 | while line = filter.forward 226 | expect(line).to eq(timed_dataset[i]) 227 | i += 1 228 | end 229 | rescue 230 | end 231 | end 232 | end 233 | end 234 | end 235 | end 236 | end 237 | -------------------------------------------------------------------------------- /spec/file_reader/io_filter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'file_reader/shared_context' 3 | 4 | require 'stringio' 5 | require 'td/file_reader' 6 | 7 | include TreasureData 8 | 9 | describe 'FileReader io filters' do 10 | include_context 'error_proc' 11 | 12 | describe FileReader::DecompressIOFilter do 13 | let :lines do 14 | [ 15 | '{"host":"128.216.140.97","user":"-","method":"GET","path":"/item/sports/2511","code":200,"size":95,"time":1353541928}', 16 | '{"host":"224.225.147.72","user":"-","method":"GET","path":"/category/electronics","code":200,"size":43,"time":1353541927}', 17 | '{"host":"172.75.186.56","user":"-","method":"GET","path":"/category/jewelry","code":200,"size":79,"time":1353541925}', 18 | ] 19 | end 20 | 21 | let :io do 22 | StringIO.new(lines.join("\n")) 23 | end 24 | 25 | let :gzipped_io do 26 | require 'zlib' 27 | 28 | io = StringIO.new('', 'w+') 29 | gz = Zlib::GzipWriter.new(io) 30 | gz.write(lines.join("\n")) 31 | gz.close 32 | StringIO.new(io.string) 33 | end 34 | 35 | it "can't filter with unknown compression" do 36 | expect { 37 | FileReader::DecompressIOFilter.filter(io, error, :compress => 'oreore') 38 | }.to raise_error(Exception, /unknown compression/) 39 | end 40 | 41 | describe 'gzip' do 42 | it "can't be wrapped with un-gzipped io" do 43 | expect { 44 | FileReader::DecompressIOFilter.filter(io, error, :compress => 'gzip') 45 | }.to raise_error(Zlib::GzipFile::Error) 46 | end 47 | 48 | it 'returns Zlib::GzipReader with :gzip' do 49 | wrapped = FileReader::DecompressIOFilter.filter(gzipped_io, error, :compress => 'gzip') 50 | expect(wrapped).to be_an_instance_of(Zlib::GzipReader) 51 | end 52 | 53 | it 'returns Zlib::GzipReader with auto detection' do 54 | wrapped = FileReader::DecompressIOFilter.filter(gzipped_io, error, {}) 55 | expect(wrapped).to be_an_instance_of(Zlib::GzipReader) 56 | end 57 | 58 | context 'after initialization' do 59 | [{:compress => 'gzip'}, {}].each { |opts| 60 | let :reader do 61 | wrapped = FileReader::DecompressIOFilter.filter(gzipped_io, error, opts) 62 | FileReader::LineReader.new(wrapped, error, {}) 63 | end 64 | 65 | it 'forward_row returns one line' do 66 | expect(reader.forward_row).to eq(lines[0]) 67 | end 68 | 69 | it 'feeds all lines' do 70 | begin 71 | i = 0 72 | while line = reader.forward_row 73 | expect(line).to eq(lines[i]) 74 | i += 1 75 | end 76 | rescue 77 | expect(gzipped_io.eof?).to be_truthy 78 | end 79 | end 80 | } 81 | end 82 | end 83 | 84 | describe 'plain' do 85 | it 'returns passed io with :plain' do 86 | wrapped = FileReader::DecompressIOFilter.filter(io, error, :compress => 'plain') 87 | expect(wrapped).to be_an_instance_of(StringIO) 88 | end 89 | 90 | it 'returns passed io with auto detection' do 91 | wrapped = FileReader::DecompressIOFilter.filter(io, error, {}) 92 | expect(wrapped).to be_an_instance_of(StringIO) 93 | end 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /spec/file_reader/line_reader_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'file_reader/shared_context' 3 | 4 | require 'stringio' 5 | require 'td/file_reader' 6 | 7 | include TreasureData 8 | 9 | describe FileReader::LineReader do 10 | include_context 'error_proc' 11 | 12 | let :lines do 13 | [ 14 | '{"host":"128.216.140.97","user":"-","method":"GET","path":"/item/sports/2511","code":200,"referer":"http://www.google.com/search?ie=UTF-8&q=google&sclient=psy-ab&q=Sports+Electronics&oq=Sports+Electronics&aq=f&aqi=g-vL1&aql=&pbx=1&bav=on.2,or.r_gc.r_pw.r_qf.,cf.osb&biw=3994&bih=421","size":95,"agent":"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.77 Safari/535.7","time":1353541928}', 15 | '{"host":"224.225.147.72","user":"-","method":"GET","path":"/category/electronics","code":200,"referer":"-","size":43,"agent":"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)","time":1353541927}', 16 | '{"host":"172.75.186.56","user":"-","method":"GET","path":"/category/jewelry","code":200,"referer":"-","size":79,"agent":"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)","time":1353541925}', 17 | ] 18 | end 19 | 20 | let :io do 21 | StringIO.new(lines.join("\n")) 22 | end 23 | 24 | it 'initialize' do 25 | if io.respond_to?(:external_encoding) 26 | ee = io.external_encoding 27 | end 28 | FileReader::LineReader.new(io, error, {}) 29 | if io.respond_to?(:external_encoding) 30 | expect(io.external_encoding).to eq(ee) 31 | end 32 | end 33 | 34 | it 'initialize with specifid encoding' do 35 | if io.respond_to?(:external_encoding) 36 | original_encoding = io.external_encoding 37 | end 38 | 39 | # when RUBY_VERSION >= 2.0, default encoding is utf-8. 40 | # ensure that external_encoding is differ from the original external_encoding(original_encoding). 41 | if original_encoding == Encoding.find('utf-8') 42 | specified_encoding = 'sjis' 43 | else 44 | specified_encoding = 'utf-8' 45 | end 46 | 47 | FileReader::LineReader.new(io, error, {:encoding => specified_encoding}) 48 | if io.respond_to?(:external_encoding) 49 | expect(io.external_encoding).not_to eq(original_encoding) 50 | expect(io.external_encoding).to eq(Encoding.find(specified_encoding)) 51 | end 52 | end 53 | 54 | context 'after initialization' do 55 | let :reader do 56 | FileReader::LineReader.new(io, error, {}) 57 | end 58 | 59 | it 'forward_row returns one line' do 60 | expect(reader.forward_row).to eq(lines[0]) 61 | end 62 | 63 | # TODO: integrate with following shared_examples_for 64 | it 'feeds all lines' do 65 | begin 66 | i = 0 67 | while line = reader.forward_row 68 | expect(line).to eq(lines[i]) 69 | i += 1 70 | end 71 | rescue RSpec::Expectations::ExpectationNotMetError => e 72 | fail 73 | rescue 74 | expect(io.eof?).to be_truthy 75 | end 76 | end 77 | 78 | shared_examples_for 'parser iterates all' do |step| 79 | step = step || 1 80 | 81 | it 'feeds all' do 82 | begin 83 | i = 0 84 | while line = parser.forward 85 | expect(line).to eq(get_expected.call(i)) 86 | i += step 87 | end 88 | rescue RSpec::Expectations::ExpectationNotMetError => e 89 | fail 90 | rescue 91 | expect(io.eof?).to be_truthy 92 | end 93 | end 94 | end 95 | 96 | describe FileReader::JSONParser do 97 | it 'initialize with LineReader' do 98 | parser = FileReader::JSONParser.new(reader, error, {}) 99 | expect(parser).not_to be_nil 100 | end 101 | 102 | context 'after initialization' do 103 | let :parser do 104 | FileReader::JSONParser.new(reader, error, {}) 105 | end 106 | 107 | it 'forward returns one line' do 108 | expect(parser.forward).to eq(JSON.parse(lines[0])) 109 | end 110 | 111 | let :get_expected do 112 | lambda { |i| JSON.parse(lines[i]) } 113 | end 114 | 115 | it_should_behave_like 'parser iterates all' 116 | 117 | context 'with broken line' do 118 | let :lines do 119 | [ 120 | '{"host":"128.216.140.97","user":"-","method":"GET","path":"/item/sports/2511","code":200,"referer":"http://www.google.com/search?ie=UTF-8&q=google&sclient=psy-ab&q=Sports+Electronics&oq=Sports+Electronics&aq=f&aqi=g-vL1&aql=&pbx=1&bav=on.2,or.r_gc.r_pw.r_qf.,cf.osb&biw=3994&bih=421","size":95,"agent":"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.77 Safari/535.7","time":1353541928}', 121 | '{This is invalid as a JSON}', 122 | '{"host":"172.75.186.56","user":"-","method":"GET","path":"/category/jewelry","code":200,"referer":"-","size":79,"agent":"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)","time":1353541925}', 123 | ] 124 | end 125 | 126 | let :error_pattern do 127 | /^invalid json format/ 128 | end 129 | 130 | it_should_behave_like 'parser iterates all', 2 131 | end 132 | end 133 | end 134 | 135 | [',', "\t"].each { |pattern| 136 | describe FileReader::DelimiterParser do 137 | let :lines do 138 | [ 139 | ['hoge', '12345', Time.now.to_s].join(pattern), 140 | ['foo', '34567', Time.now.to_s].join(pattern), 141 | ['piyo', '56789', Time.now.to_s].join(pattern), 142 | ] 143 | end 144 | 145 | it "initialize with LineReader and #{pattern} delimiter" do 146 | parser = FileReader::DelimiterParser.new(reader, error, {:delimiter_expr => Regexp.new(pattern)}) 147 | expect(parser).not_to be_nil 148 | end 149 | 150 | context 'after initialization' do 151 | let :parser do 152 | FileReader::DelimiterParser.new(reader, error, {:delimiter_expr => Regexp.new(pattern)}) 153 | end 154 | 155 | it 'forward returns one line' do 156 | expect(parser.forward).to eq(lines[0].split(pattern)) 157 | end 158 | 159 | let :get_expected do 160 | lambda { |i| lines[i].split(pattern) } 161 | end 162 | 163 | it_should_behave_like 'parser iterates all' 164 | end 165 | end 166 | } 167 | 168 | { 169 | FileReader::ApacheParser => [ 170 | [ 171 | '58.83.188.60 - - [23/Oct/2011:08:15:46 -0700] "HEAD / HTTP/1.0" 200 277 "-" "-"', 172 | '127.0.0.1 - - [23/Oct/2011:08:20:01 -0700] "GET / HTTP/1.0" 200 492 "-" "Wget/1.12 (linux-gnu)"', 173 | '68.64.37.100 - - [24/Oct/2011:01:48:54 -0700] "GET /phpMyAdmin/scripts/setup.php HTTP/1.1" 404 480 "-" "ZmEu"' 174 | ], 175 | [ 176 | ["58.83.188.60", "-", "23/Oct/2011:08:15:46 -0700", "HEAD", "/", "200", "277", "-", "-"], 177 | ["127.0.0.1", "-", "23/Oct/2011:08:20:01 -0700", "GET", "/", "200", "492", "-", "Wget/1.12 (linux-gnu)"], 178 | ["68.64.37.100", "-", "24/Oct/2011:01:48:54 -0700", "GET", "/phpMyAdmin/scripts/setup.php", "404", "480", "-", "ZmEu"], 179 | ] 180 | ], 181 | FileReader::SyslogParser => [ 182 | [ 183 | 'Dec 20 12:41:44 localhost kernel: [4843680.692840] e1000e: eth2 NIC Link is Down', 184 | 'Dec 20 12:41:44 localhost kernel: [4843680.734466] br0: port 1(eth2) entering disabled state', 185 | 'Dec 22 10:42:41 localhost kernel: [5009052.220155] zsh[25578]: segfault at 7fe849460260 ip 00007fe8474fd74d sp 00007fffe3bdf0e0 error 4 in libc-2.11.1.so[7fe847486000+17a000]', 186 | ], 187 | [ 188 | ["Dec 20 12:41:44", "localhost", "kernel", nil, "[4843680.692840] e1000e: eth2 NIC Link is Down"], 189 | ["Dec 20 12:41:44", "localhost", "kernel", nil, "[4843680.734466] br0: port 1(eth2) entering disabled state"], 190 | ["Dec 22 10:42:41", "localhost", "kernel", nil, "[5009052.220155] zsh[25578]: segfault at 7fe849460260 ip 00007fe8474fd74d sp 00007fffe3bdf0e0 error 4 in libc-2.11.1.so[7fe847486000+17a000]"], 191 | ] 192 | ] 193 | }.each_pair { |parser_class, (input, output)| 194 | describe parser_class do 195 | let :lines do 196 | input 197 | end 198 | 199 | it "initialize with LineReader" do 200 | parser = parser_class.new(reader, error, {}) 201 | expect(parser).not_to be_nil 202 | end 203 | 204 | context 'after initialization' do 205 | let :parser do 206 | parser_class.new(reader, error, {}) 207 | end 208 | 209 | it 'forward returns one line' do 210 | expect(parser.forward).to eq(output[0]) 211 | end 212 | 213 | let :get_expected do 214 | lambda { |i| output[i] } 215 | end 216 | 217 | it_should_behave_like 'parser iterates all' 218 | 219 | context 'with broken line' do 220 | let :lines do 221 | broken = input.dup 222 | broken[1] = "Raw text sometimes is broken!" 223 | broken 224 | end 225 | 226 | let :error_pattern do 227 | /^invalid #{parser.instance_variable_get(:@format)} format/ 228 | end 229 | 230 | it_should_behave_like 'parser iterates all', 2 231 | end 232 | end 233 | end 234 | } 235 | end 236 | end 237 | -------------------------------------------------------------------------------- /spec/file_reader/parsing_reader_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | require 'spec_helper' 3 | require 'file_reader/shared_context' 4 | 5 | require 'stringio' 6 | require 'msgpack' 7 | require 'td/file_reader' 8 | 9 | include TreasureData 10 | 11 | describe 'FileReader parsing readers' do 12 | include_context 'error_proc' 13 | 14 | shared_examples_for 'forward basics' do 15 | it 'forward returns one data' do 16 | expect(reader.forward).to eq(dataset[0]) 17 | end 18 | 19 | it 'feeds all dataset' do 20 | begin 21 | i = 0 22 | while line = reader.forward 23 | expect(line).to eq(dataset[i]) 24 | i += 1 25 | end 26 | rescue RSpec::Expectations::ExpectationNotMetError => e 27 | fail 28 | rescue => e 29 | expect(io.eof?).to be_truthy 30 | end 31 | end 32 | end 33 | 34 | describe FileReader::MessagePackParsingReader do 35 | let :dataset do 36 | [ 37 | {'name' => 'k', 'num' => 12345, 'time' => Time.now.to_i}, 38 | {'name' => 's', 'num' => 34567, 'time' => Time.now.to_i}, 39 | {'name' => 'n', 'num' => 56789, 'time' => Time.now.to_i}, 40 | ] 41 | end 42 | 43 | let :io do 44 | StringIO.new(dataset.map(&:to_msgpack).join("")) 45 | end 46 | 47 | it 'initialize' do 48 | reader = FileReader::MessagePackParsingReader.new(io, error, {}) 49 | expect(reader).not_to be_nil 50 | end 51 | 52 | context 'after initialization' do 53 | let :reader do 54 | FileReader::MessagePackParsingReader.new(io, error, {}) 55 | end 56 | 57 | it_should_behave_like 'forward basics' 58 | end 59 | end 60 | 61 | test_time = Time.now.to_i 62 | { 63 | 'csv' => [ 64 | {:delimiter_expr => ',', :quote_char => '"', :encoding => 'utf-8'}, 65 | [ 66 | %!k,123,"fo\no",true,#{test_time}!, 67 | %!s,456,"T,D",false,#{test_time}!, 68 | %!n,789,"ba""z",false,#{test_time}!, 69 | ], 70 | [ 71 | %W(k 123 fo\no true #{test_time}), 72 | %W(s 456 T,D false #{test_time}), 73 | %W(n 789 ba\"z false #{test_time}), 74 | ] 75 | ], 76 | 'tsv' => [ 77 | {:delimiter_expr => "\t"}, 78 | [ 79 | %!k\t123\t"fo\no"\ttrue\t#{test_time}!, 80 | %!s\t456\t"b,ar"\tfalse\t#{test_time}!, 81 | %!n\t789\t"ba\tz"\tfalse\t#{test_time}!, 82 | ], 83 | [ 84 | %W(k 123 fo\no true #{test_time}), 85 | %W(s 456 b,ar false #{test_time}), 86 | %W(n 789 ba\tz false #{test_time}), 87 | ] 88 | ] 89 | }.each_pair { |format, (opts, input, output)| 90 | describe FileReader::SeparatedValueParsingReader do 91 | let :dataset do 92 | output 93 | end 94 | 95 | let :lines do 96 | input 97 | end 98 | 99 | let :io do 100 | StringIO.new(lines.join($/)) 101 | end 102 | 103 | it "initialize #{format}" do 104 | reader = FileReader::SeparatedValueParsingReader.new(io, error, opts) 105 | expect(reader).not_to be_nil 106 | end 107 | 108 | context "after #{format} initialization" do 109 | let :reader do 110 | reader = FileReader::SeparatedValueParsingReader.new(io, error, opts) 111 | end 112 | 113 | it_should_behave_like 'forward basics' 114 | 115 | context "broken encodings" do 116 | end 117 | end 118 | end 119 | } 120 | end 121 | -------------------------------------------------------------------------------- /spec/file_reader/shared_context.rb: -------------------------------------------------------------------------------- 1 | require 'rspec' 2 | 3 | shared_context 'error_proc' do 4 | let :error do 5 | Proc.new { |reason, data| 6 | expect(reason).to match(error_pattern) 7 | } 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 3 | 4 | require 'rspec' 5 | require 'json' 6 | 7 | if ENV['SIMPLE_COV'] 8 | # SimpleCov 9 | # https://github.com/colszowka/simplecov 10 | require 'simplecov' 11 | SimpleCov.start do 12 | add_filter 'spec/' 13 | add_filter 'pkg/' 14 | add_filter 'vendor/' 15 | end 16 | end 17 | 18 | # XXX skip coverage setting if run appveyor. Because, fail to push coveralls in appveyor. 19 | unless ENV['APPVEYOR'] 20 | require 'coveralls' 21 | Coveralls.wear!('rails') 22 | end 23 | 24 | RSpec.configure do |config| 25 | # This allows you to limit a spec run to individual examples or groups 26 | # you care about by tagging them with `:focus` metadata. When nothing 27 | # is tagged with `:focus`, all examples get run. RSpec also provides 28 | # aliases for `it`, `describe`, and `context` that include `:focus` 29 | # metadata: `fit`, `fdescribe` and `fcontext`, respectively. 30 | config.filter_run_when_matching :focus 31 | end 32 | 33 | require 'td/command/runner' 34 | 35 | def execute_td(command_line) 36 | args = command_line.split(" ") 37 | original_stdin, original_stderr, original_stdout = $stdin, $stderr, $stdout 38 | 39 | $stdin = captured_stdin = StringIO.new 40 | $stderr = captured_stderr = StringIO.new 41 | $stdout = captured_stdout = StringIO.new 42 | class << captured_stdout 43 | def tty? 44 | true 45 | end 46 | end 47 | 48 | begin 49 | runner = TreasureData::Command::Runner.new 50 | $0 = 'td' 51 | runner.run(args) 52 | rescue SystemExit 53 | ensure 54 | $stdin, $stderr, $stdout = original_stdin, original_stderr, original_stdout 55 | end 56 | 57 | [captured_stderr.string, captured_stdout.string] 58 | end 59 | 60 | class CallSystemExitError < RuntimeError; end 61 | 62 | shared_context 'quiet_out' do 63 | let(:stdout_io) { StringIO.new } 64 | let(:stderr_io) { StringIO.new } 65 | 66 | around do |example| 67 | out = $stdout.dup 68 | err= $stdout.dup 69 | begin 70 | $stdout = stdout_io 71 | $stderr = stderr_io 72 | example.call 73 | ensure 74 | $stdout = out 75 | $stderr = err 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /spec/td/command/account_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | require "tempfile" 5 | require 'td/command/common' 6 | require 'td/command/account' 7 | require 'td/command/list' 8 | require 'td/helpers' 9 | 10 | module TreasureData::Command 11 | describe 'account command' do 12 | let :command do 13 | Class.new { include TreasureData::Command }.new 14 | end 15 | let(:client) { double(:client) } 16 | let(:conf_file) { Tempfile.new("td.conf").tap {|s| s.close } } 17 | let(:conf_expect) { 18 | """[account] 19 | user = test@email.com 20 | apikey = 1/xxx 21 | """ 22 | } 23 | 24 | before do 25 | TreasureData::Config.path = conf_file.path 26 | end 27 | 28 | it 'is called without any option' do 29 | expect(STDIN).to receive(:gets).and_return('test@email.com') 30 | if TreasureData::Helpers.on_windows? 31 | 'password'.chars.each { |c| expect(command).to receive(:get_char).and_return(c) } 32 | expect(command).to receive(:get_char).and_return(nil) 33 | else 34 | expect(STDIN).to receive(:gets).and_return('password') 35 | end 36 | expect(TreasureData::Client).to receive(:authenticate).and_return(client) 37 | expect(client).to receive(:apikey).and_return("1/xxx") 38 | 39 | op = List::CommandParser.new("account", %w[], %w[], false, [], true) 40 | command.account(op) 41 | 42 | expect(File.read(conf_file.path)).to eq(conf_expect) 43 | end 44 | 45 | it 'is called with -f option' do 46 | if TreasureData::Helpers.on_windows? 47 | 'password'.chars.each { |c| expect(command).to receive(:get_char).and_return(c) } 48 | expect(command).to receive(:get_char).and_return(nil) 49 | else 50 | expect(STDIN).to receive(:gets).and_return('password') 51 | end 52 | expect(TreasureData::Client).to receive(:authenticate).and_return(client) 53 | expect(client).to receive(:apikey).and_return("1/xxx") 54 | 55 | op = List::CommandParser.new("account", %w[-f], %w[], false, ['test@email.com'], true) 56 | command.account(op) 57 | 58 | expect(File.read(conf_file.path)).to eq(conf_expect) 59 | end 60 | 61 | it 'is called with username password mismatched' do 62 | if TreasureData::Helpers.on_windows? 63 | 3.times do 64 | 'password'.chars.each { |c| expect(command).to receive(:get_char).and_return(c) } 65 | expect(command).to receive(:get_char).and_return(nil) 66 | end 67 | else 68 | expect(STDIN).to receive(:gets).and_return('password').thrice 69 | end 70 | expect(TreasureData::Client).to receive(:authenticate).thrice.and_raise(TreasureData::AuthError) 71 | expect(STDERR).to receive(:puts).with('User name or password mismatched.').thrice 72 | 73 | op = List::CommandParser.new("account", %w[-f], %w[], false, ['test@email.com'], true) 74 | command.account(op) 75 | 76 | expect(File.read(conf_file.path)).to eq("") 77 | end 78 | end 79 | end -------------------------------------------------------------------------------- /spec/td/command/export_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'td/command/common' 3 | require 'td/command/list' 4 | require 'td/command/export' 5 | 6 | module TreasureData::Command 7 | describe 'export commands' do 8 | let(:command) { 9 | Class.new { include TreasureData::Command }.new 10 | } 11 | let(:stdout_io) { StringIO.new } 12 | let(:stderr_io) { StringIO.new } 13 | 14 | around do |example| 15 | stdout = $stdout.dup 16 | stderr = $stderr.dup 17 | 18 | begin 19 | $stdout = stdout_io 20 | $stderr = stderr_io 21 | 22 | example.run 23 | ensure 24 | $stdout = stdout 25 | $stderr = stderr 26 | end 27 | end 28 | 29 | describe '#table_export' do 30 | let(:option) { 31 | List::CommandParser.new("table:export", ["db_name", "table_name"], [], nil, option_list, true) 32 | } 33 | let(:option_with_encryption) { 34 | ops = option_list 35 | ops.push "-e" 36 | ops.push encryption 37 | List::CommandParser.new("table:export", ["db_name", "table_name"], [], nil, ops, true) 38 | } 39 | let(:option_with_wrong_encryption) { 40 | ops = option_list 41 | ops.push "-e" 42 | ops.push wrong_encryption 43 | List::CommandParser.new("table:export", ["db_name", "table_name"], [], nil, ops, true) 44 | } 45 | let(:option_with_assume_role) { 46 | ops = option_list 47 | ops.push "-a" 48 | ops.push assume_role 49 | List::CommandParser.new("table:export", ["db_name", "table_name"], [], nil, ops, true) 50 | } 51 | let(:option_list) { [database, table, "-b", bucket, "-p", path, "-k", key, "-s", pass, "-F", format] } 52 | let(:database) { 'database' } 53 | let(:table) { 'table' } 54 | let(:bucket) { 'bucket' } 55 | let(:path) { 'path' } 56 | let(:key) { 'key' } 57 | let(:pass) { 'pass' } 58 | let(:format) { 'tsv.gz' } 59 | let(:encryption) { 's3' } 60 | let(:assume_role) { 'arn:aws:iam::000:role/assume' } 61 | let(:wrong_encryption) { 's3s3' } 62 | let(:job_id) { 111 } 63 | 64 | before do 65 | client = double(:client) 66 | job = double(:job, job_id: job_id) 67 | allow(client).to receive(:export).and_return(job) 68 | table = double(:table) 69 | 70 | allow(command).to receive(:get_client).and_return(client) 71 | allow(command).to receive(:get_table).and_return(table) 72 | end 73 | 74 | it 'export table without encryption' do 75 | expect { 76 | command.table_export(option) 77 | }.to_not raise_exception 78 | end 79 | 80 | it 'export table with encryption' do 81 | expect { 82 | command.table_export(option_with_encryption) 83 | }.to_not raise_exception 84 | end 85 | 86 | it 'fail to export table with wrong encryption' do 87 | expect { 88 | command.table_export(option_with_wrong_encryption) 89 | }.to raise_exception 90 | end 91 | 92 | it 'export table without assume role' do 93 | expect { 94 | command.table_export(option_with_assume_role) 95 | }.to_not raise_exception 96 | end 97 | 98 | it 'export table with assume role' do 99 | expect { 100 | command.table_export(option_with_assume_role) 101 | }.to_not raise_exception 102 | end 103 | end 104 | 105 | describe '#export_table' do 106 | let(:option) { 107 | List::CommandParser.new("export:table", ["db_name", "table_name"], [], nil, option_list, true) 108 | } 109 | let(:option_list) { [database, table, "-b", bucket, "-p", path, "-k", key, "-s", pass, "-F", format] } 110 | let(:database) { 'database' } 111 | let(:table) { 'table' } 112 | let(:bucket) { 'bucket' } 113 | let(:path) { 'path' } 114 | let(:key) { 'key' } 115 | let(:pass) { 'pass' } 116 | let(:format) { 'tsv.gz' } 117 | let(:job_id) { 111 } 118 | 119 | before do 120 | client = double(:client) 121 | job = double(:job, job_id: job_id) 122 | allow(client).to receive(:export).and_return(job) 123 | table = double(:table) 124 | 125 | allow(command).to receive(:get_client).and_return(client) 126 | allow(command).to receive(:get_table).and_return(table) 127 | end 128 | 129 | it 'export table successfully like #table_export' do 130 | expect { 131 | command.table_export(option) 132 | }.to_not raise_exception 133 | end 134 | end 135 | 136 | describe '#export_result' do 137 | let(:option) { 138 | List::CommandParser.new("export:result", ["target_job_id", "result_url"], [], nil, option_list, true) 139 | } 140 | let(:option_with_retry) { 141 | List::CommandParser.new("export:result", ["target_job_id", "result_url"], [], nil, ['-R', '3'] + option_list, true) 142 | } 143 | let(:option_with_priority) { 144 | List::CommandParser.new("export:result", ["target_job_id", "result_url"], [], nil, ['-P', '-2'] + option_list, true) 145 | } 146 | let(:option_with_wrong_priority) { 147 | List::CommandParser.new("export:result", ["target_job_id", "result_url"], [], nil, ['-P', '3'] + option_list, true) 148 | } 149 | let(:option_with_wait) { 150 | List::CommandParser.new("export:result", ["target_job_id", "result_url"], [], nil, ['-w'] + option_list, true) 151 | } 152 | let(:option_list) { [110, 'mysql://user:pass@host.com/database/table'] } 153 | let(:job_id) { 111 } 154 | let(:client) { double(:client) } 155 | let(:job) { double(:job, job_id: job_id) } 156 | 157 | before do 158 | allow(client).to receive(:result_export).and_return(job) 159 | 160 | allow(command).to receive(:get_client).and_return(client) 161 | end 162 | 163 | it 'export result successfully' do 164 | expect { 165 | command.export_result(option) 166 | }.not_to raise_exception 167 | end 168 | 169 | it 'works with retry option' do 170 | expect { 171 | command.export_result(option_with_retry) 172 | }.not_to raise_exception 173 | end 174 | 175 | it 'works with priority option' do 176 | expect { 177 | command.export_result(option_with_priority) 178 | }.not_to raise_exception 179 | end 180 | 181 | it 'detects wrong priority option' do 182 | expect { 183 | command.export_result(option_with_wrong_priority) 184 | }.to raise_exception 185 | end 186 | 187 | it 'detects wait option' do 188 | target_job = double('target_job') 189 | expect(client).to receive(:job).and_return(target_job) 190 | count_target_job_finished_p = 0 191 | expect(target_job).to receive(:finished?).and_return(false) 192 | allow(target_job).to receive(:wait) 193 | allow(target_job).to receive(:status) 194 | allow(job).to receive(:wait) 195 | allow(job).to receive(:status) 196 | command.export_result(option_with_wait) 197 | end 198 | end 199 | end 200 | end 201 | -------------------------------------------------------------------------------- /spec/td/command/import_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'td/command/common' 3 | require 'td/command/list' 4 | require 'td/command/import' 5 | 6 | module TreasureData::Command 7 | describe 'import commands' do 8 | let(:command) { 9 | Class.new { include TreasureData::Command }.new 10 | } 11 | let(:stdout_io) { StringIO.new } 12 | let(:stderr_io) { StringIO.new } 13 | 14 | around do |example| 15 | stdout = $stdout.dup 16 | stderr = $stderr.dup 17 | 18 | begin 19 | $stdout = stdout_io 20 | $stderr = stderr_io 21 | 22 | example.run 23 | ensure 24 | $stdout = stdout 25 | $stderr = stderr 26 | end 27 | end 28 | 29 | describe '#import_list' do 30 | let(:option) { 31 | List::CommandParser.new("import:list", [], [], nil, [], true) 32 | } 33 | let(:database) { 'database' } 34 | let(:table) { 'table' } 35 | let(:response) { 36 | [ 37 | double(:bulk_import, 38 | name: 'bulk_import', 39 | database: database, 40 | table: table, 41 | status: :committed, 42 | upload_frozen?: true, 43 | job_id: '123456', 44 | valid_parts: '', 45 | error_parts: '', 46 | valid_records: '', 47 | error_records: '', 48 | ) 49 | ] 50 | } 51 | 52 | before do 53 | client = double(:client, bulk_imports: response) 54 | allow(command).to receive(:get_client).and_return(client) 55 | 56 | command.import_list(option) 57 | end 58 | 59 | it 'show import list' do 60 | expect(stdout_io.string).to include [database, table].join('.') 61 | end 62 | end 63 | 64 | describe '#import_list' do 65 | let(:option) { 66 | List::CommandParser.new("import:list", [], [], nil, [], true) 67 | } 68 | let(:database) { 'database' } 69 | let(:table) { 'table' } 70 | let(:response) { 71 | [ 72 | double(:bulk_import, 73 | name: 'bulk_import', 74 | database: database, 75 | table: table, 76 | status: :committed, 77 | upload_frozen?: true, 78 | job_id: '123456', 79 | valid_parts: '', 80 | error_parts: '', 81 | valid_records: '', 82 | error_records: '', 83 | ) 84 | ] 85 | } 86 | 87 | before do 88 | client = double(:client, bulk_imports: response) 89 | allow(command).to receive(:get_client).and_return(client) 90 | 91 | command.import_list(option) 92 | end 93 | 94 | it 'show import list' do 95 | expect(stdout_io.string).to include [database, table].join('.') 96 | end 97 | end 98 | 99 | describe '#import_show' do 100 | let(:import_name) { 'bulk_import' } 101 | let(:option) { 102 | List::CommandParser.new("import:show", ['name'], [], nil, [import_name], true) 103 | } 104 | let(:client) { double(:client) } 105 | 106 | before do 107 | allow(command).to receive(:get_client).and_return(client) 108 | end 109 | 110 | context 'not exists import' do 111 | it 'should be error' do 112 | expect(client).to receive(:bulk_import).with(import_name).and_return(nil) 113 | expect(command).to receive(:exit).with(1) { raise CallSystemExitError } 114 | 115 | expect { 116 | command.import_show(option) 117 | }.to raise_error CallSystemExitError 118 | end 119 | end 120 | 121 | context 'exist import' do 122 | let(:import_name) { 'bulk_import' } 123 | let(:bulk_import) { 124 | double(:bulk_import, 125 | name: import_name, 126 | database: 'database', 127 | table: 'table', 128 | status: :committed, 129 | upload_frozen?: true, 130 | job_id: '123456', 131 | valid_parts: '', 132 | error_parts: '', 133 | valid_records: '', 134 | error_records: '', 135 | ) 136 | } 137 | let(:bulk_import_parts) { 138 | %w(part1 part2 part3) 139 | } 140 | 141 | before do 142 | expect(client).to receive(:bulk_import).with(import_name).and_return(bulk_import) 143 | expect(client).to receive(:list_bulk_import_parts).with(import_name).and_return(bulk_import_parts) 144 | 145 | command.import_show(option) 146 | end 147 | 148 | it 'stderr should be include import name' do 149 | expect(stderr_io.string).to include import_name 150 | end 151 | 152 | it 'stdout should be include import parts' do 153 | bulk_import_parts.each do |part| 154 | expect(stdout_io.string).to include part 155 | end 156 | end 157 | end 158 | end 159 | 160 | describe '#import_create' do 161 | let(:import_name) { 'bulk_import' } 162 | let(:database) { 'database' } 163 | let(:table) { 'table' } 164 | let(:option) { 165 | List::CommandParser.new("import:create", %w(name, db_name, table_name), [], nil, [import_name, database, table], true) 166 | } 167 | let(:client) { double(:client) } 168 | 169 | before do 170 | allow(command).to receive(:get_client).and_return(client) 171 | end 172 | 173 | it 'create bulk import' do 174 | expect(client).to receive(:create_bulk_import).with(import_name, database, table, {}) 175 | 176 | command.import_create(option) 177 | 178 | expect(stderr_io.string).to include import_name 179 | end 180 | end 181 | 182 | describe '#import_config' do 183 | include_context 'quiet_out' 184 | 185 | let :command do 186 | Class.new { include TreasureData::Command }.new 187 | end 188 | let :option do 189 | List::CommandParser.new("import:config", args, [], nil, arguments, true) 190 | end 191 | let :out_file do 192 | Tempfile.new("seed.yml").tap {|s| s.close } 193 | end 194 | let(:endpoint) { 'http://example.com' } 195 | let(:apikey) { '1234ABCDEFGHIJKLMN' } 196 | 197 | before do 198 | allow(TreasureData::Config).to receive(:endpoint).and_return(endpoint) 199 | allow(TreasureData::Config).to receive(:apikey).and_return(apikey) 200 | end 201 | 202 | context 'unknown format' do 203 | let(:args) { ['url'] } 204 | let(:arguments) { ['localhost', '--format', 'msgpack', '-o', out_file.path] } 205 | 206 | it 'exit command' do 207 | expect { 208 | command.import_config(option) 209 | }.to raise_error TreasureData::Command::ParameterConfigurationError 210 | end 211 | end 212 | 213 | context 'support format' do 214 | let(:td_output_config) { 215 | { 216 | 'type' => 'td', 217 | 'endpoint' => TreasureData::Config.endpoint, 218 | 'apikey' => TreasureData::Config.apikey, 219 | 'database' => '', 220 | 'table' => '', 221 | } 222 | } 223 | 224 | before do 225 | command.import_config(option) 226 | end 227 | 228 | subject(:generated_config) { YAML.load_file(out_file.path) } 229 | 230 | %w(csv tsv).each do |format| 231 | context "--format #{format}" do 232 | let(:args) { ['url'] } 233 | context 'use path' do 234 | let(:path_prefix) { 'path/to/prefix_' } 235 | let(:arguments) { ["#{path_prefix}*.#{format}", '--format', format, '-o', out_file.path] } 236 | 237 | it 'generate configuration file' do 238 | expect(generated_config).to eq({ 239 | 'in' => { 240 | 'type' => 'file', 241 | 'decorders' => [{'type' => 'gzip'}], 242 | 'path_prefix' => path_prefix, 243 | }, 244 | 'out' => td_output_config 245 | }) 246 | end 247 | end 248 | 249 | context 'use s3 scheme' do 250 | let(:s3_access_key) { 'ABCDEFGHIJKLMN' } 251 | let(:s3_secret_key) { '1234ABCDEFGHIJKLMN' } 252 | let(:buckt_name) { 'my_bucket' } 253 | let(:path_prefix) { 'path/to/prefix_' } 254 | let(:s3_url) { "s3://#{s3_access_key}:#{s3_secret_key}@/#{buckt_name}/#{path_prefix}*.#{format}" } 255 | let(:arguments) { [s3_url, '--format', format, '-o', out_file.path] } 256 | 257 | it 'generate configuration file' do 258 | expect(generated_config).to eq({ 259 | 'in' => { 260 | 'type' => 's3', 261 | 'access_key_id' => s3_access_key, 262 | 'secret_access_key' => s3_secret_key, 263 | 'bucket' => buckt_name, 264 | 'path_prefix' => path_prefix, 265 | }, 266 | 'out' => { 267 | 'mode' => 'append' 268 | } 269 | }) 270 | end 271 | end 272 | end 273 | end 274 | 275 | context 'format is mysql' do 276 | let(:format) { 'mysql' } 277 | let(:host) { 'localhost' } 278 | let(:database) { 'database' } 279 | let(:user) { 'my_user' } 280 | let(:password) { 'my_password' } 281 | let(:table) { 'my_table' } 282 | 283 | let(:expected_config) { 284 | { 285 | 'in' => { 286 | 'type' => 'mysql', 287 | 'host' => host, 288 | 'port' => port, 289 | 'database' => database, 290 | 'user' => user, 291 | 'password' => password, 292 | 'table' => table, 293 | 'select' => "*", 294 | }, 295 | 'out' => td_output_config 296 | } 297 | } 298 | 299 | context 'like import:prepare arguments' do 300 | let(:args) { ['url'] } 301 | let(:arguments) { [table, '--db-url', mysql_url, '--db-user', user, '--db-password', password, '--format', 'mysql', '-o', out_file.path] } 302 | 303 | context 'scheme is jdbc' do 304 | let(:port) { 3333 } 305 | let(:mysql_url) { "jdbc:mysql://#{host}:#{port}/#{database}" } 306 | 307 | it 'generate configuration file' do 308 | expect(generated_config).to eq expected_config 309 | end 310 | end 311 | 312 | context 'scheme is mysql' do 313 | context 'with port' do 314 | let(:port) { 3333 } 315 | let(:mysql_url) { "mysql://#{host}:#{port}/#{database}" } 316 | 317 | it 'generate configuration file' do 318 | expect(generated_config).to eq expected_config 319 | end 320 | end 321 | 322 | context 'without port' do 323 | let(:mysql_url) { "mysql://#{host}/#{database}" } 324 | let(:port) { 3306 } 325 | 326 | it 'generate configuration file' do 327 | expect(generated_config).to eq expected_config 328 | end 329 | end 330 | end 331 | end 332 | 333 | context 'like import:upload arguments' do 334 | let(:args) { ['session', 'url'] } 335 | let(:arguments) { ['session', table, '--db-url', mysql_url, '--db-user', user, '--db-password', password, '--format', 'mysql', '-o', out_file.path] } 336 | let(:mysql_url) { "jdbc:mysql://#{host}/#{database}" } 337 | let(:port) { 3306 } 338 | 339 | it 'generate configuration file' do 340 | expect(generated_config).to eq expected_config 341 | end 342 | end 343 | end 344 | end 345 | 346 | context 'not migrate options' do 347 | %w(--columns --column-header).each do |opt| 348 | context "with #{opt}" do 349 | let(:args) { ['url'] } 350 | let(:arguments) { ["path/to/prefix_*.csv", '--format', 'csv', opt, 'col1,col2', '-o', out_file.path] } 351 | 352 | it "#{opt} is not migrate" do 353 | expect { command.import_config(option) }.not_to raise_error 354 | expect(stderr_io.string).to include 'not migrate. Please, edit config file after execute guess commands.' 355 | end 356 | end 357 | end 358 | end 359 | end 360 | end 361 | end 362 | -------------------------------------------------------------------------------- /spec/td/command/query_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'spec_helper' 4 | require 'td/command/common' 5 | require 'td/command/list' 6 | require 'td/command/query' 7 | require 'td/client' 8 | require 'tempfile' 9 | 10 | module TreasureData::Command 11 | describe 'query commands' do 12 | let :client do 13 | double('client') 14 | end 15 | let :job do 16 | double('job', job_id: 123) 17 | end 18 | let :command do 19 | Class.new { include TreasureData::Command }.new 20 | end 21 | before do 22 | allow(command).to receive(:get_client).and_return(client) 23 | expect(client).to receive(:database).with('sample_datasets') 24 | end 25 | 26 | describe 'domain key' do 27 | it 'accepts --domain-key' do 28 | expect(client).to receive(:query). 29 | with("sample_datasets", "SELECT 1;", nil, nil, nil, {"domain_key"=>"hoge"}). 30 | and_return(job) 31 | op = List::CommandParser.new("query", %w[query], %w[], nil, ['--domain-key=hoge', '-dsample_datasets', 'SELECT 1;'], false) 32 | command.query(op) 33 | end 34 | 35 | it 'raises error if --domain-key is duplicated' do 36 | expect(client).to receive(:query). 37 | with("sample_datasets", "SELECT 1;", nil, nil, nil, {"domain_key"=>"hoge"}). 38 | and_raise(::TreasureData::AlreadyExistsError.new('Query failed: domain_key has already been taken')) 39 | op = List::CommandParser.new("query", %w[query], %w[], nil, ['--domain-key=hoge', '-dsample_datasets', 'SELECT 1;'], false) 40 | expect{ command.query(op) }.to raise_error(TreasureData::AlreadyExistsError) 41 | end 42 | end 43 | 44 | describe 'wait' do 45 | let (:job_id){ 123 } 46 | let :job do 47 | obj = TreasureData::Job.new(client, job_id, 'presto', 'SELECT 1;') 48 | allow(obj).to receive(:debug).and_return(double.as_null_object) 49 | allow(obj).to receive(:sleep){|arg|@now += arg} 50 | obj 51 | end 52 | before do 53 | expect(client).to receive(:query). 54 | with("sample_datasets", "SELECT 1;", nil, nil, nil, {}). 55 | and_return(job) 56 | @now = 1_400_000_000 57 | allow(Time).to receive(:now){ @now } 58 | allow(command).to receive(:show_result_with_retry) 59 | end 60 | context 'success' do 61 | before do 62 | status = nil 63 | allow(client).to receive_message_chain(:api, :show_job).with(job_id) do 64 | if @now < 1_400_000_010 65 | status = TreasureData::Job::STATUS_RUNNING 66 | else 67 | status = TreasureData::Job::STATUS_SUCCESS 68 | end 69 | nil 70 | end 71 | allow(client).to receive(:job_status).with(job_id){ status } 72 | end 73 | it 'works with --wait' do 74 | op = List::CommandParser.new("query", %w[query], %w[], nil, ['--wait', '-dsample_datasets', 'SELECT 1;'], false) 75 | expect(command.query(op)).to be_nil 76 | end 77 | it 'works with --wait=360' do 78 | op = List::CommandParser.new("query", %w[query], %w[], nil, ['--wait=360', '-dsample_datasets', 'SELECT 1;'], false) 79 | expect(command.query(op)).to be_nil 80 | end 81 | end 82 | context 'temporary failure' do 83 | before do 84 | status = nil 85 | allow(client).to receive_message_chain(:api, :show_job).with(job_id) do 86 | if @now < 1_400_000_010 87 | status = TreasureData::Job::STATUS_RUNNING 88 | else 89 | status = TreasureData::Job::STATUS_SUCCESS 90 | end 91 | nil 92 | end 93 | allow(client).to receive(:job_status).with(job_id){ status } 94 | end 95 | it 'works with --wait' do 96 | op = List::CommandParser.new("query", %w[query], %w[], nil, ['--wait', '-dsample_datasets', 'SELECT 1;'], false) 97 | expect(command.query(op)).to be_nil 98 | end 99 | it 'works with --wait=360' do 100 | op = List::CommandParser.new("query", %w[query], %w[], nil, ['--wait=360', '-dsample_datasets', 'SELECT 1;'], false) 101 | expect(command.query(op)).to be_nil 102 | end 103 | end 104 | end 105 | 106 | describe 'engine version' do 107 | it 'accepts --engine-version' do 108 | expect(client).to receive(:query). 109 | with("sample_datasets", "SELECT 1;", nil, nil, nil, {"engine_version"=>"stable"}). 110 | and_return(job) 111 | op = List::CommandParser.new("query", %w[query], %w[], nil, ['--engine-version=stable', '-dsample_datasets', 'SELECT 1;'], false) 112 | command.query(op) 113 | end 114 | end 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /spec/td/command/sched_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'td/command/common' 3 | require 'td/config' 4 | require 'td/command/list' 5 | require 'td/command/sched' 6 | require 'td/client/model' 7 | require 'time' 8 | 9 | module TreasureData::Command 10 | describe TreasureData::Command do 11 | let(:client) { Object.new } 12 | 13 | let :job_params do 14 | ['job_id', :type, 'query', 'status', nil, nil, time, time, 123, 456] 15 | end 16 | 17 | let(:job1) { TreasureData::ScheduledJob.new(client, '2015-02-17 13:22:52 +0900', *job_params) } 18 | let(:job2) { TreasureData::ScheduledJob.new(client, nil, *job_params) } 19 | let(:time) { Time.now.xmlschema } 20 | let(:command) { Class.new { include TreasureData::Command }.new } 21 | let(:argv) { [] } 22 | let(:schedules) { [] } 23 | let(:op) { List::CommandParser.new('sched:last_job', %w[], %w[], false, argv, []) } 24 | 25 | before do 26 | allow(client).to receive(:schedules).and_return(schedules) 27 | allow(command).to receive(:get_client).and_return(client) 28 | end 29 | 30 | describe 'sched_create' do 31 | before do 32 | expect(client).to receive(:database).with('sample_datasets') 33 | end 34 | 35 | describe 'engine version' do 36 | let(:op) { 37 | List::CommandParser.new( 38 | 'sched:create', 39 | %w[name cron sql], 40 | %w[], 41 | false, 42 | ['--engine-version=stable', '-dsample_datasets', 'sched1', '0 * * * *', 'select count(*) from table1'], 43 | false 44 | ) 45 | } 46 | 47 | it 'accepts --engine-version' do 48 | expect(client).to receive(:create_schedule). 49 | with( 50 | "sched1", 51 | { 52 | cron: '0 * * * *', 53 | query: 'select count(*) from table1', 54 | database: 'sample_datasets', 55 | result: nil, 56 | timezone: nil, 57 | delay: 0, 58 | priority: nil, 59 | retry_limit: nil, 60 | type: nil, 61 | engine_version: "stable" 62 | } 63 | ). 64 | and_return(nil) 65 | command.sched_create(op) 66 | end 67 | end 68 | end 69 | 70 | describe 'sched_update' do 71 | describe 'engine version' do 72 | let(:op) { List::CommandParser.new('sched:update', %w[name], %w[], false, ['--engine-version=stable', 'old_name'], false) } 73 | 74 | it 'accepts --engine-version' do 75 | expect(client).to receive(:update_schedule). 76 | with("old_name", {"engine_version"=>"stable"}). 77 | and_return(nil) 78 | command.sched_update(op) 79 | end 80 | end 81 | end 82 | 83 | describe 'sched_history' do 84 | before do 85 | allow(client).to receive(:history).and_return(history) 86 | end 87 | 88 | let(:history) { [job1, job2] } 89 | 90 | it 'runs' do 91 | expect { 92 | command.sched_history(op) 93 | }.to_not raise_exception 94 | end 95 | end 96 | 97 | describe 'sched_result' do 98 | subject { command.sched_result(op) } 99 | 100 | before do 101 | allow(command).to receive(:get_history).with(client, nil, (back_number - 1), back_number).and_return(history) 102 | end 103 | 104 | shared_examples_for("passing argv and job_id to job:show") do 105 | it "invoke 'job:show [original argv] [job id]'" do 106 | expect_any_instance_of(TreasureData::Command::Runner).to receive(:run).with(["job:show", *show_arg, job_id]) 107 | subject 108 | end 109 | end 110 | 111 | context "history exists" do 112 | 113 | let(:job_id) { history.first.job_id } 114 | 115 | context 'without --last option' do 116 | let(:history) { [job1] } 117 | let(:back_number) { 1 } 118 | 119 | let(:argv) { %w(--last --format csv) } 120 | let(:show_arg) { %w(--format csv) } 121 | it_behaves_like "passing argv and job_id to job:show" 122 | end 123 | 124 | context '--last witout Num' do 125 | let(:history) { [job1] } 126 | let(:back_number) { 1 } 127 | 128 | let(:argv) { %w(--last --format csv) } 129 | let(:show_arg) { %w(--format csv) } 130 | it_behaves_like "passing argv and job_id to job:show" 131 | end 132 | 133 | context '--last 1' do 134 | let(:history) { [job1] } 135 | let(:back_number) { 1 } 136 | 137 | let(:argv) { %w(--last 1 --format csv) } 138 | let(:show_arg) { %w(--format csv) } 139 | it_behaves_like "passing argv and job_id to job:show" 140 | end 141 | 142 | context '--last 1 and --limit 1' do 143 | let(:history) { [job1] } 144 | let(:back_number) { 1 } 145 | 146 | let(:argv) { %w(--last 1 --format csv --limit 1) } 147 | let(:show_arg) { %w(--format csv --limit 1) } 148 | it_behaves_like "passing argv and job_id to job:show" 149 | end 150 | 151 | context '--last 2 after format option' do 152 | let(:history) { [job2] } 153 | let(:back_number) { 2 } 154 | 155 | let(:argv) { %w(--format csv --last 2 ) } 156 | let(:show_arg) { %w(--format csv) } 157 | it_behaves_like "passing argv and job_id to job:show" 158 | end 159 | 160 | context '--last 3(too back over)' do 161 | let(:history) { [] } 162 | let(:back_number) { 3 } 163 | 164 | let(:argv) { %w(--last 3 --format csv) } 165 | it 'raise ' do 166 | expect { 167 | command.sched_result(op) 168 | }.to raise_exception, "No jobs available for this query. Refer to 'sched:history'" 169 | end 170 | end 171 | 172 | context '--last WRONGARG(not a number)' do 173 | let(:history) { [job1] } 174 | let(:back_number) { 1 } 175 | 176 | let(:argv) { %w(--last TEST --format csv) } 177 | 178 | it "exit with 1" do 179 | begin 180 | command.sched_result(op) 181 | rescue SystemExit => e 182 | expect(e.status).to eq 1 183 | end 184 | end 185 | end 186 | end 187 | 188 | context "history dose not exists" do 189 | let(:back_number) { 1 } 190 | let(:history) { [] } 191 | before { allow(client).to receive(:history) { raise TreasureData::NotFoundError } } 192 | 193 | it "exit with 1" do 194 | begin 195 | subject 196 | rescue SystemExit => e 197 | expect(e.status).to eq 1 198 | end 199 | end 200 | end 201 | end 202 | end 203 | end 204 | -------------------------------------------------------------------------------- /spec/td/command/schema_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'td/command/common' 3 | require 'td/config' 4 | require 'td/command/list' 5 | require 'td/command/schema' 6 | require 'td/client/model' 7 | require 'time' 8 | 9 | describe TreasureData::Command do 10 | let(:client){ double('client') } 11 | let(:table){ double('table', schema: schema) } 12 | let(:db_name){ "database" } 13 | let(:table_name){ "table" } 14 | let(:command) { Class.new { include TreasureData::Command }.new } 15 | let(:columns){ ["foo:int", "BAR\u3070\u30FC:string@bar", "baz:baz!:array@baz"] } 16 | let(:schema){ TreasureData::Schema.parse(columns) } 17 | let(:stdout){ StringIO.new } 18 | before do 19 | allow(command).to receive(:get_client).and_return(client) 20 | allow(command).to receive(:get_table).with(client, db_name, table_name).and_return(table) 21 | allow(table).to receive(:get_table).with(client, db_name, table_name).and_return(table) 22 | allow($stdout).to receive(:puts){|arg| stdout.puts(*arg) } 23 | end 24 | 25 | describe 'schema_show' do 26 | let(:op){ double('op', cmd_parse: [db_name, table_name]) } 27 | it 'puts $stdout the schema' do 28 | command.schema_show(op) 29 | expect(stdout.string).to eq <@baz 34 | ) 35 | eom 36 | end 37 | end 38 | 39 | describe 'schema_set' do 40 | let(:op){ double('op', cmd_parse: [db_name, table_name, *columns]) } 41 | it 'calls client.update_schema' do 42 | allow($stderr).to receive(:puts) 43 | expect(client).to receive(:update_schema) do |x, y, z| 44 | expect(x).to eq db_name 45 | expect(y).to eq table_name 46 | expect(z.to_json).to eq schema.to_json 47 | end 48 | command.schema_set(op) 49 | end 50 | end 51 | 52 | describe 'schema_add' do 53 | let(:columns2){ ["foo2:int", "BAR\u30702:string@bar2", "baz:baz!2:array@baz2"] } 54 | let(:schema2){ TreasureData::Schema.parse(columns+columns2) } 55 | let(:op){ double('op', cmd_parse: [db_name, table_name, *columns2]) } 56 | it 'calls client.update_schema' do 57 | allow($stderr).to receive(:puts) 58 | expect(client).to receive(:update_schema) do |x, y, z| 59 | expect(x).to eq db_name 60 | expect(y).to eq table_name 61 | expect(z.to_json).to eq schema2.to_json 62 | end 63 | command.schema_add(op) 64 | end 65 | end 66 | 67 | describe 'schema_remove' do 68 | let(:op){ double('op', cmd_parse: [db_name, table_name, "foo"]) } 69 | let(:columns2){ ["BAR\u3070\u30FC:string@bar", "baz:baz!:array@baz"] } 70 | let(:schema2){ TreasureData::Schema.parse(columns2) } 71 | it 'calls client.update_schema' do 72 | allow($stderr).to receive(:puts) 73 | expect(client).to receive(:update_schema) do |x, y, z| 74 | expect(x).to eq db_name 75 | expect(y).to eq table_name 76 | expect(z.to_json).to eq schema.to_json 77 | end 78 | command.schema_remove(op) 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /spec/td/common_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'td/command/common' 3 | require 'td/client/api_error' 4 | 5 | module TreasureData::Command 6 | describe 'humanize_bytesize' do 7 | describe 'for values < 1024' do 8 | values = [0, 1, 10, 1023] 9 | values.each {|v| 10 | it "uses B as label and has no suffix (#{v})" do 11 | expect(TreasureData::Command::humanize_bytesize(v, 1)).to eq("#{v} B") 12 | end 13 | } 14 | end 15 | 16 | describe 'for 1024' do 17 | it 'uses kB and does not have a suffix' do 18 | expect(TreasureData::Command::humanize_bytesize(1024)).to eq("1 kB") 19 | end 20 | end 21 | describe 'for values between 1025 and (1024^2 - 1)' do 22 | base = 1024 23 | values = [ 24 | [base + 1, "1.0"], 25 | [base + 2, "1.0"], 26 | [base * 1024 - 1, "1023.9"] 27 | ] 28 | values.each {|val, exp| 29 | it "uses kB as label and has a suffix (#{val})" do 30 | result = TreasureData::Command::humanize_bytesize(val, 1) 31 | expect(result).to eq("#{exp} kB") 32 | end 33 | } 34 | end 35 | 36 | describe 'for 1024^2' do 37 | it 'uses MB and does not have a suffix' do 38 | expect(TreasureData::Command::humanize_bytesize(1024 ** 2)).to eq("1 MB") 39 | end 40 | end 41 | describe 'for values between (1024^2 + 1) and (1024^3 - 1)' do 42 | base = 1024 ** 2 43 | values = [ 44 | [base + 1, "1.0"], 45 | [base + 2, "1.0"], 46 | [base * 1024 - 1, "1023.9"] 47 | ] 48 | values.each {|val, exp| 49 | it "uses MB as label and has a suffix (#{val})" do 50 | result = TreasureData::Command::humanize_bytesize(val, 1) 51 | expect(result).to eq("#{exp} MB") 52 | end 53 | } 54 | end 55 | 56 | describe 'for 1024^3' do 57 | it 'uses GB and does not have a suffix' do 58 | expect(TreasureData::Command::humanize_bytesize(1024 ** 3)).to eq("1 GB") 59 | end 60 | end 61 | describe 'for values between (1024^3 + 1) and (1024^4 - 1)' do 62 | base = 1024 ** 3 63 | values = [ 64 | [base + 1, "1.0"], 65 | [base + 2, "1.0"], 66 | [base * 1024 - 1, "1023.9"] 67 | ] 68 | values.each {|val, exp| 69 | it "uses GB as label and has a suffix (#{val})" do 70 | result = TreasureData::Command::humanize_bytesize(val, 1) 71 | expect(result).to eq("#{exp} GB") 72 | end 73 | } 74 | end 75 | 76 | describe 'for 1024^4' do 77 | it 'uses TB and does not have a suffix' do 78 | expect(TreasureData::Command::humanize_bytesize(1024 ** 4)).to eq("1 TB") 79 | end 80 | end 81 | describe 'for values between (1024^4 + 1) and (1024^5 - 1)' do 82 | base = 1024 ** 4 83 | values = [ 84 | [base + 1, "1.0"], 85 | [base + 2, "1.0"], 86 | [base * 1024 - 1, "1023.9"] 87 | ] 88 | values.each {|val, exp| 89 | it "uses TB as label and has a suffix (#{val})" do 90 | result = TreasureData::Command::humanize_bytesize(val, 1) 91 | expect(result).to eq("#{exp} TB") 92 | end 93 | } 94 | end 95 | 96 | describe 'for 1024^5' do 97 | it 'uses TB and does not have a suffix' do 98 | expect(TreasureData::Command::humanize_bytesize(1024 ** 5)).to eq("1024 TB") 99 | end 100 | end 101 | describe 'for values between (1024^5 + 1) and (1024^6 - 1)' do 102 | base = 1024 ** 5 103 | values = [ 104 | [base + 1, "1024.0"], 105 | [base + 2, "1024.0"], 106 | [base * 1024 - 1, "1048575.9"] 107 | ] 108 | values.each {|val, exp| 109 | it "uses TB as label and has a suffix (#{val})" do 110 | result = TreasureData::Command::humanize_bytesize(val, 1) 111 | expect(result).to eq("#{exp} TB") 112 | end 113 | } 114 | end 115 | 116 | describe 'shows 1 digit' do 117 | it 'without second function argument' do 118 | values = [1024 + 1024 / 2, "1.5"] 119 | val, exp = values 120 | result = TreasureData::Command::humanize_bytesize(val) 121 | expect(result).to eq("#{exp} kB") 122 | end 123 | end 124 | describe 'shows the correct number of digits specified by the second argument' do 125 | (0...5).each {|i| 126 | it "when = #{i}" do 127 | val = 1024 + 1024 / 2 128 | if i == 0 129 | exp = 1.to_s 130 | else 131 | exp = sprintf "%.*f", i, 1.5 132 | end 133 | result = TreasureData::Command::humanize_bytesize(val, i) 134 | expect(result).to eq("#{exp} kB") 135 | end 136 | } 137 | end 138 | end 139 | 140 | describe 'SizeBasedDownloadProgressIndicator' do 141 | it "shows in 1% increments with default 'perc_step'" do 142 | size = 200 143 | indicator = TreasureData::Command::SizeBasedDownloadProgressIndicator.new("Downloading", size) 144 | size_increments = 2 145 | curr_size = 0 146 | while (curr_size += size_increments) < size do 147 | indicator.update(curr_size) 148 | sleep(0.05) 149 | end 150 | indicator.finish 151 | end 152 | 153 | it "shows in only current byte size if total is nil" do 154 | indicator = TreasureData::Command::SizeBasedDownloadProgressIndicator.new("Downloading", nil) 155 | size_increments = 2 156 | curr_size = 0 157 | while (curr_size += size_increments) < 200 do 158 | indicator.update(curr_size) 159 | sleep(0.05) 160 | end 161 | indicator.finish 162 | end 163 | 164 | it "shows in only current byte size if total is 0" do 165 | indicator = TreasureData::Command::SizeBasedDownloadProgressIndicator.new("Downloading", 0) 166 | size_increments = 2 167 | curr_size = 0 168 | while (curr_size += size_increments) < 200 do 169 | indicator.update(curr_size) 170 | sleep(0.05) 171 | end 172 | indicator.finish 173 | end 174 | end 175 | 176 | describe 'TimeBasedDownloadProgressIndicator' do 177 | it "increments about every 2 seconds with default 'periodicity'" do 178 | start_time = Time.now.to_i 179 | indicator = TreasureData::Command::TimeBasedDownloadProgressIndicator.new("Downloading", start_time) 180 | end_time = start_time + 10 181 | last_time = start_time 182 | while (curr_time = Time.now.to_i) < end_time do 183 | ret = indicator.update 184 | if ret == true 185 | diff = curr_time - last_time 186 | expect(diff).to be >= 2 187 | expect(diff).to be < 3 188 | last_time = curr_time 189 | end 190 | sleep(0.5) 191 | end 192 | indicator.finish 193 | end 194 | 195 | periodicities = [1, 2, 5] 196 | periodicities.each {|periodicity| 197 | it "increments about every #{periodicity} seconds with 'periodicity' = #{periodicity}" do 198 | start_time = Time.now.to_i 199 | indicator = TreasureData::Command::TimeBasedDownloadProgressIndicator.new("Downloading", start_time, periodicity) 200 | end_time = start_time + 10 201 | last_time = start_time 202 | while (curr_time = Time.now.to_i) < end_time do 203 | ret = indicator.update 204 | if ret == true 205 | expect(curr_time - last_time).to be >= periodicity 206 | expect(curr_time - last_time).to be < (periodicity + 1) 207 | last_time = curr_time 208 | end 209 | sleep(0.5) 210 | end 211 | indicator.finish 212 | end 213 | } 214 | end 215 | 216 | describe '#create_database_and_table_if_not_exist' do 217 | let(:command_class) { Class.new { include TreasureData::Command } } 218 | let(:command) { command_class.new } 219 | let(:client) { double(:client) } 220 | let(:database_name) { 'database' } 221 | let(:table_name) { 'table1' } 222 | 223 | let(:stderr_io) { StringIO.new } 224 | 225 | subject(:call_create_database_and_table_if_not_exist) { 226 | stderr = $stderr.dup 227 | begin 228 | $stderr = stderr_io 229 | 230 | command.__send__(:create_database_and_table_if_not_exist, client, database_name, table_name) 231 | ensure 232 | $stderr = stderr 233 | end 234 | } 235 | 236 | describe 'create database' do 237 | before do 238 | allow(client).to receive(:create_log_table) 239 | end 240 | 241 | context 'client.database success' do 242 | it 'not call client.create_database' do 243 | expect(client).to receive(:database).with(database_name) 244 | expect(client).not_to receive(:create_database) 245 | 246 | call_create_database_and_table_if_not_exist 247 | expect(stderr_io.string).not_to include "Database '#{database_name}'" 248 | end 249 | end 250 | 251 | context 'client.database raise NotFoundError' do 252 | before do 253 | expect(client).to receive(:database).with(database_name) { raise TreasureData::NotFoundError } 254 | end 255 | 256 | context 'craet_database success' do 257 | it 'call client.create_database' do 258 | expect(client).to receive(:create_database).with(database_name) 259 | 260 | call_create_database_and_table_if_not_exist 261 | expect(stderr_io.string).to include "Database '#{database_name}'" 262 | end 263 | end 264 | 265 | context 'craet_database raise AlreadyExistsError' do 266 | it 'resuce in method' do 267 | expect(client).to receive(:create_database).with(database_name) { raise TreasureData::AlreadyExistsError } 268 | 269 | expect { 270 | call_create_database_and_table_if_not_exist 271 | }.not_to raise_error 272 | 273 | expect(stderr_io.string).not_to include "Database '#{database_name}'" 274 | end 275 | end 276 | end 277 | 278 | context 'client.database raise ForbiddenError' do 279 | it 'not call client.create_database' do 280 | expect(client).to receive(:database).with(database_name) { raise TreasureData::ForbiddenError } 281 | expect(client).not_to receive(:create_database) 282 | 283 | call_create_database_and_table_if_not_exist 284 | expect(stderr_io.string).not_to include "Database '#{database_name}'" 285 | end 286 | end 287 | end 288 | 289 | describe 'create table' do 290 | before do 291 | allow(client).to receive(:database) 292 | end 293 | 294 | context 'create_log_table success' do 295 | it 'show message' do 296 | expect(client).to receive(:create_log_table).with(database_name, table_name) 297 | 298 | call_create_database_and_table_if_not_exist 299 | expect(stderr_io.string).to include "Table '#{database_name}.#{table_name}'" 300 | end 301 | end 302 | 303 | context 'create_log_table raise AlreadyExistsError' do 304 | it 'resuce in method' do 305 | expect(client).to receive(:create_log_table).with(database_name, table_name) { raise TreasureData::AlreadyExistsError } 306 | 307 | expect { 308 | call_create_database_and_table_if_not_exist 309 | }.not_to raise_error 310 | 311 | expect(stderr_io.string).not_to include "Table '#{database_name}.#{table_name}'" 312 | end 313 | end 314 | end 315 | end 316 | end 317 | -------------------------------------------------------------------------------- /spec/td/compact_format_yamler_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'td/compact_format_yamler' 3 | 4 | module TreasureData 5 | describe TreasureData::CompactFormatYamler do 6 | describe '.dump' do 7 | let(:data) { 8 | { 9 | 'a' => { 10 | 'b' => { 11 | 'c' => 1, 12 | 'd' => 'e' 13 | }, 14 | 'f' => [1, 2, 3] 15 | } 16 | } 17 | } 18 | 19 | let(:comapct_format_yaml) { 20 | <<-EOS 21 | --- 22 | a: 23 | b: {c: 1, d: e} 24 | f: 25 | - 1 26 | - 2 27 | - 3 28 | EOS 29 | } 30 | 31 | subject { TreasureData::CompactFormatYamler.dump data } 32 | 33 | it 'use compact format for deepest Hash' do 34 | expect(subject).to eq comapct_format_yaml 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/td/config_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tempfile' 3 | require 'td/config' 4 | 5 | describe TreasureData::Config do 6 | context 'workflow_endpoint' do 7 | before { TreasureData::Config.endpoint = api_endpoint } 8 | subject { TreasureData::Config.workflow_endpoint } 9 | context 'api.treasuredata.com' do 10 | context 'works without http schema' do 11 | let(:api_endpoint){ 'api.treasuredata.com' } 12 | it { is_expected.to eq 'https://api-workflow.treasuredata.com' } 13 | end 14 | context 'works with http schema' do 15 | let(:api_endpoint){ 'http://api.treasuredata.com' } 16 | it { is_expected.to eq 'https://api-workflow.treasuredata.com' } 17 | end 18 | context 'works with https schema' do 19 | let(:api_endpoint){ 'https://api.treasuredata.com' } 20 | it { is_expected.to eq 'https://api-workflow.treasuredata.com' } 21 | end 22 | end 23 | context 'api.treasuredata.co.jp' do 24 | let(:api_endpoint){ 'api.treasuredata.co.jp' } 25 | it { is_expected.to eq 'https://api-workflow.treasuredata.co.jp' } 26 | end 27 | context 'api.eu01.treasuredata.com' do 28 | let(:api_endpoint){ 'api.eu01.treasuredata.com' } 29 | it { is_expected.to eq 'https://api-workflow.eu01.treasuredata.com' } 30 | end 31 | context 'api.ap02.treasuredata.com' do 32 | let(:api_endpoint){ 'api.ap02.treasuredata.com' } 33 | it { is_expected.to eq 'https://api-workflow.ap02.treasuredata.com' } 34 | end 35 | context 'api.ap03.treasuredata.com' do 36 | let(:api_endpoint){ 'api.ap03.treasuredata.com' } 37 | it { is_expected.to eq 'https://api-workflow.ap03.treasuredata.com' } 38 | end 39 | 40 | context 'api-hoge.connect.treasuredata.com' do 41 | let(:api_endpoint){ 'api-hoge.connect.treasuredata.com' } 42 | it { is_expected.to eq 'https://api-workflow-hoge.connect.treasuredata.com' } 43 | end 44 | context 'api-czc21f.connect.treasuredata.co.jp' do 45 | let(:api_endpoint){ 'api-czc21f.connect.treasuredata.co.jp' } 46 | it { is_expected.to eq 'https://api-workflow-czc21f.connect.treasuredata.co.jp' } 47 | end 48 | 49 | context 'api-staging.treasuredata.com' do 50 | let(:api_endpoint){ 'api-staging.treasuredata.com' } 51 | it { is_expected.to eq 'https://api-staging-workflow.treasuredata.com' } 52 | end 53 | context 'api-staging.treasuredata.co.jp' do 54 | let(:api_endpoint){ 'api-staging.treasuredata.co.jp' } 55 | it { is_expected.to eq 'https://api-staging-workflow.treasuredata.co.jp' } 56 | end 57 | context 'api-staging.eu01.treasuredata.com' do 58 | let(:api_endpoint){ 'api-staging.eu01.treasuredata.com' } 59 | it { is_expected.to eq 'https://api-staging-workflow.eu01.treasuredata.com' } 60 | end 61 | context 'api-staging.ap02.treasuredata.com' do 62 | let(:api_endpoint){ 'api-staging.ap02.treasuredata.com' } 63 | it { is_expected.to eq 'https://api-staging-workflow.ap02.treasuredata.com' } 64 | end 65 | context 'api-staging.ap03.treasuredata.com' do 66 | let(:api_endpoint){ 'api-staging.ap03.treasuredata.com' } 67 | it { is_expected.to eq 'https://api-staging-workflow.ap03.treasuredata.com' } 68 | end 69 | 70 | context 'api-development.treasuredata.com' do 71 | let(:api_endpoint){ 'api-development.treasuredata.com' } 72 | it { is_expected.to eq 'https://api-development-workflow.treasuredata.com' } 73 | end 74 | context 'api-development.treasuredata.co.jp' do 75 | let(:api_endpoint){ 'api-development.treasuredata.co.jp' } 76 | it { is_expected.to eq 'https://api-development-workflow.treasuredata.co.jp' } 77 | end 78 | context 'api-development.eu01.treasuredata.com' do 79 | let(:api_endpoint){ 'api-development.eu01.treasuredata.com' } 80 | it { is_expected.to eq 'https://api-development-workflow.eu01.treasuredata.com' } 81 | end 82 | context 'api-development.ap02.treasuredata.com' do 83 | let(:api_endpoint){ 'api-development.ap02.treasuredata.com' } 84 | it { is_expected.to eq 'https://api-development-workflow.ap02.treasuredata.com' } 85 | end 86 | context 'api-development.ap03.treasuredata.com' do 87 | let(:api_endpoint){ 'api-development.ap03.treasuredata.com' } 88 | it { is_expected.to eq 'https://api-development-workflow.ap03.treasuredata.com' } 89 | end 90 | 91 | context 'ybi.jp-east.idcfcloud.com' do 92 | let(:api_endpoint){ 'ybi.jp-east.idcfcloud.com' } 93 | it 'raise error' do 94 | expect { subject }.to raise_error(TreasureData::ConfigError) 95 | end 96 | end 97 | end 98 | 99 | describe 'allow endpoint with trailing slash' do 100 | context 'self.endpoint' do 101 | before { TreasureData::Config.endpoint = api_endpoint } 102 | subject { TreasureData::Config.endpoint } 103 | 104 | context 'api.treasuredata.com' do 105 | let(:api_endpoint) { 'https://api.treasuredata.com/' } 106 | it { is_expected.to eq 'https://api.treasuredata.com' } 107 | end 108 | context 'api.treasuredata.co.jp' do 109 | let(:api_endpoint) { 'https://api.treasuredata.co.jp/' } 110 | it { is_expected.to eq 'https://api.treasuredata.co.jp' } 111 | end 112 | context 'api.eu01.treasuredata.com' do 113 | let(:api_endpoint) { 'https://api.eu01.treasuredata.com/' } 114 | it { is_expected.to eq 'https://api.eu01.treasuredata.com' } 115 | end 116 | context 'api.ap02.treasuredata.com' do 117 | let(:api_endpoint) { 'https://api.ap02.treasuredata.com/' } 118 | it { is_expected.to eq 'https://api.ap02.treasuredata.com' } 119 | end 120 | context 'api.ap03.treasuredata.com' do 121 | let(:api_endpoint) { 'https://api.ap03.treasuredata.com/' } 122 | it { is_expected.to eq 'https://api.ap03.treasuredata.com' } 123 | end 124 | context 'api-hoge.connect.treasuredata.com' do 125 | let(:api_endpoint){ 'https://api-hoge.connect.treasuredata.com/' } 126 | it { is_expected.to eq 'https://api-hoge.connect.treasuredata.com' } 127 | end 128 | end 129 | 130 | context 'self.import_endpoint' do 131 | before { TreasureData::Config.import_endpoint = api_endpoint } 132 | subject { TreasureData::Config.import_endpoint } 133 | 134 | context 'api-import.treasuredata.com' do 135 | let(:api_endpoint) { 'https://api-import.treasuredata.com/' } 136 | it { is_expected.to eq 'https://api-import.treasuredata.com' } 137 | end 138 | context 'api-import.treasuredata.co.jp' do 139 | let(:api_endpoint) { 'https://api-import.treasuredata.co.jp/' } 140 | it { is_expected.to eq 'https://api-import.treasuredata.co.jp' } 141 | end 142 | context 'api-import.eu01.treasuredata.com' do 143 | let(:api_endpoint) { 'https://api-import.eu01.treasuredata.com/' } 144 | it { is_expected.to eq 'https://api-import.eu01.treasuredata.com' } 145 | end 146 | context 'api-import.ap02.treasuredata.com' do 147 | let(:api_endpoint) { 'https://api-import.ap02.treasuredata.com/' } 148 | it { is_expected.to eq 'https://api-import.ap02.treasuredata.com' } 149 | end 150 | context 'api-import.ap03.treasuredata.com' do 151 | let(:api_endpoint) { 'https://api-import.ap03.treasuredata.com/' } 152 | it { is_expected.to eq 'https://api-import.ap03.treasuredata.com' } 153 | end 154 | end 155 | end 156 | 157 | describe '#read' do 158 | it 'sets @conf' do 159 | Tempfile.create('td.conf') do |f| 160 | f << <<-EOF 161 | # This is comment 162 | [section1] 163 | # This is comment 164 | key=val 165 | foo=bar 166 | EOF 167 | 168 | f.close 169 | 170 | config = TreasureData::Config.new 171 | config.read(f.path) 172 | 173 | expect(config["section1.key"]).to eq "val" 174 | expect(config["section1.foo"]).to eq "bar" 175 | end 176 | end 177 | end 178 | end 179 | -------------------------------------------------------------------------------- /spec/td/connector_config_normalizer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'td/connector_config_normalizer' 3 | require 'td/client/api_error' 4 | 5 | module TreasureData 6 | describe ConnectorConfigNormalizer do 7 | describe '#normalized_config' do 8 | subject { TreasureData::ConnectorConfigNormalizer.new(config).normalized_config } 9 | 10 | context 'has key :in' do 11 | context 'without :out, :exec' do 12 | let(:config) { {'in' => {'type' => 's3'}} } 13 | 14 | it { expect(subject).to eq config.merge('out' => {}, 'exec' => {}, 'filters' => []) } 15 | end 16 | 17 | context 'with :out, :exec, :filters' do 18 | let(:config) { 19 | { 20 | 'in' => {'type' => 's3'}, 21 | 'out' => {'mode' => 'append'}, 22 | 'exec' => {'guess_plugins' => ['json', 'query_string']}, 23 | 'filters' => [{'type' => 'speedometer'}] 24 | } 25 | } 26 | 27 | it { expect(subject).to eq config } 28 | end 29 | end 30 | 31 | context 'has key :config' do 32 | context 'with :in' do 33 | let(:config) { 34 | { 'config' => 35 | { 36 | 'in' => {'type' => 's3'}, 37 | 'out' => {'mode' => 'append'}, 38 | 'exec' => {'guess_plugins' => ['json', 'query_string']}, 39 | 'filters' => [{'type' => 'speedometer'}] 40 | } 41 | } 42 | } 43 | 44 | it { expect(subject).to eq config['config'] } 45 | 46 | end 47 | 48 | context 'without :in' do 49 | let(:config) { {'config' => {'type' => 's3'}} } 50 | 51 | it { expect(subject).to eq({'in' => config['config'], 'out' => {}, 'exec' => {}, 'filters' => []}) } 52 | end 53 | end 54 | 55 | context 'does not have key :in or :config' do 56 | let(:config) { {'type' => 's3'} } 57 | 58 | it { expect(subject).to eq({'in' => config, 'out' => {}, 'exec' => {}, 'filters' => []}) } 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/td/fixture/bulk_load.yml: -------------------------------------------------------------------------------- 1 | --- 2 | config: 3 | type: s3 4 | access_key_id: ACCESS_KEY 5 | secret_access_key: SECRET_ACCESS 6 | bucket: test-bucket 7 | path_prefix: dummy.csv.gz 8 | parser: 9 | charset: ISO-8859-9 10 | newline: CRLF 11 | type: csv 12 | delimiter: "," 13 | quote: '' 14 | escape: '' 15 | skip_header_lines: 1 16 | columns: 17 | - name: foo 18 | type: long 19 | - name: bar 20 | type: long 21 | - name: baz 22 | type: long 23 | decoders: 24 | - type: gzip 25 | -------------------------------------------------------------------------------- /spec/td/fixture/ca.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIID0DCCArigAwIBAgIBADANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGDAJKUDES 3 | MBAGA1UECgwJSklOLkdSLkpQMQwwCgYDVQQLDANSUlIxCzAJBgNVBAMMAkNBMB4X 4 | DTA0MDEzMDAwNDIzMloXDTM2MDEyMjAwNDIzMlowPDELMAkGA1UEBgwCSlAxEjAQ 5 | BgNVBAoMCUpJTi5HUi5KUDEMMAoGA1UECwwDUlJSMQswCQYDVQQDDAJDQTCCASIw 6 | DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANbv0x42BTKFEQOE+KJ2XmiSdZpR 7 | wjzQLAkPLRnLB98tlzs4xo+y4RyY/rd5TT9UzBJTIhP8CJi5GbS1oXEerQXB3P0d 8 | L5oSSMwGGyuIzgZe5+vZ1kgzQxMEKMMKlzA73rbMd4Jx3u5+jdbP0EDrPYfXSvLY 9 | bS04n2aX7zrN3x5KdDrNBfwBio2/qeaaj4+9OxnwRvYP3WOvqdW0h329eMfHw0pi 10 | JI0drIVdsEqClUV4pebT/F+CPUPkEh/weySgo9wANockkYu5ujw2GbLFcO5LXxxm 11 | dEfcVr3r6t6zOA4bJwL0W/e6LBcrwiG/qPDFErhwtgTLYf6Er67SzLyA66UCAwEA 12 | AaOB3DCB2TAPBgNVHRMBAf8EBTADAQH/MDEGCWCGSAGG+EIBDQQkFiJSdWJ5L09w 13 | ZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRlMB0GA1UdDgQWBBRJ7Xd380KzBV7f 14 | USKIQ+O/vKbhDzAOBgNVHQ8BAf8EBAMCAQYwZAYDVR0jBF0wW4AUSe13d/NCswVe 15 | 31EiiEPjv7ym4Q+hQKQ+MDwxCzAJBgNVBAYMAkpQMRIwEAYDVQQKDAlKSU4uR1Iu 16 | SlAxDDAKBgNVBAsMA1JSUjELMAkGA1UEAwwCQ0GCAQAwDQYJKoZIhvcNAQEFBQAD 17 | ggEBAIu/mfiez5XN5tn2jScgShPgHEFJBR0BTJBZF6xCk0jyqNx/g9HMj2ELCuK+ 18 | r/Y7KFW5c5M3AQ+xWW0ZSc4kvzyTcV7yTVIwj2jZ9ddYMN3nupZFgBK1GB4Y05GY 19 | MJJFRkSu6d/Ph5ypzBVw2YMT/nsOo5VwMUGLgS7YVjU+u/HNWz80J3oO17mNZllj 20 | PvORJcnjwlroDnS58KoJ7GDgejv3ESWADvX1OHLE4cRkiQGeLoEU4pxdCxXRqX0U 21 | PbwIkZN9mXVcrmPHq8MWi4eC/V7hnbZETMHuWhUoiNdOEfsAXr3iP4KjyyRdwc7a 22 | d/xgcK06UVQRL/HbEYGiQL056mc= 23 | -----END CERTIFICATE----- 24 | -------------------------------------------------------------------------------- /spec/td/fixture/server.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC/zCCAeegAwIBAgIBATANBgkqhkiG9w0BAQUFADA/MQswCQYDVQQGDAJKUDES 3 | MBAGA1UECgwJSklOLkdSLkpQMQwwCgYDVQQLDANSUlIxDjAMBgNVBAMMBVN1YkNB 4 | MB4XDTA0MDEzMTAzMTMxNloXDTMzMDEyMzAzMTMxNlowQzELMAkGA1UEBgwCSlAx 5 | EjAQBgNVBAoMCUpJTi5HUi5KUDEMMAoGA1UECwwDUlJSMRIwEAYDVQQDDAlsb2Nh 6 | bGhvc3QwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBANFJTxWqup3nV9dsJAku 7 | p+WaXnPNIzcpAA3qMGZDJTJsfa8Du7ZxTP0XJK5mETttBrn711cJxAuP3KjqnW9S 8 | vtZ9lY2sXJ6Zj62sN5LwG3VVe25dI28yR1EsbHjJ5Zjf9tmggMC6am52dxuHbt5/ 9 | vHo4ngJuKE/U+eeGRivMn6gFAgMBAAGjgYUwgYIwDAYDVR0TAQH/BAIwADAxBglg 10 | hkgBhvhCAQ0EJBYiUnVieS9PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAd 11 | BgNVHQ4EFgQUpZIyygD9JxFYHHOTEuWOLbCKfckwCwYDVR0PBAQDAgWgMBMGA1Ud 12 | JQQMMAoGCCsGAQUFBwMBMA0GCSqGSIb3DQEBBQUAA4IBAQBwAIj5SaBHaA5X31IP 13 | CFCJiep96awfp7RANO0cuUj+ZpGoFn9d6FXY0g+Eg5wAkCNIzZU5NHN9xsdOpnUo 14 | zIBbyTfQEPrge1CMWMvL6uGaoEXytq84VTitF/xBTky4KtTn6+es4/e7jrrzeUXQ 15 | RC46gkHObmDT91RkOEGjHLyld2328jo3DIN/VTHIryDeVHDWjY5dENwpwdkhhm60 16 | DR9IrNBbXWEe9emtguNXeN0iu1ux0lG1Hc6pWGQxMlRKNvGh0yZB9u5EVe38tOV0 17 | jQaoNyL7qzcQoXD3Dmbi1p0iRmg/+HngISsz8K7k7MBNVsSclztwgCzTZOBiVtkM 18 | rRlQ 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /spec/td/fixture/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXQIBAAKBgQDRSU8Vqrqd51fXbCQJLqflml5zzSM3KQAN6jBmQyUybH2vA7u2 3 | cUz9FySuZhE7bQa5+9dXCcQLj9yo6p1vUr7WfZWNrFyemY+trDeS8Bt1VXtuXSNv 4 | MkdRLGx4yeWY3/bZoIDAumpudncbh27ef7x6OJ4CbihP1PnnhkYrzJ+oBQIDAQAB 5 | AoGBAIf4CstW2ltQO7+XYGoex7Hh8s9lTSW/G2vu5Hbr1LTHy3fzAvdq8MvVR12O 6 | rk9fa+lU9vhzPc0NMB0GIDZ9GcHuhW5hD1Wg9OSCbTOkZDoH3CAFqonjh4Qfwv5W 7 | IPAFn9KHukdqGXkwEMdErsUaPTy9A1V/aROVEaAY+HJgq/eZAkEA/BP1QMV04WEZ 8 | Oynzz7/lLizJGGxp2AOvEVtqMoycA/Qk+zdKP8ufE0wbmCE3Qd6GoynavsHb6aGK 9 | gQobb8zDZwJBANSK6MrXlrZTtEaeZuyOB4mAmRzGzOUVkUyULUjEx2GDT93ujAma 10 | qm/2d3E+wXAkNSeRpjUmlQXy/2oSqnGvYbMCQQDRM+cYyEcGPUVpWpnj0shrF/QU 11 | 9vSot/X1G775EMTyaw6+BtbyNxVgOIu2J+rqGbn3c+b85XqTXOPL0A2RLYkFAkAm 12 | syhSDtE9X55aoWsCNZY/vi+i4rvaFoQ/WleogVQAeGVpdo7/DK9t9YWoFBIqth0L 13 | mGSYFu9ZhvZkvQNV8eYrAkBJ+rOIaLDsmbrgkeDruH+B/9yrm4McDtQ/rgnOGYnH 14 | LjLpLLOrgUxqpzLWe++EwSLwK2//dHO+SPsQJ4xsyQJy 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /spec/td/fixture/testRootCA.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDBDCCAewCCQCQdq0wStTtITANBgkqhkiG9w0BAQsFADBEMR0wGwYDVQQDDBR3 3 | d3cudHJlYXN1cmVkYXRhLmNvbTELMAkGA1UEBhMCVVMxFjAUBgNVBAcMDVNhbiBG 4 | cmFuc2lzY28wHhcNMjIxMDE0MDQzNjA0WhcNMzIwNzEzMDQzNjA0WjBEMR0wGwYD 5 | VQQDDBR3d3cudHJlYXN1cmVkYXRhLmNvbTELMAkGA1UEBhMCVVMxFjAUBgNVBAcM 6 | DVNhbiBGcmFuc2lzY28wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCU 7 | 4RbAD5pop+AUYyJT0L4r/ZwOFz72TqQwyPh8EbpPlkvErWPP07kLO1gmfPHPH6zE 8 | 4sAd8Pl5HZ29Z3fF9zO4ZaOehO+kMHp3jp1nq6vURzMjxDj42CyP+M7DOLrov98m 9 | uwcZMdVjJlz9ZWt7P4x/F5cyqnzmFYxefi0xbgFFXKaWL1W3Fmq1dKMCgbCVWxlA 10 | cufRFQmmO+OOcKfJTpcZZXH+VKai81OLjjoBkEERqZU8ySv2zR3gLBMAU3x/VL6/ 11 | AxcAi6ZbxQ29O28VTojQ44dA0op5wDtbI3reH9cMgALI1Swo2DyxXejYoZJYKqk3 12 | AH2BiM0dbPnzOoufxjm/AgMBAAEwDQYJKoZIhvcNAQELBQADggEBACLeA1a/TU4E 13 | 8bKVEr8X9CMROG2j5lsmPLZRTDUQT50iu5MBZzCNQi2tzUC+QHWC8m/xrhsg4b4t 14 | r52dQuZlPSEGbzdiltrzQPODnSkA6tmgPeaea5wKG8abDgVQRbMBiamwgJruIi1s 15 | ynX2yD4qdo07fTAC9CyKd9kogMRMXaqVppfIrob+HL3KQJ4MQPRJb+IN3K14Bdq8 16 | /XL+8zH4zsWxPCkJyKlcUwGx3M0NHnYX0l8Enap65McTiaPIiCgr5TnkJmctDKBm 17 | Zrv+c1sb69oJtlayGKWNymxdU+1f79IVioQf4qCa7ZB1//ON60KrQ9cRsmObJtnE 18 | KD6Oett1Kto= 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /spec/td/fixture/testServer.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIID2jCCAsKgAwIBAgIJALE6L1U9f459MA0GCSqGSIb3DQEBCwUAMEQxHTAbBgNV 3 | BAMMFHd3dy50cmVhc3VyZWRhdGEuY29tMQswCQYDVQQGEwJVUzEWMBQGA1UEBwwN 4 | U2FuIEZyYW5zaXNjbzAeFw0yMjEwMTQwOTU3NDdaFw0zMjEwMTEwOTU3NDdaMGwx 5 | CzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4g 6 | RnJhbnNpc2NvMRYwFAYDVQQKDA1UcmVhc3VyZSBEYXRhMQwwCgYDVQQLDANFbmcx 7 | CjAIBgNVBAMMASowggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6TBzz 8 | TnMXEwQhmsF9ggvyjzhsVQRQnVafPjR45mKgN5+4QmkW+t1RUv93ysz2UOXM1XfN 9 | HptcrgbdsEmhDDWopsPgfJrSdN6O44oxplZOTusn20lJDG4Ogm0Kdn2k0P6Cj1bG 10 | PmFLgw4BeXqhMWI60rbQw/HtFDMx6MWU555wr//jJNPIPOVBzVOiGhmoWYJY8ARB 11 | WlplKeW0BkQ2J+650Q4dcqrrq4R6UbnyTjf9pqxSa2qP5y9ZQr+69COveX26ZSmA 12 | 76PRzGE1oohsB37OCcQMwU+CuIHZkS9CMEGgkYfmfKaGtrsGtG52eGGwiYoAwSGJ 13 | BJ/v53Pp3BGKWo9tAgMBAAGjgaYwgaMwXgYDVR0jBFcwVaFIpEYwRDEdMBsGA1UE 14 | AwwUd3d3LnRyZWFzdXJlZGF0YS5jb20xCzAJBgNVBAYTAlVTMRYwFAYDVQQHDA1T 15 | YW4gRnJhbnNpc2NvggkAkHatMErU7SEwCQYDVR0TBAIwADALBgNVHQ8EBAMCBPAw 16 | EwYDVR0lBAwwCgYIKwYBBQUHAwEwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqG 17 | SIb3DQEBCwUAA4IBAQBQkwlcEc98Pj9NB9NjhwZalgI36ptmj7vxyD8GXaJQWh7P 18 | BKnOTxwbqhY68B7QgQ+NTusltnWsiO2IBAZR3MZGeLh4rFUUi4daww7fCLbYQD0t 19 | YKOjUv8C4rlp8lRs/IMDMl1i2w1R6x78+RKSy1OhIvxT3qV9Zxx8RfqINboyDlbo 20 | 6lOEQlLYGGUJutIbkUoPCo3TO3/oaLfwlFhnxgEwf+lX/qHoqTkz16JRzil6yFOx 21 | qc5ugRF0S34lpnBd6qfbhV1QFY17WvSbbXmpBPONI2SLs0WSwwCTFVRg8Z+Y90oG 22 | oGH+JdWsxn+9e8JTB/UOpxr+vWTyQ63wLaElPOyC 23 | -----END CERTIFICATE----- 24 | -------------------------------------------------------------------------------- /spec/td/fixture/testServer.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEogIBAAKCAQEAukwc805zFxMEIZrBfYIL8o84bFUEUJ1Wnz40eOZioDefuEJp 3 | FvrdUVL/d8rM9lDlzNV3zR6bXK4G3bBJoQw1qKbD4Hya0nTejuOKMaZWTk7rJ9tJ 4 | SQxuDoJtCnZ9pND+go9Wxj5hS4MOAXl6oTFiOtK20MPx7RQzMejFlOeecK//4yTT 5 | yDzlQc1TohoZqFmCWPAEQVpaZSnltAZENifuudEOHXKq66uEelG58k43/aasUmtq 6 | j+cvWUK/uvQjr3l9umUpgO+j0cxhNaKIbAd+zgnEDMFPgriB2ZEvQjBBoJGH5nym 7 | hra7BrRudnhhsImKAMEhiQSf7+dz6dwRilqPbQIDAQABAoIBAF8IKpB2yUDRA398 8 | 6Qz0BNIz+u1QJQZWbHSJD81IgLEIDuK4hdEiITm14/mgqxNPSxpFHnq1DT2mzHvT 9 | zItppgmlIDBof7WxxkIPklQnbMk/erd3JhgsTgv6vlLjBM7JibriEbrI4WrarI9V 10 | /5cwkNI+4OD3w3ZTopXoDroZuPnz/v0EKhDz05E4jHijfiRL3OxaAvZ2zWVKifVh 11 | elilbCeLR2KgKG6jPrBzub2Yll9C7Xxt7emIMvyTg5jEOkmm7IWfAZ6uIraI7pAs 12 | 5MlwQDIb/Ypby4gHJ0CPWy2AhdybF2Y+hm5jXY1aD7xjlG8pjudidYtnrpWO1kPV 13 | bbqIvUUCgYEA3G5JQvcA9ZTiHculyW/6O++NXrOkznXI04JhhKN6JvdTdp3xC61k 14 | H+gnP0e2/FeqqZWF6KWlmPBuonofEQqyCwJdHRZESaixgw7iKqN2t11Z9EhWzoGq 15 | UGc1oMT90AIHxsGUBbUIZdlj1D8KDx/qymlsKp8ozTKwyCqNesiYet8CgYEA2FvQ 16 | 94zgOauOCwfOwDipwivrJ9nblykMfcp8NCC2jLxI3rjH30EhRWBZrc0V55erjfrz 17 | QgJYlj4kN/3GwLVq2Agr5EczjePScRlkpPNHdfAfXKvg7dHHsgrYf9lP6j2Adz8B 18 | mhyYs1gfo6ekeHLoqKBlSn08AosfSTpETuIdizMCgYBFATFmCTT/rA/tC+dmW+uV 19 | /7PdxZb+Gtk3fUVR5GtE73/tThw7b5g8dMx0ftrFvBvs4qX84n4olnvL2TcIerSp 20 | xZ+oj2PpOyn2wR4EAxAS7uJOGqcyFl1etjCPl5ttFnWgvtC7yKRMXfVmaCWZ/n/d 21 | xYra/OAk/I1i3A9WNJ2nOQKBgD83jLZYPkf7fXRxopJ9u/RVOs+ZE1V2lATJPkNI 22 | 763tcelJ2nS8JgmMXoeu7eCOa3z/v0YhQ1sa6yBFEWbLW12l/ZUkzMZ/s8SCI+si 23 | flXShIdiXUV/zzaRfrLUf0o1EC1HhqNOCbwVWqFJ4X+kK6DhxNbgAsHHfqu5z62w 24 | 2esLAoGAbIbd5Xd+lRXbuNTyIqAONf4KgH529hU4W9tw+kR5OFqadC557Jvs33nn 25 | iSqGgET+DZTxaOZk8KeCKZys8IcIxvWyhq22/Pd0BKl1ozWj4w/bkDlqO2V9I9NY 26 | 32DBk4iAHljJqkmxnJkSXgGypf4guPZlNGPbTijXw3oHaIpvPl0= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /spec/td/fixture/tmp.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treasure-data/td/7da645b4e835615a31fd31bc3e76e710fd39ce25/spec/td/fixture/tmp.zip -------------------------------------------------------------------------------- /spec/td/helpers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'td/helpers' 3 | require 'open3' 4 | 5 | module TreasureData 6 | 7 | describe 'format_with_delimiter' do 8 | it "delimits the number with ',' by default" do 9 | expect(Helpers.format_with_delimiter(0)).to eq("0") 10 | expect(Helpers.format_with_delimiter(10)).to eq("10") 11 | expect(Helpers.format_with_delimiter(100)).to eq("100") 12 | expect(Helpers.format_with_delimiter(1000)).to eq("1,000") 13 | expect(Helpers.format_with_delimiter(10000)).to eq("10,000") 14 | expect(Helpers.format_with_delimiter(100000)).to eq("100,000") 15 | expect(Helpers.format_with_delimiter(1000000)).to eq("1,000,000") 16 | end 17 | end 18 | 19 | describe 'on_64bit_os?' do 20 | 21 | def with_env(name, var) 22 | backup, ENV[name] = ENV[name], var 23 | begin 24 | yield 25 | ensure 26 | ENV[name] = backup 27 | end 28 | end 29 | 30 | it 'returns true for windows when PROCESSOR_ARCHITECTURE=amd64' do 31 | allow(Helpers).to receive(:on_windows?) {true} 32 | with_env('PROCESSOR_ARCHITECTURE', 'amd64') { 33 | expect(Helpers.on_64bit_os?).to be(true) 34 | } 35 | end 36 | 37 | it 'returns true for windows when PROCESSOR_ARCHITECTURE=x86 and PROCESSOR_ARCHITEW6432 is set' do 38 | allow(Helpers).to receive(:on_windows?) {true} 39 | with_env('PROCESSOR_ARCHITECTURE', 'x86') { 40 | with_env('PROCESSOR_ARCHITEW6432', '') { 41 | expect(Helpers.on_64bit_os?).to be(true) 42 | } 43 | } 44 | end 45 | 46 | it 'returns false for windows when PROCESSOR_ARCHITECTURE=x86 and PROCESSOR_ARCHITEW6432 is not set' do 47 | allow(Helpers).to receive(:on_windows?) {true} 48 | with_env('PROCESSOR_ARCHITECTURE', 'x86') { 49 | with_env('PROCESSOR_ARCHITEW6432', nil) { 50 | expect(Helpers.on_64bit_os?).to be(false) 51 | } 52 | } 53 | end 54 | 55 | it 'returns true for non-windows when uname -m prints x86_64' do 56 | allow(Helpers).to receive(:on_windows?) {false} 57 | allow(Open3).to receive(:capture2).with('uname', '-m') {['x86_64', double(:success? => true)]} 58 | expect(Helpers.on_64bit_os?).to be(true) 59 | expect(Open3).to have_received(:capture2).with('uname', '-m') 60 | end 61 | 62 | it 'returns false for non-windows when uname -m prints i686' do 63 | allow(Helpers).to receive(:on_windows?) {false} 64 | allow(Open3).to receive(:capture2).with('uname', '-m') {['i686', double(:success? => true)]} 65 | expect(Helpers.on_64bit_os?).to be(false) 66 | expect(Open3).to have_received(:capture2).with('uname', '-m') 67 | end 68 | 69 | end 70 | 71 | end 72 | -------------------------------------------------------------------------------- /spec/td/updater_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'td/command/common' 3 | require 'td/updater' 4 | require 'webrick' 5 | require 'webrick/https' 6 | require 'webrick/httpproxy' 7 | require 'logger' 8 | 9 | module TreasureData::Updater 10 | 11 | %w(x86_64-darwin14 x64-mingw32).each do |platform| 12 | describe "RUBY_PLATFORM is '#{platform}'" do 13 | before do 14 | stub_const('RUBY_PLATFORM', platform) 15 | end 16 | 17 | describe 'without the TD_TOOLBELT_UPDATE_ROOT environment variable defined' do 18 | let :default_toolbelt_url do 19 | "http://toolbelt.treasuredata.com" 20 | end 21 | 22 | describe 'endpoints methods' do 23 | it 'use the default root path' do 24 | expect(TreasureData::Updater.endpoint_root).to eq(default_toolbelt_url) 25 | expect(TreasureData::Updater.version_endpoint).to match(Regexp.new(default_toolbelt_url)) 26 | expect(TreasureData::Updater.update_package_endpoint).to match(Regexp.new(default_toolbelt_url)) 27 | end 28 | end 29 | end 30 | 31 | describe 'with the TD_TOOLBELT_UPDATE_ROOT environment variable defined' do 32 | before do 33 | ENV['TD_TOOLBELT_UPDATE_ROOT'] = 'https://0.0.0.0:5000/' 34 | end 35 | describe 'endpoints methods' do 36 | it 'use the custom root path' do 37 | expect(TreasureData::Updater.endpoint_root).to eq(ENV['TD_TOOLBELT_UPDATE_ROOT']) 38 | expect(TreasureData::Updater.version_endpoint).to match(Regexp.new(ENV['TD_TOOLBELT_UPDATE_ROOT'])) 39 | expect(TreasureData::Updater.update_package_endpoint).to match(Regexp.new(ENV['TD_TOOLBELT_UPDATE_ROOT'])) 40 | end 41 | end 42 | after do 43 | ENV.delete 'TD_TOOLBELT_UPDATE_ROOT' 44 | end 45 | end 46 | end 47 | end 48 | 49 | describe 'with a proxy' do 50 | before :each do 51 | setup_proxy_server 52 | setup_server 53 | end 54 | 55 | after :each do 56 | if @proxy_server 57 | @proxy_server.shutdown 58 | @proxy_server_thread.join 59 | end 60 | if @server 61 | @server.shutdown 62 | @server_thread.join 63 | end 64 | end 65 | 66 | class TestUpdater 67 | include TreasureData::Updater::ModuleDefinition 68 | 69 | def initialize(endpoint_root) 70 | @endpoint_root = endpoint_root 71 | end 72 | 73 | def updating_lock_path 74 | File.expand_path("updating_lock_path.lock", File.dirname(__FILE__)) 75 | end 76 | 77 | def on_windows? 78 | true 79 | end 80 | 81 | def endpoint_root 82 | @endpoint_root 83 | end 84 | 85 | def latest_local_version 86 | '0.11.5' 87 | end 88 | end 89 | 90 | class JarUpdateTester 91 | include TreasureData::Updater 92 | 93 | def kick 94 | jar_update 95 | end 96 | end 97 | 98 | it 'downloads tmp.zip via proxy and raise td version conflict' do 99 | with_proxy do 100 | expect { 101 | TestUpdater.new("https://localhost:#{@server.config[:Port]}").update 102 | }.to raise_error TreasureData::Command::UpdateError 103 | end 104 | end 105 | 106 | it 'works' do 107 | with_proxy do 108 | with_env('TD_TOOLBELT_JARUPDATE_ROOT', "https://localhost:#{@server.config[:Port]}") do 109 | expect { 110 | JarUpdateTester.new.kick 111 | }.not_to raise_error 112 | end 113 | end 114 | end 115 | 116 | describe "current working directory doesn't change after call `jar_update`" do 117 | shared_examples_for("jar_update behavior") do 118 | it "doesn't change cwd" do 119 | with_env('TD_TOOLBELT_JARUPDATE_ROOT', "https://localhost:#{@server.config[:Port]}") do 120 | pwd = Dir.pwd 121 | subject 122 | expect(Dir.pwd).to eq pwd 123 | end 124 | end 125 | 126 | it "don't exists td-import.jar.new" do 127 | with_env('TD_TOOLBELT_JARUPDATE_ROOT', "https://localhost:#{@server.config[:Port]}") do 128 | subject 129 | end 130 | tmpfile = File.join(TreasureData::Updater.jarfile_dest_path, 'td-import.jar.new') 131 | expect(File.exist?(tmpfile)).to eq false 132 | end 133 | end 134 | 135 | let(:updater) { JarUpdateTester.new } 136 | 137 | subject { updater.kick } 138 | 139 | context "not updated" do 140 | before { allow(updater).to receive(:existent_jar_updated_time).and_return(Time.now) } 141 | 142 | it_behaves_like "jar_update behavior" 143 | end 144 | 145 | context "updated" do 146 | before { allow(updater).to receive(:existent_jar_updated_time).and_return(Time.at(0)) } 147 | 148 | it_behaves_like "jar_update behavior" 149 | end 150 | end 151 | 152 | def with_proxy 153 | with_env('HTTP_PROXY', "http://localhost:#{@proxy_server.config[:Port]}") do 154 | yield 155 | end 156 | end 157 | 158 | def with_env(name, var) 159 | backup, ENV[name] = ENV[name], var 160 | begin 161 | yield 162 | ensure 163 | ENV[name] = backup 164 | end 165 | end 166 | 167 | def setup_proxy_server 168 | logger = Logger.new(STDERR) 169 | logger.progname = 'proxy' 170 | logger.level = Logger::Severity::FATAL # avoid logging 171 | @proxy_server = WEBrick::HTTPProxyServer.new( 172 | :BindAddress => "localhost", 173 | :Logger => logger, 174 | :Port => 1000 + rand(1000), 175 | :AccessLog => [] 176 | ) 177 | @proxy_server_thread = start_server_thread(@proxy_server) 178 | @proxy_server 179 | end 180 | 181 | def setup_server 182 | logger = Logger.new(STDERR) 183 | logger.progname = 'server' 184 | logger.level = Logger::Severity::FATAL # avoid logging 185 | @server = WEBrick::HTTPServer.new( 186 | :BindAddress => "localhost", 187 | :Logger => logger, 188 | :Port => 1000 + rand(1000), 189 | :AccessLog => [], 190 | :DocumentRoot => '.', 191 | :SSLEnable => true, 192 | :SSLCACertificateFile => fixture_file('testRootCA.crt'), 193 | :SSLCertificate => cert('testServer.crt'), 194 | :SSLPrivateKey => key('testServer.key') 195 | ) 196 | @serverport = @server.config[:Port] 197 | @server.mount( 198 | '/version.exe', 199 | WEBrick::HTTPServlet::ProcHandler.new(method(:version).to_proc) 200 | ) 201 | @server.mount( 202 | '/td-update-exe.zip', 203 | WEBrick::HTTPServlet::ProcHandler.new(method(:download).to_proc) 204 | ) 205 | @server.mount( 206 | '/maven2/com/treasuredata/td-import/maven-metadata.xml', 207 | WEBrick::HTTPServlet::ProcHandler.new(method(:metadata).to_proc) 208 | ) 209 | @server.mount( 210 | '/maven2/com/treasuredata/td-import/version/td-import-version-jar-with-dependencies.jar', 211 | WEBrick::HTTPServlet::ProcHandler.new(method(:jar).to_proc) 212 | ) 213 | @server_thread = start_server_thread(@server) 214 | @server 215 | end 216 | 217 | def version(req, res) 218 | res['content-type'] = 'text/plain' 219 | res.body = '0.11.6' 220 | end 221 | 222 | def download(req, res) 223 | res['content-type'] = 'application/octet-stream' 224 | res.body = File.read(fixture_file('tmp.zip')) 225 | end 226 | 227 | def metadata(req, res) 228 | res['content-type'] = 'application/xml' 229 | res.body = '20141204123456version' 230 | end 231 | 232 | def jar(req, res) 233 | res['content-type'] = 'application/octet-stream' 234 | res.body = File.read(fixture_file('tmp.zip')) 235 | end 236 | 237 | def start_server_thread(server) 238 | t = Thread.new { 239 | Thread.current.abort_on_exception = true 240 | server.start 241 | } 242 | while server.status != :Running 243 | sleep 0.1 244 | unless t.alive? 245 | t.join 246 | raise 247 | end 248 | end 249 | t 250 | end 251 | 252 | def cert(filename) 253 | OpenSSL::X509::Certificate.new(File.read(fixture_file(filename))) 254 | end 255 | 256 | def key(filename) 257 | OpenSSL::PKey::RSA.new(File.read(fixture_file(filename))) 258 | end 259 | 260 | def fixture_file(filename) 261 | File.expand_path(File.join('fixture', filename), File.dirname(__FILE__)) 262 | end 263 | end 264 | end 265 | -------------------------------------------------------------------------------- /spec/td/version_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module TreasureData::Command 4 | describe '--version' do 5 | it "shows version" do 6 | stderr, stdout = execute_td("--version") 7 | expect(stderr).to eq("") 8 | expect(stdout).to eq <<-STDOUT 9 | #{TreasureData::TOOLBELT_VERSION} 10 | STDOUT 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /td.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | $:.push File.expand_path('../lib', __FILE__) 3 | require 'td/version' 4 | 5 | Gem::Specification.new do |gem| 6 | gem.name = "td" 7 | gem.description = "CLI to manage data on Treasure Data, the Hadoop-based cloud data warehousing" 8 | gem.homepage = "http://treasure-data.com/" 9 | gem.summary = "CLI to manage data on Treasure Data, the Hadoop-based cloud data warehousing" 10 | gem.version = TreasureData::TOOLBELT_VERSION 11 | gem.authors = ["Treasure Data, Inc."] 12 | gem.email = "support@treasure-data.com" 13 | gem.files = `git ls-files`.split("\n").select { |f| !f.start_with?('dist') } 14 | gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 15 | gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 16 | gem.require_paths = ['lib'] 17 | gem.required_ruby_version = '>= 2.1' 18 | gem.licenses = ["Apache-2.0"] 19 | 20 | gem.add_dependency "msgpack" 21 | gem.add_dependency "rexml" 22 | gem.add_dependency "yajl-ruby", ">= 1.3.1", "< 2.0" 23 | gem.add_dependency "hirb", ">= 0.4.5" 24 | gem.add_dependency "parallel", "~> 1.20.0" 25 | gem.add_dependency "td-client", ">= 1.0.8", "< 3" 26 | gem.add_dependency "td-logger", ">= 0.3.21", "< 2" 27 | gem.add_dependency "rubyzip", "~> 1.3.0" 28 | gem.add_dependency "zip-zip", "~> 0.3" 29 | gem.add_dependency "ruby-progressbar", "~> 1.7" 30 | gem.add_development_dependency "rake" 31 | gem.add_development_dependency "rspec" 32 | gem.add_development_dependency 'webrick' 33 | gem.add_development_dependency "simplecov" 34 | gem.add_development_dependency 'coveralls' 35 | end 36 | --------------------------------------------------------------------------------