├── .autotest ├── .gitignore ├── Gemfile ├── LICENSE ├── README.markdown ├── autotest └── discover.rb ├── bin ├── as_installed │ ├── ss │ └── strongspace ├── ss └── strongspace ├── init.rb ├── lib ├── strongspace.rb └── strongspace │ ├── client.rb │ ├── command.rb │ ├── commands │ ├── auth.rb │ ├── base.rb │ ├── files.rb │ ├── help.rb │ ├── keys.rb │ ├── plugins.rb │ ├── spaces.rb │ └── version.rb │ ├── exceptions.rb │ ├── helpers.rb │ ├── plugin.rb │ ├── plugin_interface.rb │ └── version.rb ├── spec ├── auth_spec.rb ├── base.rb └── client_spec.rb └── strongspace.gemspec /.autotest: -------------------------------------------------------------------------------- 1 | require 'autotest/fsevent' 2 | require 'autotest/growl' 3 | 4 | Autotest.add_discovery { "rspec" } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **.DS_Store 2 | Gemfile.lock 3 | *.gem 4 | .bundle/ -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 ExpanDrive, Inc. 2 | 3 | Portions copyright Heroku, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | Strongspace Ruby Library and Command-line interface 2 | =================================================== 3 | 4 | This is a Ruby library and command line interface to Strongspace online storage. 5 | 6 | For more about Strongspace see . 7 | 8 | For full documentation see . 9 | 10 | 11 | Usage 12 | ----- 13 | 14 | Install: 15 | `gem install strongspace` 16 | 17 | Run via command line: 18 | `strongspace help` or `ss help` 19 | 20 | === General Commands 21 | help # show this usage 22 | version # show the gem version 23 | 24 | upload # upload a file 25 | download # download a file from Strongspace to the current directory 26 | mkdir # create a folder on Strongspace 27 | delete # delete a file or recursively delete a folder on Strongspace 28 | quota # Show the filesystem quota information 29 | 30 | === SSH Keys 31 | keys # show your user's public keys 32 | keys:add [] # Add an public key or generate a new SSH keypair and add 33 | keys:generate # Generate a new SSH keypair 34 | keys:remove # remove a key by id 35 | keys:clear # remove all keys 36 | 37 | === Spaces 38 | spaces # show your user's spaces 39 | spaces:create [type] # add a new space. type => (normal,public,backup) 40 | spaces:delete [type] # remove a space by and destroy its data 41 | spaces:snapshots # show a space's snapshots 42 | spaces:create_snapshot [snapshot_name] # take a space of a space - snapshot_name defaults to current date/time. 43 | spaces:delete_snapshot # remove a snapshot from a space 44 | 45 | === Plugins 46 | plugins # list installed plugins 47 | plugins:install # install the plugin from the specified git url 48 | plugins:uninstall # remove the specified plugin 49 | 50 | 51 | Plugins 52 | ------ 53 | Check out our sample [Rsync Plugin](https://github.com/expandrive/strongspace-rsync) as an example of how to add functionality 54 | 55 | 56 | Meta 57 | ----- 58 | 59 | Many thanks to the Heroku team, whose fantastic Heroku gem provided the basis for this project. 60 | 61 | Released under the [MIT license](http://www.opensource.org/licenses/mit-license.php). 62 | -------------------------------------------------------------------------------- /autotest/discover.rb: -------------------------------------------------------------------------------- 1 | Autotest.add_discovery { "rspec" } 2 | -------------------------------------------------------------------------------- /bin/as_installed/ss: -------------------------------------------------------------------------------- 1 | #!/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby 2 | # 3 | # This file was generated by RubyGems. 4 | # 5 | # The application 'strongspace' is installed as part of a gem, and 6 | # this file is here to facilitate running it. 7 | # 8 | 9 | require 'rubygems' 10 | 11 | version = ">= 0" 12 | 13 | if ARGV.first =~ /^_(.*)_$/ and Gem::Version.correct? $1 then 14 | version = $1 15 | ARGV.shift 16 | end 17 | 18 | gem 'strongspace', version 19 | 20 | begin 21 | load Gem.bin_path('strongspace', 'strongspace', version) 22 | rescue 23 | load 'strongspace' 24 | end -------------------------------------------------------------------------------- /bin/as_installed/strongspace: -------------------------------------------------------------------------------- 1 | #!/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby 2 | # 3 | # This file was generated by RubyGems. 4 | # 5 | # The application 'strongspace' is installed as part of a gem, and 6 | # this file is here to facilitate running it. 7 | # 8 | 9 | require 'rubygems' 10 | 11 | version = ">= 0" 12 | 13 | if ARGV.first =~ /^_(.*)_$/ and Gem::Version.correct? $1 then 14 | version = $1 15 | ARGV.shift 16 | end 17 | 18 | gem 'strongspace', version 19 | 20 | begin 21 | load Gem.bin_path('strongspace', 'strongspace', version) 22 | rescue 23 | load 'strongspace' 24 | end -------------------------------------------------------------------------------- /bin/ss: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | lib = File.expand_path(File.dirname(__FILE__) + '/../lib') 4 | $LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib) 5 | 6 | require 'strongspace' 7 | require 'strongspace/command' 8 | 9 | args = ARGV.dup 10 | ARGV.clear 11 | command = args.shift.strip rescue 'help' 12 | 13 | Strongspace::Command.run(command, args) 14 | 15 | -------------------------------------------------------------------------------- /bin/strongspace: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | lib = File.expand_path(File.dirname(__FILE__) + '/../lib') 4 | $LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib) 5 | 6 | require 'strongspace' 7 | require 'strongspace/command' 8 | 9 | args = ARGV.dup 10 | ARGV.clear 11 | command = args.shift.strip rescue 'help' 12 | 13 | Strongspace::Command.run(command, args) 14 | 15 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + "/lib/strongspace.rb" 2 | -------------------------------------------------------------------------------- /lib/strongspace.rb: -------------------------------------------------------------------------------- 1 | module Strongspace; end 2 | 3 | require 'rest_client' 4 | require 'uri' 5 | require 'json/pure' unless {}.respond_to?(:to_json) 6 | require 'fileutils' 7 | 8 | STRONGSPACE_LIB_PATH = File.dirname(__FILE__) + "/strongspace/" 9 | 10 | [ 11 | "version", 12 | "exceptions", 13 | "client", 14 | "helpers", 15 | "plugin_interface", 16 | "plugin", 17 | "command", 18 | "commands/base" 19 | ].each do |library| 20 | require STRONGSPACE_LIB_PATH + library 21 | end 22 | 23 | 24 | Dir["#{STRONGSPACE_LIB_PATH}/commands/*.rb"].each { |c| require c } 25 | -------------------------------------------------------------------------------- /lib/strongspace/client.rb: -------------------------------------------------------------------------------- 1 | # A Ruby class to call the Strongspace REST API. You might use this if you want to 2 | # manage your Strongspace apps from within a Ruby program, such as Capistrano. 3 | # 4 | # Example: 5 | # 6 | # require 'strongspace' 7 | # strongspace = Strongspace::Client.new('me@example.com', 'mypass') 8 | # 9 | class Strongspace::Client 10 | def self.version 11 | Strongspace::VERSION 12 | end 13 | 14 | def self.gem_version_string 15 | "strongspace-gem/#{version}" 16 | end 17 | 18 | attr_accessor :host, :user, :password 19 | 20 | def self.auth(user, password, host='https://www.strongspace.com') 21 | begin 22 | client = new(user, password, host) 23 | return JSON.parse client.get('/api/v1/api_token', :username => user, :password => password).to_s 24 | rescue RestClient::Request::Unauthorized => e 25 | raise Strongspace::Exceptions::InvalidCredentials 26 | rescue SocketError => e 27 | raise Strongspace::Exceptions::NoConnection 28 | end 29 | end 30 | 31 | def username 32 | return nil if !user 33 | 34 | self.user.split("/")[0] 35 | end 36 | 37 | def login_token 38 | doc = JSON.parse get('/api/v1/login_token') 39 | end 40 | 41 | def initialize(user, password, host='https://www.strongspace.com') 42 | @user = user 43 | @password = password 44 | @host = host 45 | end 46 | 47 | # returns a tempfile to the loaded file 48 | def download(path) 49 | RestClient::Request.execute(:method => :get, :url => (@host + '/api/v1/files' + escape(path)), :user => @user, :password => @password, :raw_response => true, :headers => {:accept_encoding => ''}).file 50 | end 51 | 52 | def upload(file, dest_path) 53 | r = resource(@host + '/api/v1/files/' + escape(dest_path[1..-1] + "/" + File.basename(file.path))) 54 | r.post(:file => file) 55 | end 56 | 57 | def mkdir(path) 58 | doc = post("/api/v1/files/#{escape(path[1..-1])}", :op => "mkdir") 59 | end 60 | 61 | def rm(path) 62 | doc = delete("/api/v1/files/#{escape(path[1..-1])}") 63 | end 64 | 65 | def size(path) 66 | doc = JSON.parse get("/api/v1/files/#{escape(path[1..-1])}?op=size") 67 | end 68 | 69 | def spaces 70 | doc = JSON.parse get('/api/v1/spaces') 71 | doc["spaces"] 72 | end 73 | 74 | def delete_space(space_name) 75 | doc = JSON.parse delete("/api/v1/spaces/#{escape(space_name)}").to_s 76 | end 77 | 78 | def create_space(name, type='normal') 79 | doc = JSON.parse post("/api/v1/spaces", :name => name, :type => type) 80 | end 81 | 82 | def get_space(space_name) 83 | doc = JSON.parse get("/api/v1/spaces/#{escape(space_name)}") 84 | end 85 | 86 | def snapshots(space_name) 87 | doc = JSON.parse get("/api/v1/spaces/#{escape(space_name)}/snapshots").to_s 88 | doc["snapshots"] 89 | end 90 | 91 | def delete_snapshot(space_name, snapshot_name) 92 | doc = JSON.parse delete("/api/v1/spaces/#{escape(space_name)}/snapshots/#{escape(snapshot_name)}").to_s 93 | end 94 | 95 | def create_snapshot(space_name, snapshot_name) 96 | doc = JSON.parse post("/api/v1/spaces/#{escape(space_name)}/snapshots", :name => snapshot_name) 97 | end 98 | 99 | def filesystem 100 | doc = JSON.parse get("/api/v1/filesystem").to_s 101 | doc["filesystem"] 102 | end 103 | 104 | # Get the list of ssh public keys for the current user. 105 | def keys 106 | doc = JSON.parse get('/api/v1/ssh_keys') 107 | doc["ssh_keys"] 108 | end 109 | 110 | # Add an ssh public key to the current user. 111 | def add_key(key) 112 | post("/api/v1/ssh_keys", :key => key).to_s 113 | end 114 | 115 | # Remove an existing ssh public key from the current user. 116 | def remove_key(key_id) 117 | delete("/api/v1/ssh_keys/#{key_id}").to_s 118 | end 119 | 120 | # Clear all keys on the current user. 121 | def remove_all_keys 122 | delete("/api/v1/ssh_keys").to_s 123 | end 124 | 125 | ################## 126 | 127 | def resource(uri) 128 | RestClient.proxy = ENV['HTTP_PROXY'] || ENV['http_proxy'] 129 | if uri =~ /^https?/ 130 | RestClient::Resource.new(uri, user, password) 131 | elsif host =~ /^https?/ 132 | RestClient::Resource.new(host, user, password)[uri] 133 | end 134 | end 135 | 136 | def get(uri, extra_headers={}) # :nodoc: 137 | process(:get, uri, extra_headers) 138 | end 139 | 140 | def post(uri, payload="", extra_headers={}) # :nodoc: 141 | process(:post, uri, extra_headers, payload) 142 | end 143 | 144 | def put(uri, payload, extra_headers={}) # :nodoc: 145 | process(:put, uri, extra_headers, payload) 146 | end 147 | 148 | def delete(uri, extra_headers={}) # :nodoc: 149 | process(:delete, uri, extra_headers) 150 | end 151 | 152 | def process(method, uri, extra_headers={}, payload=nil) 153 | headers = strongspace_headers.merge(extra_headers) 154 | args = [method, payload, headers].compact 155 | response = resource(uri).send(*args) 156 | end 157 | 158 | def escape(value) # :nodoc: 159 | escaped = URI.escape(value.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")) 160 | escaped.gsub('.', '%2E') # not covered by the previous URI.escape 161 | end 162 | 163 | def strongspace_headers # :nodoc: 164 | { 165 | 'X-Strongspace-API-Version' => '1', 166 | 'User-Agent' => self.class.gem_version_string, 167 | 'X-Ruby-Version' => RUBY_VERSION, 168 | 'X-Ruby-Platform' => RUBY_PLATFORM 169 | } 170 | end 171 | 172 | end 173 | -------------------------------------------------------------------------------- /lib/strongspace/command.rb: -------------------------------------------------------------------------------- 1 | module Strongspace 2 | module Command 3 | class InvalidCommand < RuntimeError; end 4 | class CommandFailed < RuntimeError; end 5 | 6 | extend Strongspace::Helpers 7 | 8 | class << self 9 | 10 | def run(command, args, retries=0) 11 | Strongspace::Plugin.load! 12 | begin 13 | # internal-only commands cannot be be run from the command line 14 | if command.index(":_") 15 | raise InvalidCommand 16 | end 17 | 18 | run_internal 'auth:reauthorize_interactve', args.dup if retries > 0 19 | run_internal(command, args.dup) 20 | rescue InvalidCommand 21 | error "Unknown command. Run 'strongspace help' for usage information." 22 | rescue Strongspace::Exceptions::InvalidCredentials 23 | if retries < 1 24 | STDERR.puts "Authentication failure" 25 | run(command, args, retries+1) 26 | else 27 | error "! Authentication failure" 28 | end 29 | rescue RestClient::Unauthorized 30 | if retries < 1 31 | STDERR.puts "Authentication failure" 32 | run(command, args, retries+1) 33 | else 34 | error "! Authentication failure" 35 | end 36 | rescue RestClient::ResourceNotFound => e 37 | error extract_not_found(e.http_body) 38 | rescue RestClient::RequestFailed => e 39 | error extract_error(e.http_body) unless e.http_code == 402 40 | rescue RestClient::RequestTimeout 41 | error "API request timed out. Please try again, or contact support@strongspace.com if this issue persists." 42 | rescue CommandFailed => e 43 | error e.message 44 | rescue Interrupt => e 45 | error "\n[canceled]" 46 | end 47 | end 48 | 49 | def run_internal(command, args, strongspace=nil) 50 | if command == "web:start" 51 | require 'strongspace-web' 52 | end 53 | klass, method = parse(command) 54 | runner = klass.new(args, strongspace) 55 | raise InvalidCommand unless runner.respond_to?(method) 56 | runner.send(method) 57 | end 58 | 59 | def parse(command) 60 | parts = command.split(':') 61 | case parts.size 62 | when 1 63 | begin 64 | return eval("Strongspace::Command::#{command.camelize}"), :index 65 | rescue NameError, NoMethodError 66 | return Strongspace::Command::Base, command.to_sym 67 | end 68 | else 69 | begin 70 | const = Strongspace::Command 71 | command = parts.pop 72 | parts.each { |part| const = const.const_get(part.camelize) } 73 | return const, command.to_sym 74 | rescue NameError 75 | raise InvalidCommand 76 | end 77 | end 78 | end 79 | 80 | def extract_not_found(body) 81 | body =~ /^[\w\s]+ not found$/ ? body : "Resource not found" 82 | end 83 | 84 | def extract_error(body) 85 | msg = parse_error_json(body) || 'Internal server error' 86 | msg.split("\n").map { |line| ' ! ' + line }.join("\n") 87 | end 88 | 89 | def parse_error_json(body) 90 | json = JSON.parse(body.to_s) 91 | json['status'] 92 | rescue JSON::ParserError 93 | end 94 | end 95 | end 96 | end 97 | 98 | -------------------------------------------------------------------------------- /lib/strongspace/commands/auth.rb: -------------------------------------------------------------------------------- 1 | module Strongspace::Command 2 | class Auth < Base 3 | attr_accessor :credentials 4 | 5 | def client 6 | @client ||= init_strongspace 7 | end 8 | 9 | def init_strongspace 10 | client = Strongspace::Client.new(user, password, host) 11 | client 12 | end 13 | 14 | # just a stub; will raise if not authenticated 15 | def check 16 | client.spaces 17 | end 18 | 19 | def host 20 | ENV['STRONGSPACE_HOST'] || 'https://www.strongspace.com' 21 | end 22 | 23 | def reauthorize_interactve 24 | @credentials = ask_for_credentials 25 | write_credentials 26 | end 27 | 28 | def authorize! 29 | @credentials = [args.first, args[1]] 30 | r = Strongspace::Client.auth(@credentials[0], @credentials[1], host) 31 | if r 32 | @credentials[0] = r['username'] 33 | @credentials[1] = r['api_token'] 34 | write_credentials 35 | return true 36 | end 37 | 38 | return false 39 | end 40 | 41 | def authenticated_login 42 | if args.blank? 43 | url = "#{host}/login/#{client.login_token['login_token']}" 44 | else 45 | to = URI.escape(args[0][0..1]) + URI.escape(URI.escape(args[0][2..-1])).gsub('&', '%26') 46 | url = "#{host}/login/#{client.login_token['login_token']}?to=#{to}" 47 | end 48 | if running_on_a_mac? 49 | `open "#{url}"` 50 | elsif running_on_windows? 51 | require 'win32ole' 52 | shell = WIN32OLE.new('Shell.Application') 53 | shell.shellExecute(url, '', '', 'open', 3) 54 | end 55 | 56 | end 57 | 58 | def user # :nodoc: 59 | get_credentials 60 | @credentials[0] 61 | end 62 | 63 | def password # :nodoc: 64 | get_credentials 65 | @credentials[1] 66 | end 67 | 68 | def get_credentials # :nodoc: 69 | return if @credentials 70 | unless @credentials = read_credentials 71 | @credentials = ask_for_credentials 72 | save_credentials 73 | end 74 | @credentials 75 | end 76 | 77 | def read_credentials 78 | File.exists?(credentials_file) and File.read(credentials_file).split("\n") 79 | end 80 | 81 | def echo_off 82 | system "stty -echo" 83 | end 84 | 85 | def echo_on 86 | system "stty echo" 87 | end 88 | 89 | def ask_for_credentials 90 | if ENV["STRONGSPACE_DISPLAY"] == "silent" 91 | return [nil, nil] 92 | end 93 | 94 | puts "Enter your Strongspace credentials." 95 | 96 | print "Username or Email: " 97 | user = ask 98 | 99 | print "Password: " 100 | password = running_on_windows? ? ask_for_password_on_windows : ask_for_password 101 | 102 | r = Strongspace::Client.auth(user, password, host) 103 | [r['username'], r['api_token']] 104 | end 105 | 106 | def valid_saved_credentials? 107 | if File.exists?(credentials_file) 108 | credentials = read_credentials 109 | r = Strongspace::Client.auth(credentials[0], credentials[1], host) 110 | return !r.blank? 111 | end 112 | return false 113 | end 114 | 115 | def ask_for_password_on_windows 116 | require "Win32API" 117 | char = nil 118 | password = '' 119 | 120 | while char = Win32API.new("crtdll", "_getch", [ ], "L").Call do 121 | break if char == 10 || char == 13 # received carriage return or newline 122 | if char == 127 || char == 8 # backspace and delete 123 | password.slice!(-1, 1) 124 | else 125 | # windows might throw a -1 at us so make sure to handle RangeError 126 | (password << char.chr) rescue RangeError 127 | end 128 | end 129 | puts 130 | return password 131 | end 132 | 133 | def ask_for_password 134 | echo_off 135 | password = ask 136 | puts 137 | echo_on 138 | return password 139 | end 140 | 141 | def save_credentials 142 | 143 | if args[0] and args[1] 144 | @credentials = [] 145 | @credentials[0] = args[0] 146 | @credentials[1] = args[1] 147 | end 148 | 149 | begin 150 | write_credentials 151 | command = 'auth:check' 152 | Strongspace::Command.run_internal(command, args) 153 | rescue RestClient::Unauthorized => e 154 | delete_credentials 155 | raise e unless retry_login? 156 | 157 | display "\nAuthentication failed" 158 | @credentials = ask_for_credentials 159 | @client = init_strongspace 160 | retry 161 | rescue Exception => e 162 | delete_credentials 163 | raise e 164 | end 165 | end 166 | 167 | def retry_login? 168 | @login_attempts ||= 0 169 | @login_attempts += 1 170 | @login_attempts < 3 171 | end 172 | 173 | def write_credentials 174 | begin 175 | FileUtils.mkdir_p(credentials_folder) 176 | rescue Errno::EEXIST => e 177 | 178 | end 179 | 180 | File.open(credentials_file, 'w') do |f| 181 | f.puts self.credentials 182 | end 183 | set_credentials_permissions 184 | end 185 | 186 | def set_credentials_permissions 187 | FileUtils.chmod 0700, File.dirname(credentials_file) 188 | FileUtils.chmod 0600, credentials_file 189 | end 190 | 191 | def delete_credentials 192 | FileUtils.rm_f(credentials_file) 193 | end 194 | end 195 | end 196 | -------------------------------------------------------------------------------- /lib/strongspace/commands/base.rb: -------------------------------------------------------------------------------- 1 | module Strongspace::Command 2 | class Base 3 | include Strongspace::Helpers 4 | include Strongspace::PluginInterface 5 | 6 | attr_accessor :args 7 | def initialize(args, strongspace=nil) 8 | @args = args 9 | @strongspace = strongspace 10 | end 11 | 12 | def strongspace 13 | @strongspace ||= Strongspace::Command.run_internal('auth:client', args) 14 | end 15 | 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /lib/strongspace/commands/files.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | require 'pathname' 3 | 4 | module Strongspace::Command 5 | class Download < Base 6 | def index 7 | path = Pathname.new(args.first) 8 | display "Downloading #{path.basename}" 9 | tempfile = strongspace.download(path.to_s) 10 | FileUtils.mv(tempfile.path, path.basename) 11 | end 12 | end 13 | 14 | class Upload < Base 15 | def index 16 | 17 | if args.length != 2 18 | error "usage: strongspace upload " 19 | end 20 | 21 | if not File.exist?(args.first) 22 | error "#{args.first} does not exist" 23 | end 24 | 25 | if not File.readable?(args.first) 26 | error "You don't have permissions to read #{args.first}" 27 | end 28 | 29 | display "Uploading #{Pathname.new(args.first).basename} to #{args[1]}" 30 | file = File.new(args.first, 'rb') 31 | strongspace.upload(file, args[1]) 32 | display "Succesfully uploaded #{Pathname.new(args.first).basename}" 33 | end 34 | end 35 | 36 | class Mkdir < Base 37 | def index 38 | if args.length != 1 39 | error "please supply a remote path" 40 | end 41 | 42 | strongspace.mkdir(args[0]) 43 | display "Created #{args[0]}" 44 | end 45 | end 46 | 47 | class Delete < Base 48 | def index 49 | if args.length != 1 50 | error "please supply a remote path" 51 | end 52 | 53 | strongspace.rm(args[0]) 54 | display "Deleted #{args[0]}" 55 | end 56 | end 57 | 58 | class Quota < Base 59 | def index 60 | 61 | f = strongspace.filesystem 62 | display "#{strongspace.username}:" 63 | display " Quota: #{f["quota_gib"]} GiB" 64 | display " Used: #{f["used_gib"]} GiB" 65 | display " Available: #{f["avail_gib"]} GiB" 66 | end 67 | end 68 | 69 | class Size < Base 70 | def index 71 | if args.length != 1 72 | error "please supply a remote path" 73 | end 74 | 75 | r = strongspace.size(args[0]) 76 | puts r 77 | end 78 | end 79 | 80 | end 81 | -------------------------------------------------------------------------------- /lib/strongspace/commands/help.rb: -------------------------------------------------------------------------------- 1 | module Strongspace::Command 2 | class Help < Base 3 | class HelpGroup < Array 4 | attr_reader :title 5 | 6 | def initialize(title) 7 | @title = title 8 | end 9 | 10 | def command(name, description) 11 | self << [name, description] 12 | end 13 | 14 | def space 15 | self << ['', ''] 16 | end 17 | end 18 | 19 | def self.groups 20 | @groups ||= [] 21 | end 22 | 23 | def self.group(title, &block) 24 | groups << begin 25 | group = HelpGroup.new(title) 26 | yield group 27 | group 28 | end 29 | end 30 | 31 | def self.create_default_groups! 32 | group 'General Commands' do |group| 33 | group.command 'help', 'show this usage' 34 | group.command 'version', 'show the gem version' 35 | group.space 36 | group.command 'upload ', 'upload a file' 37 | group.command 'download ', 'download a file from Strongspace to the current directory' 38 | group.command 'mkdir ', 'create a folder on Strongspace' 39 | group.command 'delete ', 'delete a file or recursively delete a folder on Strongspace' 40 | group.command 'quota', 'Show the filesystem quota information' 41 | end 42 | 43 | group 'SSH Keys' do |group| 44 | group.command 'keys', 'show your user\'s public keys' 45 | 46 | if not RUBY_PLATFORM =~ /mswin32|mingw32/ 47 | group.command 'keys:add []', 'Add an public key or generate a new SSH keypair and add' 48 | group.command 'keys:generate', 'Generate a new SSH keypair' 49 | else 50 | group.command 'keys:add []', 'Add an public key' 51 | end 52 | 53 | group.command 'keys:remove ', 'remove a key by id' 54 | group.command 'keys:clear', 'remove all keys' 55 | end 56 | 57 | group 'Spaces' do |group| 58 | group.command 'spaces', 'show your user\'s spaces' 59 | group.command 'spaces:create [type]', 'add a new space. type => (normal,public,backup)' 60 | group.command 'spaces:delete [type]', 'remove a space by and destroy its data' 61 | group.command 'spaces:snapshots ', 'show a space\'s snapshots' 62 | group.command 'spaces:create_snapshot [snapshot_name]', 'take a space of a space - snapshot_name defaults to current date/time.' 63 | group.command 'spaces:delete_snapshot ', 'remove a snapshot from a space' 64 | end 65 | 66 | group 'Plugins' do |group| 67 | group.command 'plugins', 'list installed plugins' 68 | group.command 'plugins:install ', 'install the plugin from the specified git url' 69 | group.command 'plugins:uninstall ', 'remove the specified plugin' 70 | end 71 | 72 | end 73 | 74 | 75 | 76 | def index 77 | display usage 78 | end 79 | 80 | def version 81 | display Strongspace::Client.version 82 | end 83 | 84 | def usage 85 | longest_command_length = self.class.groups.map do |group| 86 | group.map { |g| g.first.length } 87 | end.flatten.max 88 | 89 | self.class.groups.inject(StringIO.new) do |output, group| 90 | output.puts "=== %s" % group.title 91 | 92 | group.each do |command, description| 93 | if command.empty? 94 | output.puts 95 | else 96 | output.puts "%-*s # %s" % [longest_command_length, command, description] 97 | end 98 | end 99 | 100 | output.puts 101 | output 102 | end.string 103 | end 104 | end 105 | end 106 | 107 | Strongspace::Command::Help.create_default_groups! 108 | -------------------------------------------------------------------------------- /lib/strongspace/commands/keys.rb: -------------------------------------------------------------------------------- 1 | module Strongspace::Command 2 | class Keys < Base 3 | def list 4 | long = args.any? { |a| a == '--long' } 5 | keys = strongspace.keys 6 | if keys.empty? 7 | display "No keys for #{strongspace.username}" 8 | else 9 | display "=== #{keys.size} key#{'s' if keys.size > 1} for #{strongspace.username}" 10 | keys.each do |key| 11 | display long ? key["key"].strip : format_key_for_display(key["key"]) + " key-id: #{key["id"]}" 12 | end 13 | end 14 | keys 15 | end 16 | alias :index :list 17 | 18 | def generate 19 | `ssh-keygen` 20 | return ($? == 0) 21 | end 22 | 23 | def generate_for_gui 24 | FileUtils.mkdir "#{credentials_folder}" unless File.exist? "#{credentials_folder}" 25 | 26 | 27 | File.open("#{credentials_folder}/known_hosts", "w") do |f| 28 | f.write "* ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEArXBYAoHZWVzLfHNMlgteAbq20AaCVcE1qALqVjYZerIpa3rBjNlv2i/2O8ul3OmSfcQwQGPTnABLqz9cozAbxF01eDfqUiSABUDT6m1/lY1a0V7RGS46Y/KJMVbOb4mVpxDZOVwBQh/DYTu7R55vFc93lXpE+tZboqnuq+LvJIZDqzoGTHIUprRs3sNY8Xegnz+m68P+tV6iLkXMRk8Gh8/IIavN4mXYhWPVbCv6Gqo2XhiYVMrCqLZFKLG0W6uwWY/xOhUjWxKDZMlqhyU/YUsMB5BZc9/x0t+Sc82OL+Eh3IB5EUmmCWnhm/LKxjMIn2UNe48BQqwaU/gozVtVPQ==\n" 29 | end 30 | 31 | 32 | if !File.exist? "#{credentials_folder}/#{computername}.rsa" 33 | `#{ssh_keygen_binary} -f \"#{credentials_folder}/#{computername}.rsa\" -b 2048 -C \" Strongspace App - #{computername}\" -q -N ""` unless File.exist? "#{credentials_folder}/#{computername}.rsa" 34 | args[0] = "#{credentials_folder}/#{computername}.rsa.pub" 35 | begin 36 | add 37 | rescue RestClient::Conflict => e # Swallow errors if the key already exists on Strongspace 38 | end 39 | end 40 | end 41 | 42 | def add 43 | keyfile = args.first || find_key 44 | key = File.read(keyfile) 45 | 46 | display "Uploading ssh public key #{keyfile}" 47 | 48 | strongspace.add_key(key) 49 | end 50 | 51 | def remove 52 | strongspace.remove_key(args.first) 53 | display "Key #{args.first} removed." 54 | end 55 | 56 | def clear 57 | strongspace.remove_all_keys 58 | display "All keys removed." 59 | end 60 | 61 | def valid_key_gui? 62 | return unless running_on_a_mac? and File.exist? "#{support_directory}/ssh" 63 | 64 | ret = `ssh -o PreferredAuthentications=publickey -i "#{support_directory}/ssh/#{computername}.rsa" #{strongspace.username}@#{strongspace.username}.strongspace.com 2>&1` 65 | 66 | if ret.include? "Strongspace" 67 | display "Valid key installed" 68 | return true 69 | end 70 | display "No valid key installed" 71 | return false 72 | end 73 | 74 | protected 75 | def find_key 76 | %w(rsa dsa).each do |key_type| 77 | keyfile = "#{home_directory}/.ssh/id_#{key_type}.pub" 78 | return keyfile if File.exists? keyfile 79 | end 80 | 81 | 82 | display "No ssh public key found in #{home_directory}/.ssh/id_[rd]sa.pub" 83 | if not running_on_windows? 84 | display " Generate a new key? [yes/no]: ", false 85 | answer = ask("no") 86 | if answer.downcase == "yes" or answer.downcase == "y" 87 | r = Strongspace::Command.run_internal("keys:generate", nil) 88 | if r 89 | return find_key 90 | end 91 | end 92 | end 93 | 94 | raise CommandFailed, "No ssh public key available" 95 | end 96 | 97 | def format_key_for_display(key) 98 | type, hex, local = key.strip.split(/\s/) 99 | [type, hex[0,10] + '...' + hex[-10,10], local].join(' ') 100 | end 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /lib/strongspace/commands/plugins.rb: -------------------------------------------------------------------------------- 1 | module Strongspace::Command 2 | class Plugins < Base 3 | def list 4 | ::Strongspace::Plugin.list.each do |plugin| 5 | display plugin 6 | end 7 | end 8 | alias :index :list 9 | 10 | def install 11 | plugin = Strongspace::Plugin.new(args.shift) 12 | if plugin.install 13 | begin 14 | Strongspace::Plugin.load_plugin(plugin.name) 15 | rescue Exception => ex 16 | installation_failed(plugin, ex.message) 17 | end 18 | display "#{plugin} installed" 19 | else 20 | error "Could not install #{plugin}. Please check the URL and try again" 21 | end 22 | end 23 | 24 | def uninstall 25 | plugin = Strongspace::Plugin.new(args.shift) 26 | plugin.uninstall 27 | display "#{plugin} uninstalled" 28 | end 29 | 30 | protected 31 | 32 | def installation_failed(plugin, message) 33 | plugin.uninstall 34 | error <<-ERROR 35 | Could not initialize #{plugin}: #{message} 36 | ERROR 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/strongspace/commands/spaces.rb: -------------------------------------------------------------------------------- 1 | module Strongspace::Command 2 | class Spaces < Base 3 | def list 4 | long = args.any? { |a| a == '--long' } 5 | spaces = strongspace.spaces 6 | 7 | if spaces.empty? 8 | display "#{strongspace.username} has no spaces" 9 | else 10 | display "=== #{strongspace.username} has #{spaces.size} space#{'s' if spaces.size > 1}" 11 | spaces.each do |space| 12 | display "#{space['name']} [type: #{space['type']}, snapshots: #{space['snapshots']}]" 13 | end 14 | end 15 | end 16 | alias :index :list 17 | 18 | def create 19 | name = args[0] 20 | type = args[1] 21 | 22 | strongspace.create_space(name, type) 23 | display "Create space #{name}" 24 | end 25 | 26 | def delete 27 | strongspace.delete_space(args.first) 28 | display "Space #{args.first} removed." 29 | end 30 | 31 | def snapshots 32 | if args.length == 0 33 | display "No space specified." 34 | return 35 | end 36 | snapshots = strongspace.snapshots(args.first) 37 | 38 | if snapshots.empty? 39 | display "Space #{args.first} has no snapshots" 40 | else 41 | display "=== Space #{args.first} has #{snapshots.size} snapshot#{'s' if snapshots.size > 1}" 42 | snapshots.each do |snapshot| 43 | display "#{args.first}@#{snapshot['name']} [created: #{snapshot['created_at']}]" 44 | end 45 | end 46 | end 47 | 48 | def create_snapshot 49 | space_name, snapshot_name = args[0..1] 50 | 51 | if snapshot_name.blank? 52 | snapshot_name = Time.now.strftime("%Y-%m-%d-%H%M%S") 53 | end 54 | 55 | strongspace.create_snapshot(space_name, snapshot_name) 56 | display "Created snapshot '#{space_name}@#{snapshot_name}'" 57 | end 58 | 59 | def create_snapshot_and_thin 60 | 61 | retries = 0 62 | success = false 63 | while (!success and (retries < 5)) do 64 | begin 65 | create_snapshot 66 | #thin_snapshots 67 | rescue SocketError => e 68 | sleep(10) 69 | retries = retries + 1 70 | next 71 | end 72 | success = true 73 | end 74 | 75 | end 76 | 77 | def delete_snapshot 78 | space_name, snapshot_name = args[0..1] 79 | 80 | strongspace.delete_snapshot(space_name, snapshot_name) 81 | display "Destroyed snapshot '#{space_name}@#{snapshot_name}'" 82 | end 83 | 84 | def thin_snapshots 85 | snapshots = strongspace.snapshots(args.first) 86 | 87 | keeplist = [] 88 | 89 | snapshots.each do |s| 90 | if DateTime.parse(s['created_at']).to_local_time > (Time.now - 3600*24) 91 | keeplist << s 92 | next 93 | end 94 | end 95 | 96 | (snapshots - keeplist).each do |k| 97 | puts "Drop: " + k['name'] 98 | strongspace.delete_snapshot(args.first, k['name']) 99 | end 100 | 101 | end 102 | 103 | 104 | def schedule_snapshots 105 | space_name = args[0] 106 | 107 | if running_on_a_mac? 108 | plist = " 109 | 111 | 112 | 113 | Label 114 | com.strongspace.Snapshots.#{space_name} 115 | Program 116 | #{support_directory}/gems/bin/strongspace 117 | ProgramArguments 118 | 119 | strongspace 120 | spaces:create_snapshot_and_thin 121 | #{space_name} 122 | 123 | KeepAlive 124 | 125 | StartCalendarInterval 126 | 127 | Minute 128 | 0 129 | 130 | RunAtLoad 131 | 132 | StandardOutPath 133 | #{log_file} 134 | StandardErrorPath 135 | #{log_file} 136 | EnvironmentVariables 137 | 138 | STRONGSPACE_DISPLAY 139 | logging 140 | GEM_PATH 141 | #{support_directory}/gems 142 | GEM_HOME 143 | #{support_directory}/gems 144 | RACK_ENV 145 | production 146 | 147 | 148 | 149 | " 150 | 151 | file = File.new(scheduled_launch_file(space_name), "w+") 152 | file.puts plist 153 | file.close 154 | 155 | r = `launchctl load -S aqua '#{scheduled_launch_file(space_name)}'` 156 | if r.strip.ends_with?("Already loaded") 157 | error "This task is aready scheduled, unload before scheduling again" 158 | return 159 | end 160 | display "Scheduled Snapshots of #{space_name}" 161 | end 162 | end 163 | 164 | def unschedule_snapshots 165 | space_name = args[0] 166 | 167 | if space_name.blank? 168 | display "Please supply the name of a space" 169 | return false 170 | end 171 | 172 | if running_on_windows? 173 | error "Scheduling currently isn't supported on Windows" 174 | return 175 | end 176 | 177 | if running_on_a_mac? 178 | if File.exist? scheduled_launch_file(space_name) 179 | `launchctl unload '#{scheduled_launch_file(space_name)}'` 180 | FileUtils.rm(scheduled_launch_file(space_name)) 181 | end 182 | else # Assume we're running on linux/unix 183 | CronEdit::Crontab.Remove "strongspace-snapshots-#{space_name}" 184 | end 185 | 186 | end 187 | 188 | 189 | private 190 | def scheduled_launch_file(space_name) 191 | "#{launchd_agents_folder}/com.strongspace.Snapshots.#{space_name}.plist" 192 | end 193 | 194 | def log_file 195 | "#{logs_folder}/Strongspace.log" 196 | end 197 | 198 | end 199 | end 200 | 201 | -------------------------------------------------------------------------------- /lib/strongspace/commands/version.rb: -------------------------------------------------------------------------------- 1 | module Strongspace::Command 2 | class Version < Base 3 | def index 4 | display Strongspace::Client.gem_version_string 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/strongspace/exceptions.rb: -------------------------------------------------------------------------------- 1 | module Strongspace 2 | module Exceptions 3 | class StrongspaceError < StandardError; end 4 | 5 | class InvalidCredentials < StrongspaceError 6 | def message 7 | "Invalid Strongspace Credentials" 8 | end 9 | end 10 | 11 | class NoConnection < StrongspaceError 12 | def message 13 | "Could not connect to Strongspace" 14 | end 15 | end 16 | end 17 | end -------------------------------------------------------------------------------- /lib/strongspace/helpers.rb: -------------------------------------------------------------------------------- 1 | module Strongspace 2 | module Helpers 3 | 4 | def command_name 5 | self.class.name.split("::").last 6 | end 7 | 8 | def self.home_directory 9 | running_on_windows? ? ENV['APPDATA'] : ENV['HOME'] 10 | end 11 | 12 | def home_directory 13 | return Strongspace::Helpers.home_directory 14 | end 15 | 16 | def self.support_directory 17 | running_on_windows? ? "#{home_directory}\\Strongspace" : "#{home_directory}/Library/Strongspace" 18 | end 19 | 20 | def support_directory 21 | return Strongspace::Helpers.support_directory 22 | end 23 | 24 | def self.running_on_windows? 25 | RUBY_PLATFORM =~ /mswin32|mingw32/ 26 | end 27 | 28 | def running_on_windows? 29 | RUBY_PLATFORM =~ /mswin32|mingw32/ 30 | end 31 | 32 | def self.running_on_a_mac? 33 | RUBY_PLATFORM =~ /-darwin\d/ 34 | end 35 | 36 | def running_on_a_mac? 37 | RUBY_PLATFORM =~ /-darwin\d/ 38 | end 39 | 40 | def gui_ssh_key 41 | "#{credentials_folder}/#{computername}.rsa" 42 | end 43 | 44 | def ssh_keygen_binary 45 | return "/usr/bin/ssh-keygen" if running_on_a_mac? 46 | return "#{support_directory}/bin/ssh-keygen.exe" if running_on_windows? 47 | return "ssh-keygen" 48 | end 49 | 50 | def ssh_binary 51 | return "/usr/bin/ssh" if running_on_a_mac? 52 | return "#{support_directory}/bin/ssh.exe".to_cygpath if running_on_windows? 53 | return "ssh" 54 | end 55 | 56 | def computername 57 | n = File.read(credentials_file).split("\n")[2] 58 | 59 | if n.blank? 60 | if running_on_a_mac? 61 | @computername ||= `system_profiler SPSoftwareDataType | grep "Computer Name"`.split(":").last.nice_slug 62 | elsif running_on_windows? 63 | @computername ||= ENV['COMPUTERNAME'].nice_slug 64 | else 65 | @computername ||= `hostname`.strip.nice_slug 66 | end 67 | 68 | if @computername.length < 3 69 | @computername = ENV['USER'].nice_slug 70 | end 71 | 72 | File.open(credentials_file, 'a') do |f| 73 | f.puts @computername 74 | end 75 | 76 | else 77 | @computername = n.strip 78 | end 79 | 80 | return @computername 81 | end 82 | 83 | def credentials_folder 84 | "#{support_directory}/credentials" 85 | end 86 | 87 | def credentials_file 88 | "#{credentials_folder}/credentials" 89 | end 90 | 91 | def pids_folder 92 | "#{support_directory}/pids" 93 | end 94 | 95 | def plugins_folder 96 | Strongspace::Plugin.directory 97 | end 98 | 99 | def logs_folder 100 | if running_on_a_mac? 101 | "#{home_directory}/Library/Logs/Strongspace" 102 | else 103 | "#{support_directory}/logs" 104 | end 105 | end 106 | 107 | def bin_folder 108 | "#{support_directory}/bin" 109 | end 110 | 111 | def launchd_agents_folder 112 | "#{support_directory}/LaunchAgents" 113 | end 114 | 115 | def pid_file_path(name) 116 | "#{pids_folder}/#{name}" 117 | end 118 | 119 | def pid_from_pid_file(name) 120 | if File.exist?(pid_file_path(name)) 121 | 122 | f = File.open(pid_file_path(name)) 123 | existing_pid = Integer(f.gets) 124 | f.close 125 | 126 | return existing_pid 127 | end 128 | 129 | return nil 130 | end 131 | 132 | def kill_via_pidfile(name) 133 | existing_pid = pid_from_pid_file(name) 134 | 135 | if not existing_pid 136 | return false 137 | end 138 | 139 | begin 140 | # This process is running, Kill 0 is a no-op that only works 141 | # if the process exists 142 | Process.kill(9, existing_pid) 143 | return true 144 | rescue Errno::EPERM 145 | error "No longer have permissions to check this PID" 146 | return 147 | rescue Errno::ESRCH 148 | # Cleanup orphaned pid file and continue on as normal 149 | File.unlink(pid_file_path(name)) 150 | return 151 | rescue 152 | error "Unable to determine status for #{existing_pid} : #{$!}" 153 | return 154 | end 155 | 156 | File.unlink(pid_file_path(name)) 157 | return false 158 | end 159 | 160 | def process_running?(name) 161 | existing_pid = pid_from_pid_file(name) 162 | 163 | if not existing_pid 164 | return false 165 | end 166 | 167 | begin 168 | # This process is running, Kill 0 is a no-op that only works 169 | # if the process exists 170 | Process.kill(0, existing_pid) 171 | return true 172 | rescue Errno::EPERM 173 | error "No longer have permissions to check this PID" 174 | rescue Errno::ESRCH 175 | # Cleanup orphaned pid file and continue on as normal 176 | File.unlink(pid_file_path(name)) 177 | rescue 178 | error "Unable to determine status for #{existing_pid} : #{$!}" 179 | end 180 | 181 | return false 182 | end 183 | 184 | def create_pid_file(name, pid) 185 | 186 | if process_running?(name) 187 | return nil 188 | end 189 | 190 | if not File.exist?(pids_folder) 191 | FileUtils.mkdir_p(pids_folder) 192 | end 193 | 194 | file = File.new(pid_file_path(name), "w") 195 | file.puts "#{pid}" 196 | file.close 197 | 198 | return true 199 | end 200 | 201 | def delete_pid_file(name) 202 | if File.exist?(pid_file_path(name)) 203 | File.unlink(pid_file_path(name)) 204 | end 205 | end 206 | 207 | def display(msg, newline=true) 208 | if ENV["STRONGSPACE_DISPLAY"] == "silent" 209 | return 210 | end 211 | if newline 212 | puts(msg) 213 | else 214 | print(msg) 215 | STDOUT.flush 216 | end 217 | end 218 | 219 | def redisplay(line, line_break = false) 220 | display("\r\e[0K#{line}", line_break) 221 | end 222 | 223 | def error(msg) 224 | STDERR.puts(msg) 225 | exit 1 226 | end 227 | 228 | def confirm(message="Are you sure you wish to continue? (y/n)?") 229 | display("#{message} ", false) 230 | ask.downcase == 'y' 231 | end 232 | 233 | def confirm_command(app = app) 234 | if extract_option('--force') 235 | display("Warning: The --force switch is deprecated, and will be removed in a future release. Use --confirm #{app} instead.") 236 | return true 237 | end 238 | 239 | raise(Strongspace::Command::CommandFailed, "No app specified.\nRun this command from app folder or set it adding --app ") unless app 240 | 241 | confirmed_app = extract_option('--confirm', false) 242 | if confirmed_app 243 | unless confirmed_app == app 244 | raise(Strongspace::Command::CommandFailed, "Confirmed app #{confirmed_app} did not match the selected app #{app}.") 245 | end 246 | return true 247 | else 248 | display "\n ! Potentially Destructive Action" 249 | display " ! To proceed, type \"#{app}\" or re-run this command with --confirm #{@app}" 250 | display "> ", false 251 | if ask.downcase != app 252 | display " ! Input did not match #{app}. Aborted." 253 | false 254 | else 255 | true 256 | end 257 | end 258 | end 259 | 260 | def format_date(date) 261 | date = Time.parse(date) if date.is_a?(String) 262 | date.strftime("%Y-%m-%d %H:%M %Z") 263 | end 264 | 265 | def ask(default=nil) 266 | r = gets.strip 267 | if r.blank? 268 | return default 269 | else 270 | return r 271 | end 272 | end 273 | 274 | def shell(cmd) 275 | FileUtils.cd(Dir.pwd) {|d| return `#{cmd}`} 276 | end 277 | 278 | def space_exist?(name) 279 | strongspace.spaces.each do |space| 280 | # TODO: clean up the json returned by the strongspace API requests to simplify this iteration 281 | return true if space["name"] == name 282 | end 283 | return false 284 | end 285 | 286 | 287 | def backup_space?(name) 288 | space = nil 289 | strongspace.spaces.each do |s| 290 | if s["name"] == name then 291 | space = s 292 | break 293 | end 294 | end 295 | return space["type"] == "backup" 296 | end 297 | 298 | 299 | 300 | end 301 | end 302 | 303 | unless Object.method_defined?(:blank?) 304 | class Object 305 | def blank? 306 | respond_to?(:empty?) ? empty? : !self 307 | end 308 | end 309 | end 310 | 311 | unless String.method_defined?(:starts_with?) 312 | class String 313 | def starts_with?(str) 314 | str = str.to_str 315 | head = self[0, str.length] 316 | head == str 317 | end 318 | end 319 | end 320 | 321 | unless String.method_defined?(:ends_with?) 322 | class String 323 | def ends_with?(str) 324 | str = str.to_str 325 | tail = self[-str.length, str.length] 326 | tail == str 327 | end 328 | end 329 | end 330 | 331 | unless String.method_defined?(:shellescape) 332 | class String 333 | def shellescape 334 | empty? ? "''" : gsub(/([^A-Za-z0-9_\-.,:\/@\n])/n, '\\\\\\1').gsub(/\n/, "'\n'") 335 | end 336 | end 337 | end 338 | 339 | unless String.method_defined?(:camelize) 340 | class String 341 | def camelize 342 | self.split(/[^a-z0-9]/i).map{|w| w.capitalize}.join 343 | end 344 | end 345 | end 346 | 347 | unless String.method_defined?(:underscore) 348 | class String 349 | def underscore 350 | self.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').gsub(/([a-z\d])([A-Z])/,'\1_\2').tr("-", "_").downcase 351 | end 352 | end 353 | end 354 | 355 | unless String.method_defined?(:nice_slug) 356 | class String 357 | def nice_slug 358 | str = self.dup 359 | accents = { 360 | ['á','à','â','ä','ã'] => 'a', 361 | ['Ã','Ä','Â','À','Á'] => 'A', 362 | ['é','è','ê','ë'] => 'e', 363 | ['Ë','É','È','Ê'] => 'E', 364 | ['í','ì','î','ï'] => 'i', 365 | ['Í','Î','Ì','Ï'] => 'I', 366 | ['ó','ò','ô','ö','õ'] => 'o', 367 | ['Õ','Ö','Ô','Ò','Ó'] => 'O', 368 | ['ú','ù','û','ü'] => 'u', 369 | ['Ú','Û','Ù','Ü'] => 'U', 370 | ['ç'] => 'c', ['Ç'] => 'C', 371 | ['ñ'] => 'n', ['Ñ'] => 'N' 372 | } 373 | accents.each do |ac,rep| 374 | ac.each do |s| 375 | str = str.gsub(s, rep) 376 | end 377 | end 378 | str = str.gsub(/[^a-zA-Z0-9\. ]/,"") 379 | str = str.gsub(/[ ]+/," ") 380 | str = str.gsub(/[_]/,"-") 381 | end 382 | end 383 | end 384 | 385 | unless String.method_defined?(:to_cygpath) 386 | class String 387 | def to_cygpath 388 | return self unless Strongspace::Command::running_on_windows? 389 | r = "/cygdrive/#{self[0..0].downcase}/#{self[3..-1]}".gsub("\\", "/") 390 | return r 391 | end 392 | end 393 | end 394 | 395 | unless String.method_defined?(:from_cygpath) 396 | class String 397 | def from_cygpath 398 | if self.starts_with? "/cygdrive" and Strongspace::Command::running_on_windows? 399 | return "#{self[10..10]}:\\#{self[12..-1].gsub("/","\\")}" 400 | end 401 | return self 402 | end 403 | end 404 | end 405 | 406 | unless String.method_defined?(:normalize_pathslash) 407 | class String 408 | def normalize_pathslash 409 | return self.gsub("/", "\\") if Strongspace::Command::running_on_windows? 410 | return self 411 | end 412 | end 413 | end 414 | -------------------------------------------------------------------------------- /lib/strongspace/plugin.rb: -------------------------------------------------------------------------------- 1 | module Strongspace 2 | class Plugin 3 | class << self 4 | include Strongspace::Helpers 5 | end 6 | 7 | attr_reader :name, :uri 8 | 9 | def self.directory 10 | File.expand_path("#{support_directory}/plugins") 11 | end 12 | 13 | def self.list 14 | Dir["#{directory}/*"].sort.map do |folder| 15 | File.basename(folder) 16 | end 17 | end 18 | 19 | def self.load! 20 | self.update_support_directory! 21 | list.each do |plugin| 22 | begin 23 | load_plugin(plugin) 24 | rescue Exception => e 25 | display "Unable to load plugin: #{plugin}: #{e.message}" 26 | end 27 | end 28 | self.load_default_gem_plugins 29 | end 30 | 31 | def self.load_default_gem_plugins 32 | begin 33 | require File.dirname(__FILE__) + '/../../../strongspace-rsync/lib/strongspace-rsync' 34 | rescue Exception => e 35 | begin 36 | require 'strongspace-rsync' 37 | rescue Exception => e 38 | end 39 | end 40 | begin 41 | require File.dirname(__FILE__) + '/../../../strongspace-web/lib/strongspace-web' 42 | rescue Exception => e 43 | begin 44 | require 'strongspace-web' 45 | rescue Exception => ee 46 | end 47 | end 48 | end 49 | 50 | def self.load_plugin(plugin) 51 | folder = "#{self.directory}/#{plugin}" 52 | $: << "#{folder}/lib" if File.directory? "#{folder}/lib" 53 | load "#{folder}/init.rb" if File.exists? "#{folder}/init.rb" 54 | end 55 | 56 | def self.remove_plugin(plugin) 57 | FileUtils.rm_rf("#{self.directory}/#{plugin}") 58 | end 59 | 60 | def self.update_support_directory! 61 | if running_on_a_mac? 62 | # if File.exist?("#{home_directory}/.strongspace") and !File.exist?("#{support_directory}") 63 | # FileUtils.mv("#{home_directory}/.strongspace", "#{support_directory}") 64 | # end 65 | 66 | FileUtils.mkdir_p(launchd_agents_folder) unless File.exist? launchd_agents_folder 67 | end 68 | end 69 | 70 | def initialize(uri) 71 | @uri = uri 72 | guess_name(uri) 73 | end 74 | 75 | def to_s 76 | name 77 | end 78 | 79 | def path 80 | "#{self.class.directory}/#{name}" 81 | end 82 | 83 | def install 84 | FileUtils.mkdir_p(path) 85 | Dir.chdir(path) do 86 | system("git init -q") 87 | if !system("git pull #{uri} master -q") 88 | FileUtils.rm_rf path 89 | return false 90 | end 91 | end 92 | true 93 | end 94 | 95 | def uninstall 96 | FileUtils.rm_r path if File.directory?(path) 97 | end 98 | 99 | private 100 | def guess_name(url) 101 | @name = File.basename(url) 102 | @name = File.basename(File.dirname(url)) if @name.empty? 103 | @name.gsub!(/\.git$/, '') if @name =~ /\.git$/ 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /lib/strongspace/plugin_interface.rb: -------------------------------------------------------------------------------- 1 | module Strongspace::PluginInterface 2 | 3 | def self.included(base) 4 | base.extend Strongspace::PluginInterface 5 | end 6 | 7 | def command(command, *args) 8 | Strongspace::Command.run_internal command.to_s, args 9 | end 10 | 11 | def base_command 12 | @base_command ||= Strongspace::Command::Base.new(ARGV) 13 | end 14 | 15 | end 16 | -------------------------------------------------------------------------------- /lib/strongspace/version.rb: -------------------------------------------------------------------------------- 1 | module Strongspace 2 | VERSION = "0.3.7" 3 | end 4 | -------------------------------------------------------------------------------- /spec/auth_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path("./base", File.dirname(__FILE__)) 2 | 3 | require "strongspace" 4 | 5 | def prepare_command(klass) 6 | command = klass.new(['--app', 'myapp']) 7 | command.stub!(:args).and_return([]) 8 | command.stub!(:display) 9 | command 10 | end 11 | 12 | 13 | 14 | describe Strongspace::Command::Auth do 15 | 16 | 17 | 18 | 19 | end -------------------------------------------------------------------------------- /spec/base.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | # ruby 1.9.2 drops . from the load path 4 | $:.unshift File.expand_path("../..", __FILE__) 5 | 6 | require 'spec' 7 | require 'fileutils' 8 | require 'tmpdir' 9 | require 'webmock/rspec' 10 | 11 | require 'strongspace' 12 | 13 | include WebMock::API 14 | 15 | def stub_api_request(method, path) 16 | stub_request(method, "https://www.strongspace.com/api/v1#{path}") 17 | end -------------------------------------------------------------------------------- /spec/client_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path("./base", File.dirname(__FILE__)) 2 | require "cgi" 3 | require "strongspace" 4 | 5 | describe Strongspace::Client do 6 | before do 7 | @client = Strongspace::Client.new(nil, nil) 8 | end 9 | 10 | it "should return the current version" do 11 | Strongspace::Client.version.should == Strongspace::VERSION 12 | end 13 | 14 | it "should return a gem version string" do 15 | Strongspace::Client.gem_version_string.should == "strongspace-gem/#{Strongspace::VERSION}" 16 | end 17 | 18 | it "should return an API key hash for auth" do 19 | api_token = { "api_key" => "abc", "username" => "foo/token" } 20 | stub_request(:get, "https://foo:bar@www.strongspace.com/api/v1/api_token").to_return(:body => api_token.to_json) 21 | Strongspace::Client.auth("foo", "bar").should == api_token 22 | end 23 | 24 | it "should fail auth gracefully with a bad password" do 25 | api_token = { "api_key" => "abc", "username" => "foo/token" } 26 | stub_request(:get, "https://foo:bar@www.strongspace.com/api/v1/api_token").to_return(:body => api_token.to_json) 27 | lambda {Strongspace::Client.auth("foo", "ba3r")}.should raise_error(WebMock::NetConnectNotAllowedError) 28 | end 29 | 30 | it "should return nil for username and password" do 31 | @client.username.should == nil 32 | @client.password.should == nil 33 | end 34 | 35 | it "should return an array of spaces" do 36 | stub_api_request(:get, "/spaces").to_return(:body => <<-EOJSON) 37 | {"spaces" : [{"space":{"name":"a space", "snapshots":0, "type":"normal"}}, {"space":{"name":"diskimages", "snapshots":0, "type":"normal"}}]} 38 | EOJSON 39 | @client.spaces.should == [{"space"=>{"name"=>"a space", "snapshots"=>0, "type"=>"normal"}}, {"space"=>{"name"=>"diskimages", "snapshots"=>0, "type"=>"normal"}}] 40 | end 41 | 42 | end -------------------------------------------------------------------------------- /strongspace.gemspec: -------------------------------------------------------------------------------- 1 | $:.unshift File.expand_path("../lib", __FILE__) 2 | 3 | Gem::Specification.new do |gem| 4 | gem.name = "strongspace" 5 | gem.version = "0.3.7" 6 | 7 | gem.author = "Strongspace" 8 | gem.email = "support@strongspace.com" 9 | gem.homepage = "https://www.strongspace.com/" 10 | 11 | gem.summary = "Client library and CLI for Strongspace." 12 | gem.description = "Client library and command line tool for Strongspace." 13 | gem.homepage = "http://github.com/expandrive/strongspace" 14 | gem.executables = ["strongspace", "ss"] 15 | 16 | gem.files = Dir["**/*"].select { |d| d =~ %r{^(README|bin/|data/|ext/|lib/|spec/|test/)} } 17 | 18 | gem.add_development_dependency "rake" 19 | gem.add_development_dependency "ZenTest" 20 | gem.add_development_dependency "autotest-growl" 21 | # gem.add_development_dependency "autotest-fsevent" 22 | gem.add_development_dependency "rspec", "~> 1.3.0" 23 | gem.add_development_dependency "webmock", "~> 1.5.0" 24 | # gem.add_development_dependency "ruby-fsevent" 25 | 26 | 27 | gem.add_development_dependency "sinatra" 28 | gem.add_development_dependency "sinatra-reloader" 29 | gem.add_development_dependency "hoptoad_notifier", " ~> 2.4" 30 | 31 | gem.add_dependency "open4" # this is required for non-windows 32 | # gem.add_dependency "win32-open3" # this is required for windows, will fail elsehwere - comment out 33 | gem.add_dependency "POpen4" 34 | gem.add_dependency "cronedit" 35 | gem.add_dependency "rest-client", "1.6.1" 36 | gem.add_dependency "json_pure", "1.8.3" 37 | end 38 | --------------------------------------------------------------------------------