clouddrive 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8d64073347f2f8b488312ea58fad514dba3c1cb6
4
+ data.tar.gz: 8395374122e477a41de9987861a7f1fc4b013329
5
+ SHA512:
6
+ metadata.gz: 8781961a4a644e9839084f17e6f234326ab00548864c3fb7dc67c824f49f069c6bc570fd82bc20350e15b2847e8add9f23b70f6095688a219bfa6cee48b04ba7
7
+ data.tar.gz: 3ea157a6089a9abf9310addd4d027343e00ab7bbf000fcc0e415301fadaf4705074386b6574fa90af4c824fe44741009a3f0f7ce4c1f5e57b49188e931a6f697
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /.idea/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.1
@@ -0,0 +1,13 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
4
+
5
+ We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion.
6
+
7
+ Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
8
+
9
+ Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
10
+
11
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
12
+
13
+ This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in clouddrive.gemspec
4
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 TODO: Write your name
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.
@@ -0,0 +1,73 @@
1
+ # CloudDrive SDK and CLI
2
+
3
+ This i a Ruby project built to interact with Amazon's CloudDrive API. It works as both an SDK and a CLI in the sense that I've built the code to easily be implemented in your own projects but it also includes an executable to run many common processes right from the command line.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'clouddrive'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install clouddrive
20
+
21
+ ## CLI Usage
22
+
23
+ The CLI is used by running `clouddrive` with one of the following commands followed by any necessary arguments (use `help` argument before any of the following commands for more information).
24
+
25
+ ```bash
26
+ init Initialize the CLI with your Amazon email and CloudDrive API credentials
27
+ sync Sync the local cache with Amazon CloudDrive
28
+ clearcache Clear the local cache
29
+ metadata Output JSON-formatted metadata related to the remote file give its remote path
30
+ ```
31
+
32
+ ## SDK Usage
33
+
34
+ ### Account
35
+
36
+ #### Initialization
37
+
38
+ The CloudDrive SDK first needs have an authenticated `Account` object which can then be passed into the different classes for API calls.
39
+
40
+ The `Account` class is created by passing in the `email`, `client_id`, and `client_secret` into the constructor and calling the `authorize` method. This will handle authorizing and (if necessary) renewing authorization.
41
+
42
+ The initial `authorize` method call will return false with an `auth_url` key in its `data`. This URL can then be passed into a second `authorize` call which will parse out the `code` parameter and complete the initial OAuth process.
43
+
44
+ ```ruby
45
+ account = CloudDrive::Account.new("me@example.com", "my-client-id", "clientsecret")
46
+ account.authorize
47
+ ...
48
+ account.authorize(auth_url)
49
+ ```
50
+
51
+ The `authorize` method call will still need to be called periodically to renew its authorization as the OAuth token expires every 60 minutes.
52
+
53
+ #### Local Cache
54
+
55
+ By default, the SDK stores all necessary information (OAuth token information, local account caches, etc) into `~/.clouddrive`. Each account (email), has its own cache file and local cache database of its remote filesystem. Once authenticated, the local cache is initially synced with the remote CloudDrive by calling the `sync` method.
56
+
57
+ ```ruby
58
+ account.sync
59
+ ```
60
+
61
+ Every time `sync` is called, it will update the local cache with all changes since the last `sync` call. The local cache can be cleared out and reset by calling `clear_cache`.
62
+
63
+ ```ruby
64
+ account.clear_cache
65
+ ```
66
+
67
+ ## Contributing
68
+
69
+ 1. Fork it ( https://github.com/[my-github-username]/clouddrive/fork )
70
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
71
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
72
+ 4. Push to the branch (`git push origin my-new-feature`)
73
+ 5. Create a new Pull Request
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,151 @@
1
+ require 'thor'
2
+ require 'clouddrive'
3
+ require 'yaml'
4
+ require 'json'
5
+
6
+ module CloudDrive
7
+ class CLI < Thor
8
+
9
+ def initialize(*args)
10
+ super
11
+ @config_path = File.expand_path('~/.clouddrive') + "/"
12
+ end
13
+
14
+ desc "init", "Initialize app with Amazon e-mail and CloudDrive credentials"
15
+ option :email, :aliases => :e
16
+ option :client_id, :aliases => :i
17
+ option :client_secret, :aliases => :s
18
+ option :auth_url, :aliases => :u
19
+ def init
20
+ email = options[:email]
21
+ client_id = options[:client_id]
22
+ client_secret = options[:client_secret]
23
+
24
+ config = read_config
25
+
26
+ if email != nil
27
+ config[:email] = email
28
+ end
29
+
30
+ if client_id != nil
31
+ config[:client_id] = client_id
32
+ end
33
+
34
+ if client_secret != nil
35
+ config[:client_secret] = client_secret
36
+ end
37
+
38
+ if config[:email] == nil
39
+ puts "Email is required for authorization"
40
+ exit
41
+ end
42
+
43
+ if config[:client_id] == nil || config[:client_secret] == nil
44
+ puts "Amazon CloudDrive API credentials required"
45
+ exit
46
+ end
47
+
48
+ save_config(config)
49
+
50
+ account = CloudDrive::Account.new(config[:email], config[:client_id], config[:client_secret])
51
+ result = account.authorize
52
+ if result[:success] === false
53
+ if result[:data]["message"] == "Initial authorization required."
54
+ puts "Initiali authorization required. Navigate to the following URL and paste in the redirect URL here."
55
+ url = ask(result[:data]["auth_url"] + "\n")
56
+ result = account.authorize(url)
57
+ if result[:success]
58
+ puts "Successfully authenticated with Amazon CloudDrive"
59
+ else
60
+ puts "Failed to authenticate with Amazon CloudDrive: #{result[:data].to_json}"
61
+ end
62
+ end
63
+ else
64
+ puts "Already authenticated with Amazon CloudDrive"
65
+ end
66
+ end
67
+
68
+ desc "sync", "Sync local cache with Amazon CloudDrive"
69
+ long_desc <<'LONGDESC'
70
+ Syncing the nodes from Amazon CloudDrive to a local database allows for quicker
71
+ API calls as well as the ability for some operations to be performed 'offline'.
72
+ Before running any commands that alter data remotely, the local cache should
73
+ always be synced up.
74
+ LONGDESC
75
+ def sync
76
+ setup
77
+ @account.authorize
78
+ @account.sync
79
+ end
80
+
81
+ desc "clearcache", "Clear local nodes cache"
82
+ def clearcache
83
+ setup
84
+ @account.authorize
85
+ @account.clear_cache
86
+ end
87
+
88
+ desc 'metadata REMOTE_PATH', 'Retrieve the node\'s metadata given its remote path'
89
+ def metadata(path)
90
+ setup
91
+ @account.authorize
92
+ api = CloudDrive::Node.new(@account)
93
+
94
+ if (node = api.find_by_path(path)) != nil
95
+ puts node.to_json
96
+ else
97
+ puts "File does not exist."
98
+ end
99
+ end
100
+
101
+ desc "mkdir REMOTE_PATH", "Create (recursively) a new remote directory"
102
+ def mkdir(path)
103
+ setup
104
+ @account.authorize
105
+ node = CloudDrive::Node.new(@account)
106
+
107
+ result = node.create_directory_path(path)
108
+ if result[:success]
109
+ puts "Successfully created new directory path: #{result[:data].to_json}"
110
+ else
111
+ puts "Failed to create new directory path: #{result[:data].to_json}"
112
+ end
113
+ end
114
+
115
+ private
116
+
117
+ def setup
118
+ config = read_config
119
+ @account = CloudDrive::Account.new(config[:email], config[:client_id], config[:client_secret])
120
+ end
121
+
122
+ def read_config
123
+ if File.exists?("#{@config_path}config.yaml")
124
+ return YAML.load_file("#{@config_path}config.yaml")
125
+ else
126
+ if !File.exists?(@config_path)
127
+ Dir.mkdir(@config_path)
128
+ end
129
+ end
130
+
131
+ {
132
+ :email => nil,
133
+ :client_id => nil,
134
+ :client_secret => nil
135
+ }
136
+ end
137
+
138
+ def save_config config
139
+ if !File.exists?(@config_path)
140
+ Dir.mkdir(@config_path)
141
+ end
142
+
143
+ File.open("#{@config_path}config.yaml", 'w') do |file|
144
+ file.write(config.to_yaml)
145
+ end
146
+ end
147
+
148
+ end
149
+ end
150
+
151
+ CloudDrive::CLI.start( ARGV )
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "clouddrive"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,36 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'clouddrive/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "clouddrive"
8
+ spec.version = CloudDrive::VERSION
9
+ spec.authors = ["Alex Phillips"]
10
+ spec.email = ["exonintrendo@gmail.com"]
11
+
12
+ spec.summary = %q{CloudDrive for Ruby}
13
+ spec.description = %q{Ruby SDK and command line application for Amazon's CloudDrive}
14
+ spec.homepage = "https://github.com/exonintrendo/clouddrive"
15
+ spec.license = "MIT"
16
+
17
+ # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
18
+ # delete this section to allow pushing this gem to any host.
19
+ # if spec.respond_to?(:metadata)
20
+ # spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'"
21
+ # else
22
+ # raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
23
+ # end
24
+
25
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } + Dir['lib/**/*.rb']
26
+ spec.bindir = "bin"
27
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_development_dependency "bundler", "~> 1.9"
31
+ spec.add_development_dependency "rake", "~> 10.0"
32
+
33
+ spec.add_runtime_dependency "rest-client", "~> 1.8"
34
+ spec.add_runtime_dependency "thor", "~> 0.19"
35
+ spec.add_runtime_dependency "sqlite3", "~>1.3"
36
+ end
@@ -0,0 +1,7 @@
1
+ require "clouddrive/version"
2
+ require "clouddrive/account"
3
+ require "clouddrive/node"
4
+
5
+ module CloudDrive
6
+ # Your code goes here...
7
+ end
@@ -0,0 +1,307 @@
1
+ require 'json'
2
+ require 'cgi'
3
+ require 'rest-client'
4
+ require 'sqlite3'
5
+
6
+ module CloudDrive
7
+
8
+ class Account
9
+
10
+ attr_reader :access_token, :metadata_url, :content_url, :email, :token_store, :db
11
+
12
+ def initialize(email, client_id, client_secret)
13
+ @email = email
14
+ @cache_file = File.expand_path("~/.clouddrive/#{email}.cache")
15
+ @client_id = client_id
16
+ @client_secret = client_secret
17
+
18
+ @db = SQLite3::Database.new(File.expand_path("~/.clouddrive/#{@email}.db"))
19
+ if @db.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='nodes';").empty?
20
+ @db.execute <<-SQL
21
+ CREATE TABLE nodes(
22
+ id VARCHAR PRIMARY KEY NOT NULL,
23
+ name VARCHAR NOT NULL,
24
+ kind VARCHAR NOT NULL,
25
+ md5 VARCHAR,
26
+ created DATETIME NOT NULL,
27
+ modified DATETIME NOT NULL,
28
+ raw_data TEXT NOT NULL
29
+ );
30
+ SQL
31
+ end
32
+ end
33
+
34
+ def authorize(auth_url = nil)
35
+ retval = {
36
+ :success => true,
37
+ :data => {}
38
+ }
39
+
40
+ @token_store = {
41
+ "checkpoint" => nil,
42
+ "nodes" => {}
43
+ }
44
+
45
+ if File.exists?(@cache_file)
46
+ @token_store = JSON.parse(File.read(@cache_file))
47
+ end
48
+
49
+ if !@token_store.has_key?("access_token")
50
+ if auth_url.nil?
51
+ retval = {
52
+ :success => false,
53
+ :data => {
54
+ "message" => "Initial authorization required",
55
+ "auth_url" => "https://www.amazon.com/ap/oa?client_id=#{@client_id}&scope=clouddrive%3Aread%20clouddrive%3Awrite&response_type=code&redirect_uri=http://localhost"
56
+ }
57
+ }
58
+
59
+ return retval
60
+ else
61
+ data = request_authorization(auth_url)
62
+ end
63
+
64
+ if data[:success] === true
65
+ @token_store = data[:data]
66
+ else
67
+ return data
68
+ end
69
+
70
+ save_token_store
71
+ elsif (Time.new.to_i - @token_store["last_authorized"]) > 60
72
+ data = renew_authorization
73
+ if data[:success] === false
74
+ return data
75
+ end
76
+ end
77
+
78
+ @access_token = @token_store["access_token"]
79
+
80
+ if !@token_store.has_key?("metadataUrl") || !@token_store.has_key?("contentUrl")
81
+ result = get_endpoint
82
+ if result[:success] === true
83
+ @metadata_url = result[:data]["metadataUrl"]
84
+ @content_url = result[:data]["contentUrl"]
85
+ @token_store["contentUrl"] = @content_url
86
+ @token_store["metadataUrl"] = @metadata_url
87
+
88
+ save_token_store
89
+ end
90
+ end
91
+
92
+ @metadata_url = @token_store["metadataUrl"]
93
+ @content_url = @token_store["contentUrl"]
94
+
95
+ retval
96
+ end
97
+
98
+ def clear_cache
99
+ @token_store["nodes"] = {}
100
+ @token_store["checkpoint"] = nil
101
+ save_token_store
102
+ end
103
+
104
+ def get_endpoint
105
+ retval = {
106
+ :success => false,
107
+ :data => {}
108
+ }
109
+ RestClient.get("https://cdws.us-east-1.amazonaws.com/drive/v1/account/endpoint", {:Authorization => "Bearer #{@access_token}"}) do |response, request, result|
110
+ retval[:data] = JSON.parse(response.body)
111
+ if response.code === 200
112
+ retval[:success] = true
113
+ end
114
+ end
115
+
116
+ retval
117
+ end
118
+
119
+ def get_quota
120
+ retval = {
121
+ :success => false,
122
+ :data => {}
123
+ }
124
+
125
+ RestClient.get("#{@metadata_url}account/quota", {:Authorization => "Bearer #{@access_token}"}) do |response, request, result|
126
+ retval[:data] = JSON.parse(response.body)
127
+ if response.code === 200
128
+ retval[:success] = true
129
+ end
130
+ end
131
+
132
+ retval
133
+ end
134
+
135
+ def get_usage
136
+ retval = {
137
+ :success => false,
138
+ :data => {}
139
+ }
140
+
141
+ RestClient.get("#{@metadata_url}account/usage", {:Authorization => "Bearer #{@access_token}"}) do |response, request, result|
142
+ retval[:data] = JSON.parse(response.body)
143
+ if response.code === 200
144
+ retval[:success] = true
145
+ end
146
+ end
147
+
148
+ retval
149
+ end
150
+
151
+ def nodes
152
+ @token_store["nodes"]
153
+ end
154
+
155
+ def request_authorization(auth_url)
156
+ retval = {
157
+ :success => false,
158
+ :data => {}
159
+ }
160
+
161
+ params = CGI.parse(URI.parse(auth_url).query)
162
+ if !params.has_key?('code')
163
+ retval[:data]["message"] = "No authorization code exists in the callback URL: #{params}"
164
+
165
+ return retval
166
+ end
167
+
168
+ code = params["code"]
169
+
170
+ # Get token
171
+ #
172
+ # @TODO: why do I need to do this with code? (i.e., code[0])
173
+ body = {
174
+ 'grant_type' => "authorization_code",
175
+ 'code' => code[0],
176
+ 'client_id' => @client_id,
177
+ 'client_secret' => @client_secret,
178
+ 'redirect_uri' => "http://localhost"
179
+ }
180
+
181
+ RestClient.post("https://api.amazon.com/auth/o2/token", body, :content_type => 'application/x-www-form-urlencoded') do |response, request, result|
182
+ retval[:data] = JSON.parse(response.body)
183
+ if response.code === 200
184
+ retval[:success] = true
185
+ retval[:data]["last_authorized"] = Time.new.to_i
186
+ end
187
+ end
188
+
189
+ retval
190
+ end
191
+
192
+ def renew_authorization
193
+ retval = {
194
+ :success => false,
195
+ :data => {}
196
+ }
197
+
198
+ body = {
199
+ 'grant_type' => "refresh_token",
200
+ 'refresh_token' => @token_store["refresh_token"],
201
+ 'client_id' => @client_id,
202
+ 'client_secret' => @client_secret,
203
+ 'redirect_uri' => "http://localhost"
204
+ }
205
+ RestClient.post("https://api.amazon.com/auth/o2/token", body, :content_type => 'application/x-www-form-urlencoded') do |response, request, result|
206
+ retval[:data] = JSON.parse(response.body)
207
+ if response.code === 200
208
+ retval[:success] = true
209
+
210
+ @token_store["last_authorized"] = Time.new.to_i
211
+ @token_store["refresh_token"] = retval[:data]["refresh_token"]
212
+ @token_store["access_token"] = retval[:data]["access_token"]
213
+
214
+ @access_token = @token_store["access_token"]
215
+ @refresh_token = @token_store["refresh_token"]
216
+
217
+ save_token_store
218
+ end
219
+ end
220
+
221
+ retval
222
+ end
223
+
224
+ def save_token_store
225
+ File.open(@cache_file, 'w') do |file|
226
+ file.write(@token_store.to_json)
227
+ end
228
+ end
229
+
230
+ def sync
231
+ if !@token_store.has_key?("checkpoint")
232
+ @token_store["checkpoint"] = nil
233
+ end
234
+
235
+ body = {
236
+ :includePurged => "true"
237
+ }
238
+
239
+ loop do
240
+ if @token_store["checkpoint"] != nil
241
+ body[:checkpoint] = @token_store["checkpoint"]
242
+ end
243
+
244
+ loop = true
245
+ RestClient.post(@metadata_url + "changes", body.to_json, :Authorization => "Bearer #{@access_token}") do |response, request, result|
246
+ if response.code === 200
247
+ data = response.body.split("\n")
248
+ data.each do |xary|
249
+ xary = JSON.parse(xary)
250
+ if xary.has_key?("reset") && xary["reset"] == true
251
+ @db.execute("DELETE FROM nodes WHERE 1=1")
252
+ end
253
+
254
+ if xary.has_key?("end") && xary["end"] == true
255
+ loop = false
256
+ elsif xary.has_key?("nodes")
257
+ @token_store["checkpoint"] = xary["checkpoint"]
258
+ xary["nodes"].each do |node|
259
+ if node["status"] == "PURGED"
260
+ delete_node_by_id(node["id"])
261
+ else
262
+ save_node(node)
263
+ end
264
+ end
265
+ end
266
+ end
267
+ end
268
+ end
269
+
270
+ break if loop === false
271
+ end
272
+
273
+ save_token_store
274
+ end
275
+
276
+ def delete_node_by_id(id)
277
+ begin
278
+ @db.execute("DELETE FROM nodes WHERE id = ?", id)
279
+ rescue SQLite3::Exception => e
280
+ puts "Exception deleting node with ID #{id}: #{e}"
281
+ end
282
+ end
283
+
284
+ def save_node(node)
285
+ md5 = nil
286
+ if node["contentProperties"] != nil && node["contentProperties"]["md5"] != nil
287
+ md5 = node["contentProperties"]["md5"]
288
+ end
289
+
290
+ if node["name"] == nil && node["isRoot"] != nil && node["isRoot"] == true
291
+ node["name"] = "root"
292
+ end
293
+
294
+ begin
295
+ result = db.execute("INSERT OR REPLACE INTO nodes (id, name, kind, md5, created, modified, raw_data)
296
+ VALUES (?, ?, ?, ?, ?, ?, ?);", [node["id"], node["name"], node["kind"], md5, node["createdDate"], node["modifiedDate"], node.to_json])
297
+ rescue SQLite3::Exception => e
298
+ if node["name"] == nil
299
+ puts "Exception saving node: #{e}"
300
+ puts node.to_json
301
+ end
302
+ end
303
+ end
304
+
305
+ end
306
+
307
+ end
@@ -0,0 +1,329 @@
1
+ require 'rest-client'
2
+ require 'pathname'
3
+ require 'find'
4
+ require 'digest/md5'
5
+ require 'json'
6
+
7
+ module CloudDrive
8
+
9
+ class Node
10
+
11
+ def initialize(account)
12
+ @account = account
13
+ end
14
+
15
+ def build_node_path(node)
16
+ path = []
17
+ loop do
18
+ path.push node["name"]
19
+
20
+ break if node.has_key?('isRoot') && node['isRoot'] == true
21
+
22
+ results = find_by_id(node["parents"][0])
23
+ if results[:success] == false
24
+ raise "No parent node found with ID #{node["parents"][0]}"
25
+ end
26
+
27
+ node = results[:data]
28
+
29
+ break if node.has_key?('isRoot') && node['isRoot'] === true
30
+ end
31
+
32
+ path = path.reverse
33
+ path.join('/')
34
+ end
35
+
36
+ def create_new_folder(name, parent_id = nil)
37
+ if parent_id == nil
38
+ parent_id = get_root['id']
39
+ end
40
+
41
+ body = {
42
+ :name => name,
43
+ :parents => [
44
+ parent_id
45
+ ],
46
+ :kind => "FOLDER"
47
+ }
48
+
49
+ retval = {
50
+ :success => false,
51
+ :data => []
52
+ }
53
+
54
+ RestClient.post(
55
+ "#{@account.metadata_url}nodes",
56
+ body.to_json,
57
+ :Authorization => "Bearer #{@account.access_token}"
58
+ ) do |response, request, result|
59
+ data = JSON.parse(response.body)
60
+ retval[:data] = data
61
+ if response.code === 201
62
+ retval[:success] = true
63
+ @account.save_node(data)
64
+ end
65
+ end
66
+
67
+ retval
68
+ end
69
+
70
+ def create_directory_path(path)
71
+ retval = {
72
+ :success => true,
73
+ :data => {}
74
+ }
75
+
76
+ path = get_path_array(path)
77
+ previous_node = get_root
78
+
79
+ match = nil
80
+ path.each_with_index do |folder, index|
81
+ xary = path.slice(0, index + 1)
82
+ if (match = find_by_path(xary.join('/'))) === nil
83
+ result = create_new_folder(folder, previous_node["id"])
84
+ if result[:success] === false
85
+ return result
86
+ end
87
+
88
+ match = result[:data]
89
+ end
90
+
91
+ previous_node = match
92
+ end
93
+
94
+ if match == nil
95
+ retval[:data] = previous_node
96
+ else
97
+ retval[:data] = match
98
+ end
99
+
100
+ retval
101
+ end
102
+
103
+ # If given a local file, the MD5 will be compared as well
104
+ def exists?(remote_file, local_file = nil)
105
+ if (file = find_by_path(remote_file)) == nil
106
+ if local_file != nil
107
+ if (file = find_by_md5(Digest::MD5.file(local_file).to_s)) != nil
108
+ path = build_node_path(file)
109
+ return {
110
+ :success => true,
111
+ :data => {
112
+ "message" => "File with same MD5 exists at #{path}: #{file.to_json}"
113
+ }
114
+ }
115
+ end
116
+ end
117
+ return {
118
+ :success => false,
119
+ :data => {
120
+ "message" => "File #{remote_file} does not exist"
121
+ }
122
+ }
123
+ end
124
+
125
+ retval = {
126
+ :success => true,
127
+ :data => {
128
+ "message" => "File #{remote_file} exists"
129
+ }
130
+ }
131
+
132
+ if local_file != nil
133
+ if file["contentProperties"] != nil && file["contentProperties"]["md5"] != nil
134
+ if Digest::MD5.file(local_file).to_s != file["contentProperties"]["md5"]
135
+ retval[:data]["message"] = "File #{remote_file} exists butuum doesn't match"
136
+ else
137
+ retval[:data]["message"] = "File #{remote_file} exists and is identical"
138
+ end
139
+ else
140
+ retval[:data]["message"] = "File #{remote_file} exists, but no checksum is available"
141
+ end
142
+ end
143
+
144
+ retval
145
+ end
146
+
147
+ def find_by_id(id)
148
+ retval = {
149
+ :success => false,
150
+ :data => {}
151
+ }
152
+
153
+ results = @account.db.execute("SELECT raw_data FROM nodes WHERE id = ?;", id)
154
+ if results.empty?
155
+ return retval
156
+ end
157
+
158
+ if results.count > 1
159
+ raise "Multiple nodes with same ID found: #{results[:data].to_json}"
160
+ end
161
+
162
+ {
163
+ :success => true,
164
+ :data => JSON.parse(results[0][0])
165
+ }
166
+ end
167
+
168
+ def find_by_md5(hash)
169
+
170
+ results = @account.db.execute("SELECT raw_data FROM nodes WHERE md5 = ?;", hash)
171
+ if results.empty?
172
+ return nil
173
+ end
174
+
175
+ if results.count > 1
176
+ raise "Multiple nodes with same MD5: #{results.to_json}"
177
+ end
178
+
179
+ JSON.parse(results[0][0])
180
+ end
181
+
182
+ def find_by_name(name)
183
+ retval = []
184
+ results = @account.db.execute("SELECT raw_data FROM nodes WHERE name = ?;", name)
185
+ if results.empty?
186
+ return retval
187
+ end
188
+
189
+ results.each do |result|
190
+ retval.push(JSON.parse(result[0]))
191
+ end
192
+
193
+ retval
194
+ end
195
+
196
+ def find_by_path(path)
197
+ path = path.gsub(/\A\//, '')
198
+ path = path.gsub(/\/$/, '')
199
+ path_info = Pathname.new(path)
200
+
201
+ found_nodes = find_by_name(path_info.basename.to_s)
202
+ if found_nodes.empty?
203
+ return nil
204
+ end
205
+
206
+ match = nil
207
+ found_nodes.each do |node|
208
+ if build_node_path(node) == path
209
+ match = node
210
+ end
211
+ end
212
+
213
+ match
214
+ end
215
+
216
+ def get_path_array(path)
217
+ return path if path.kind_of?(Array)
218
+
219
+ path = path.split('/')
220
+ path.reject! do |value|
221
+ value.empty?
222
+ end
223
+
224
+ path
225
+ end
226
+
227
+ def get_path_string(path)
228
+ path = path.join '/' if path.kind_of?(Array)
229
+
230
+ path.chomp
231
+ end
232
+
233
+ def get_root
234
+ results = find_by_name('root')
235
+ if results.empty?
236
+ raise "No node by the name of 'root' found in database"
237
+ end
238
+
239
+ results.each do |node|
240
+ if node.has_key?('isRoot') && node['isRoot'] === true
241
+ return node
242
+ end
243
+ end
244
+
245
+ nil
246
+ end
247
+
248
+ def upload_dir(src_path, dest_root, show_progress = false)
249
+ src_path = File.expand_path(src_path)
250
+
251
+ dest_root = get_path_array(dest_root)
252
+ dest_root.push(get_path_array(src_path).last)
253
+ dest_root = get_path_string(dest_root)
254
+
255
+ retval = []
256
+ Find.find(src_path) do |file|
257
+ # Skip root directory, no need to make it
258
+ next if file == src_path || File.directory?(file)
259
+
260
+ path_info = Pathname.new(file)
261
+ remote_dest = path_info.dirname.sub(src_path, dest_root).to_s
262
+
263
+ result = upload_file(file, remote_dest)
264
+ if show_progress == true
265
+ if result[:success] == true
266
+ puts "Successfully uploaded file #{file}: #{result[:data].to_json}"
267
+ else
268
+ puts "Failed to uploaded file #{file}: #{result[:data].to_json}"
269
+ end
270
+ end
271
+
272
+ retval.push(result)
273
+
274
+ # Since uploading a directory can take a while (depending on number/size of files)
275
+ # we will check if we need to renew our authorization after each file upload to
276
+ # make sure our authentication doesn't expire.
277
+ if (Time.new.to_i - @account.token_store["last_authorized"]) > 60
278
+ result = @account.renew_authorization
279
+ if result[:success] === false
280
+ raise "Failed to renew authorization: #{result[:data].to_json}"
281
+ end
282
+ end
283
+ end
284
+
285
+ retval
286
+ end
287
+
288
+ def upload_file(src_path, dest_path)
289
+ retval = {
290
+ :success => false,
291
+ :data => []
292
+ }
293
+
294
+ path_info = Pathname.new(src_path)
295
+ dest_path = get_path_string(get_path_array(dest_path))
296
+ dest_folder = create_directory_path(dest_path)
297
+
298
+ result = exists?("#{dest_path}/#{path_info.basename}", src_path)
299
+ if result[:success] == true
300
+ retval[:data] = result[:data]
301
+
302
+ return retval
303
+ end
304
+
305
+ body = {
306
+ :metadata => {
307
+ :kind => 'FILE',
308
+ :name => path_info.basename,
309
+ :parents => [
310
+ dest_folder["id"]
311
+ ]
312
+ }.to_json,
313
+ :content => File.new(File.expand_path(src_path), 'rb')
314
+ }
315
+
316
+ RestClient.post("#{@account.content_url}nodes", body, :Authorization => "Bearer #{@account.access_token}") do |response, request, result|
317
+ retval[:data] = JSON.parse(response.body)
318
+ if response.code === 201
319
+ retval[:success] = true
320
+ @account.save_node(retval[:data])
321
+ end
322
+ end
323
+
324
+ retval
325
+ end
326
+
327
+ end
328
+
329
+ end
@@ -0,0 +1,3 @@
1
+ module CloudDrive
2
+ VERSION = "0.0.1"
3
+ end
metadata ADDED
@@ -0,0 +1,133 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: clouddrive
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Alex Phillips
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-06-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.9'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.9'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rest-client
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.8'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.8'
55
+ - !ruby/object:Gem::Dependency
56
+ name: thor
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.19'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.19'
69
+ - !ruby/object:Gem::Dependency
70
+ name: sqlite3
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.3'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.3'
83
+ description: Ruby SDK and command line application for Amazon's CloudDrive
84
+ email:
85
+ - exonintrendo@gmail.com
86
+ executables:
87
+ - clouddrive
88
+ - console
89
+ - setup
90
+ extensions: []
91
+ extra_rdoc_files: []
92
+ files:
93
+ - ".gitignore"
94
+ - ".rspec"
95
+ - ".travis.yml"
96
+ - CODE_OF_CONDUCT.md
97
+ - Gemfile
98
+ - LICENSE.txt
99
+ - README.md
100
+ - Rakefile
101
+ - bin/clouddrive
102
+ - bin/console
103
+ - bin/setup
104
+ - clouddrive.gemspec
105
+ - lib/clouddrive.rb
106
+ - lib/clouddrive/account.rb
107
+ - lib/clouddrive/node.rb
108
+ - lib/clouddrive/version.rb
109
+ homepage: https://github.com/exonintrendo/clouddrive
110
+ licenses:
111
+ - MIT
112
+ metadata: {}
113
+ post_install_message:
114
+ rdoc_options: []
115
+ require_paths:
116
+ - lib
117
+ required_ruby_version: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - ">="
120
+ - !ruby/object:Gem::Version
121
+ version: '0'
122
+ required_rubygems_version: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ version: '0'
127
+ requirements: []
128
+ rubyforge_project:
129
+ rubygems_version: 2.4.7
130
+ signing_key:
131
+ specification_version: 4
132
+ summary: CloudDrive for Ruby
133
+ test_files: []