rb1drv 0.1.2 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: feafbb64ce01c80b2c8f55c4d62dbed44aa6bca0d908965d94700a01def35f8a
4
- data.tar.gz: 1b31de1595f55f7ecc4f40da51979a87fc72cb0ee6602768af2a6f7a08319661
3
+ metadata.gz: 554c9b9b3a7969cffb6d024ca89d77fd2725a449bdc1fec2d06eeec87ecba026
4
+ data.tar.gz: 6a4e906dbb95f1036cea931b0748b404e3ad9f5a42cfd40fa9a29773ab25fd19
5
5
  SHA512:
6
- metadata.gz: 9a09646c4cbb6c1afc6e65f7bf2332865a5eae3afe32028b053908c21bacbeec8338e6b31f47176632c6939502971e9962627602528fd6f9eb68765ab9ebeaa2
7
- data.tar.gz: 37cf356d74830003591bf77c5d3328d354cdfdd46f112b31fddf7baaf029f9ca23318cf782a1a228fd2c4e9842971fcef7f38d39bea5151fb4afb38f64321b31
6
+ metadata.gz: 331d44af9844787776f36f3ba638a7d8e65e1761875189bcb482a78ea7276559456711b945c4d29085a7aac34c7cf6fdb05f5d16d2c39265c75e568781f77cb1
7
+ data.tar.gz: 9a2bc0126d20e02ed2d35ccd0ec3ca620a92404e634d424c2e1289314ae021e88697a9296c68860324c1dde02d013637ddf089f6c118325583d1c0b520bb2336
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rb1drv (0.1.1)
4
+ rb1drv (0.1.4)
5
5
  excon (~> 0.62)
6
6
  oauth2 (~> 1.4)
7
7
 
data/lib/rb1drv/auth.rb CHANGED
@@ -25,5 +25,9 @@ module Rb1drv
25
25
  @access_token = @access_token.refresh! if @access_token.expired?
26
26
  @access_token
27
27
  end
28
+
29
+ def auth_check
30
+ @access_token = @access_token.refresh! if @access_token.expired?
31
+ end
28
32
  end
29
33
  end
@@ -9,6 +9,8 @@ module Rb1drv
9
9
 
10
10
  # Get an object by an arbitary path.
11
11
  #
12
+ # TODO: API endpoint does not play well with remote files
13
+ #
12
14
  # @return [OneDriveDir,OneDriveFile] the drive item you asked
13
15
  def get(path)
14
16
  path = "/#{path}" unless path[0] == '/'
@@ -0,0 +1,16 @@
1
+ module Rb1drv
2
+ class OneDrive404 < OneDriveItem
3
+ def initialize(*_)
4
+ end
5
+
6
+ # No
7
+ def dir?
8
+ false
9
+ end
10
+
11
+ # No
12
+ def file?
13
+ false
14
+ end
15
+ end
16
+ end
@@ -1,3 +1,4 @@
1
+ require 'time'
1
2
  require 'rb1drv/sliced_io'
2
3
 
3
4
  module Rb1drv
@@ -6,6 +7,7 @@ module Rb1drv
6
7
  def initialize(od, api_hash)
7
8
  super
8
9
  @child_count = api_hash.dig('folder', 'childCount')
10
+ @cached_gets = {}
9
11
  end
10
12
 
11
13
  # Lists contents of current directory.
@@ -13,21 +15,31 @@ module Rb1drv
13
15
  # @return [Array<OneDriveDir,OneDriveFile>] directories and files whose parent is current directory
14
16
  def children
15
17
  return [] if child_count <= 0
16
- @od.request("drive/items/#{@id}/children")['value'].map do |child|
18
+ @cached_children ||= @od.request("#{api_path}/children")['value'].map do |child|
17
19
  OneDriveItem.smart_new(@od, child)
18
20
  end
19
21
  end
20
22
 
23
+ # Get a child object by name inside current directory.
24
+ #
25
+ # @param path [String] name of a child
26
+ #
27
+ # @return [OneDriveDir,OneDriveFile,OneDrive404] the drive item you asked
28
+ def get_child(path)
29
+ children.find { |child| child.name == path } || OneDrive404.new
30
+ end
31
+
21
32
  # Get an object by an arbitary path related to current directory.
22
33
  #
23
34
  # To get an absolute path, make use of OneDrive#get and not this.
24
35
  #
25
36
  # @param path [String] path relative to current directory
26
37
  #
27
- # @return [OneDriveDir,OneDriveFile] the drive item you asked
38
+ # @return [OneDriveDir,OneDriveFile,OneDrive404] the drive item you asked
28
39
  def get(path)
29
40
  path = "/#{path}" unless path[0] == '/'
30
- OneDriveItem.smart_new(self, @od.request("drive/items/#{@id}:#{path}"))
41
+ @cached_gets[path] ||=
42
+ OneDriveItem.smart_new(@od, @od.request("#{api_path}:#{path}"))
31
43
  end
32
44
 
33
45
  # Yes
@@ -54,83 +66,149 @@ module Rb1drv
54
66
  # @param name [String] directories you'd like to create
55
67
  # @return [OneDriveDir] the directory you created
56
68
  def mkdir(name)
69
+ return self if name == '.'
70
+ name = name[1..-1] if name[0] == '/'
57
71
  newdir, *remainder = name.split('/')
58
- subdir = @od.request("drive/items/#{@id}:/#{newdir}") rescue nil
59
- unless subdir
60
- subdir = @od.request("drive/items/#{@id}/children",
72
+ subdir = get(newdir)
73
+ unless subdir.dir?
74
+ result = @od.request("#{api_path}/children",
61
75
  name: newdir,
62
76
  folder: {},
63
77
  '@microsoft.graph.conflictBehavior': 'rename'
64
78
  )
79
+ subdir = OneDriveDir.new(@od, result)
65
80
  end
66
- subdir = OneDriveDir.new(@od, subdir)
67
81
  remainder.any? ? subdir.mkdir(remainder.join('/')) : subdir
68
82
  end
69
83
 
70
- # Uploads a local file into current remote directory using large file uploading mode.
84
+ # Uploads a local file into current remote directory.
85
+ # For files no larger than 4000KiB, uses simple upload mode.
86
+ # For larger files, uses large file upload mode.
71
87
  #
72
88
  # Unfinished download is stored as +target_name.incomplete+ and renamed upon completion.
73
89
  #
74
90
  # @param filename [String] local filename you'd like to upload
75
- # @param overwrite [Boolean] whether to overwrite remote file, or rename this
91
+ # @param overwrite [Boolean] whether to overwrite remote file, or not
92
+ # If false:
93
+ # For larger files, it renames the uploaded file
94
+ # For small files, it skips the file
95
+ # Always check existence beforehand if you need consistant behavior
76
96
  # @param fragment_size [Integer] fragment size for each upload session, recommended to be multiple of 320KiB
77
97
  # @param chunk_size [Integer] IO size for each disk read request and progress notification
78
98
  # @param target_name [String] desired remote filename, a relative path to current directory
99
+ # @return [OneDriveFile,nil] uploaded file
79
100
  #
80
101
  # @yield [event, status] for receive progress notification
81
102
  # @yieldparam event [Symbol] event of this notification
82
103
  # @yieldparam status [{Symbol => String,Integer}] details
83
104
  def upload(filename, overwrite: false, fragment_size: 41_943_040, chunk_size: 1_048_576, target_name: nil, &block)
84
105
  raise ArgumentError.new('File not found') unless File.exist?(filename)
106
+ conn = nil
85
107
  file_size = File.size(filename)
108
+ target_name ||= File.basename(filename)
109
+ return upload_simple(filename, overwrite: overwrite, target_name: target_name) if file_size <= 4_096_000
110
+
86
111
  resume_file = "#{filename}.1drv_upload"
87
112
  resume_session = JSON.parse(File.read(resume_file)) rescue nil if File.exist?(resume_file)
88
113
 
89
- if resume_session && resume_session['session_url']
90
- conn = Excon.new(resume_session['session_url'])
91
- result = JSON.parse(conn.get.body)
92
- resume_position = result.dig('nextExpectedRanges', 0)&.split('-')&.first&.to_i or resume_session = nil
93
- end
114
+ result = nil
115
+ loop do
116
+ catch :restart do
117
+ if resume_session && resume_session['session_url']
118
+ conn = Excon.new(resume_session['session_url'], idempotent: true)
119
+ loop do
120
+ result = JSON.parse(conn.get.body)
121
+ break unless result.dig('error', 'code') == 'accessDenied'
122
+ sleep 5
123
+ end
124
+ resume_position = result.dig('nextExpectedRanges', 0)&.split('-')&.first&.to_i or resume_session = nil
125
+ end
94
126
 
95
- resume_position ||= 0
127
+ resume_position ||= 0
96
128
 
97
- if resume_session
98
- file_size == resume_session['source_size'] or resume_session = nil
99
- end
129
+ if resume_session
130
+ file_size == resume_session['source_size'] or resume_session = nil
131
+ end
100
132
 
101
- unless resume_session
102
- target_name ||= File.basename(filename)
103
- result = @od.request("drive/items/#{@id}:/#{target_name}:/createUploadSession", item: {'@microsoft.graph.conflictBehavior': overwrite ? 'replace' : 'rename'})
104
- resume_session = {
105
- 'session_url' => result['uploadUrl'],
106
- 'source_size' => File.size(filename),
107
- 'fragment_size' => fragment_size
108
- }
109
- File.write(resume_file, JSON.pretty_generate(resume_session))
110
- end
133
+ unless resume_session
134
+ result = @od.request("#{api_path}:/#{target_name}:/createUploadSession", item: {'@microsoft.graph.conflictBehavior': overwrite ? 'replace' : 'rename'})
135
+ resume_session = {
136
+ 'session_url' => result['uploadUrl'],
137
+ 'source_size' => File.size(filename),
138
+ 'fragment_size' => fragment_size
139
+ }
140
+ File.write(resume_file, JSON.pretty_generate(resume_session))
141
+ conn = Excon.new(resume_session['session_url'], idempotent: true)
142
+ end
111
143
 
112
- new_file = nil
113
- File.open(filename, mode: 'rb', external_encoding: Encoding::BINARY) do |f|
114
- resume_position.step(file_size, resume_session['fragment_size']) do |from|
115
- to = [from + resume_session['fragment_size'], file_size].min - 1
116
- len = to - from + 1
117
- headers = {
118
- 'Content-Length': len.to_s,
119
- 'Content-Range': "bytes #{from}-#{to}/#{file_size}"
120
- }
121
- @od.logger.info "Uploading #{from}-#{to}/#{file_size}"
122
- yield :new_segment, file: filename, from: from, to: to if block_given?
123
- sliced_io = SlicedIO.new(f, from, to) do |progress, total|
124
- yield :progress, file: filename, from: from, to: to, progress: progress, total: total if block_given?
144
+ new_file = nil
145
+ File.open(filename, mode: 'rb', external_encoding: Encoding::BINARY) do |f|
146
+ resume_position.step(file_size - 1, resume_session['fragment_size']) do |from|
147
+ to = [from + resume_session['fragment_size'], file_size].min - 1
148
+ len = to - from + 1
149
+ headers = {
150
+ 'Content-Length': len.to_s,
151
+ 'Content-Range': "bytes #{from}-#{to}/#{file_size}"
152
+ }
153
+ @od.logger.info "Uploading #{from}-#{to}/#{file_size}" if @od.logger
154
+ yield :new_segment, file: filename, from: from, to: to if block_given?
155
+ sliced_io = SlicedIO.new(f, from, to) do |progress, total|
156
+ yield :progress, file: filename, from: from, to: to, progress: progress, total: total if block_given?
157
+ end
158
+ begin
159
+ result = conn.put headers: headers, chunk_size: chunk_size, body: sliced_io, read_timeout: 15, write_timeout: 15, retry_limit: 2
160
+ raise IOError if result.body.include? 'accessDenied'
161
+ rescue Excon::Error::Timeout, IOError
162
+ conn = Excon.new(resume_session['session_url'], idempotent: true)
163
+ yield :retry, file: filename, from: from, to: to if block_given?
164
+ sleep 60
165
+ retry
166
+ end
167
+ yield :finish_segment, file: filename, from: from, to: to if block_given?
168
+ throw :restart if result.body.include?('</html>')
169
+ result = JSON.parse(result.body)
170
+ new_file = OneDriveFile.new(@od, result) if result.dig('file')
171
+ end
125
172
  end
126
- result = conn.put headers: headers, chunk_size: chunk_size, body: sliced_io
127
- yield :finish_segment, file: filename, from: from, to: to if block_given?
128
- result = JSON.parse(result.body)
129
- new_file = OneDriveFile.new(@od, result) if result.dig('file')
173
+ throw :restart unless new_file&.file?
174
+ File.unlink(resume_file)
175
+ return set_mtime(new_file, File.mtime(filename))
130
176
  end
131
- File.unlink(resume_file)
177
+ # catch :restart here
178
+ sleep 60 # and retry the whole process
132
179
  end
133
- new_file
180
+ end
181
+
182
+ # Uploads a local file into current remote directory using simple upload mode.
183
+ #
184
+ # @return [OneDriveFile,nil] uploaded file
185
+ def upload_simple(filename, overwrite:, target_name:)
186
+ target_file = get(filename)
187
+ exist = target_file.file?
188
+ return if exist && !overwrite
189
+ path = nil
190
+ if exist
191
+ path = "#{target_file.api_path}/content"
192
+ else
193
+ path = "#{api_path}:/#{target_name}:/content"
194
+ end
195
+
196
+ query = {
197
+ path: File.join('v1.0/me/', path),
198
+ headers: {
199
+ 'Authorization': "Bearer #{@od.access_token.token}",
200
+ 'Content-Type': 'application/octet-stream'
201
+ },
202
+ body: File.read(filename)
203
+ }
204
+ result = @od.conn.put(query)
205
+ result = JSON.parse(result.body)
206
+ file = OneDriveFile.new(@od, result)
207
+ set_mtime(file, File.mtime(filename))
208
+ end
209
+
210
+ def set_mtime(file, time)
211
+ OneDriveFile.new(@od, @od.request(file.api_path, {fileSystemInfo: {lastModifiedDateTime: time.utc.iso8601}}, :patch))
134
212
  end
135
213
  end
136
214
  end
@@ -1,24 +1,41 @@
1
1
  module Rb1drv
2
2
  class OneDriveItem
3
- attr_reader :id, :name, :eTag, :size, :mtime, :ctime, :muser, :cuser, :parent_path
3
+ attr_reader :id, :name, :eTag, :size, :mtime, :ctime, :muser, :cuser, :parent_path, :remote_id, :remote_drive_id
4
4
  protected
5
5
  def initialize(od, api_hash)
6
+ raise api_hash.inspect unless api_hash['createdDateTime']
6
7
  @od = od
7
8
  %w(id name eTag size).each do |key|
8
9
  instance_variable_set("@#{key}", api_hash[key])
9
10
  end
10
- @mtime = Time.iso8601(api_hash.dig('lastModifiedDateTime'))
11
- @ctime = Time.iso8601(api_hash.dig('createdDateTime'))
11
+ @remote_drive_id = api_hash.dig('remoteItem', 'parentReference', 'driveId')
12
+ @remote_id = api_hash.dig('remoteItem', 'id')
13
+ @mtime = Time.iso8601(api_hash['lastModifiedDateTime'])
14
+ @ctime = Time.iso8601(api_hash['createdDateTime'])
12
15
  @muser = api_hash.dig('lastModifiedBy', 'user', 'displayName') || 'N/A'
13
16
  @cuser = api_hash.dig('createdBy', 'user', 'displayName') || 'N/A'
14
17
  @parent_path = api_hash.dig('parentReference', 'path')
18
+ @remote = api_hash.has_key?('remoteItem')
15
19
  end
16
20
 
17
21
  # Create subclass instance by checking the item type
18
22
  #
19
23
  # @return [OneDriveFile, OneDriveDir] instanciated drive item
20
24
  def self.smart_new(od, item_hash)
21
- item_hash['file'] ? OneDriveFile.new(od, item_hash) : OneDriveDir.new(od, item_hash)
25
+ if item_hash['remoteItem']
26
+ item_hash['remoteItem'].each do |key, value|
27
+ item_hash[key] ||= value
28
+ end
29
+ end
30
+ if item_hash['file']
31
+ OneDriveFile.new(od, item_hash)
32
+ elsif item_hash['folder']
33
+ OneDriveDir.new(od, item_hash)
34
+ elsif item_hash.dig('error', 'code') == 'itemNotFound'
35
+ OneDrive404.new
36
+ else
37
+ item_hash
38
+ end
22
39
  end
23
40
 
24
41
  # @return [String] absolute path of current item
@@ -29,5 +46,23 @@ module Rb1drv
29
46
  @name
30
47
  end
31
48
  end
49
+
50
+ # TODO: API endpoint does not play well with remote files
51
+ #
52
+ # @return [String] api reference path of current object
53
+ def api_path
54
+ if remote?
55
+ "drives/#{@remote_drive_id}/items/#{@remote_id}"
56
+ else
57
+ "drive/items/#{@id}"
58
+ end
59
+ end
60
+
61
+ # TODO: API endpoint does not play well with remote files
62
+ #
63
+ # @return [Boolean] whether it's shared by others
64
+ def remote?
65
+ @remote
66
+ end
32
67
  end
33
68
  end
@@ -5,11 +5,11 @@ class SlicedIO
5
5
  @from = from
6
6
  @to = to
7
7
  @block = block
8
- @current = 0
8
+ rewind
9
9
  end
10
10
 
11
11
  def rewind
12
- io.seek(from)
12
+ @io.seek(@from)
13
13
  @current = 0
14
14
  end
15
15
 
@@ -19,10 +19,19 @@ class SlicedIO
19
19
 
20
20
  def read(len)
21
21
  return nil if @current >= size
22
- len = [len, @to - @current + 1].min
22
+ len = [len, size - @current].min
23
23
  # Notify before we read
24
24
  @block.call(@current, size)
25
- @io.read(len)
25
+ failed_count = 0
26
+ begin
27
+ @io.read(len)
28
+ rescue Errno::EIO
29
+ @io.seek(@from + @current)
30
+ sleep 1
31
+ failed_count += 1
32
+ retry unless failed_count > 5
33
+ raise
34
+ end
26
35
  ensure
27
36
  @current += len
28
37
  end
@@ -1,3 +1,3 @@
1
1
  module Rb1drv
2
- VERSION = "0.1.2"
2
+ VERSION = "0.1.4"
3
3
  end
data/lib/rb1drv.rb CHANGED
@@ -7,9 +7,9 @@ module Rb1drv
7
7
  #
8
8
  # Call +#root+ or +#get+ to get an +OneDriveDir+ or +OneDriveFile+ to wotk with.
9
9
  class OneDrive
10
- attr_reader :oauth2_client, :logger, :access_token
10
+ attr_reader :oauth2_client, :logger, :access_token, :conn
11
11
  # Instanciates with app id and secret.
12
- def initialize(client_id, client_secret, callback_url, logger=Logger.new(STDERR))
12
+ def initialize(client_id, client_secret, callback_url, logger=nil)
13
13
  @client_id = client_id
14
14
  @client_secret = client_secret
15
15
  @callback_url = callback_url
@@ -17,8 +17,8 @@ module Rb1drv
17
17
  @oauth2_client = OAuth2::Client.new client_id, client_secret,
18
18
  authorize_url: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
19
19
  token_url: 'https://login.microsoftonline.com/common/oauth2/v2.0/token'
20
- @conn = Excon.new('https://graph.microsoft.com/', persistent: true)
21
- @conn.logger = @logger
20
+ @conn = Excon.new('https://graph.microsoft.com/', persistent: true, idempotent: true)
21
+ @conn.logger = @logger if @logger
22
22
  end
23
23
 
24
24
  # Issues requests to API endpoint.
@@ -29,9 +29,10 @@ module Rb1drv
29
29
  #
30
30
  # @return [Hash] response from API.
31
31
  def request(uri, data=nil, verb=:post)
32
- @logger.info(uri)
32
+ @logger.info(uri) if @logger
33
+ auth_check
33
34
  query = {
34
- path: File.join('v1.0/me/', uri),
35
+ path: File.join('v1.0/me/', URI.escape(uri)),
35
36
  headers: {
36
37
  'Authorization': "Bearer #{@access_token.token}"
37
38
  }
@@ -39,8 +40,8 @@ module Rb1drv
39
40
  if data
40
41
  query[:body] = JSON.generate(data)
41
42
  query[:headers]['Content-Type'] = 'application/json'
42
- @logger.info(query[:body])
43
- verb = :post unless [:post, :put, :delete].include?(verb)
43
+ @logger.info(query[:body]) if @logger
44
+ verb = :post unless [:post, :put, :patch, :delete].include?(verb)
44
45
  response = @conn.send(verb, query)
45
46
  else
46
47
  response = @conn.get(query)
@@ -55,3 +56,4 @@ require 'rb1drv/onedrive'
55
56
  require 'rb1drv/onedrive_item'
56
57
  require 'rb1drv/onedrive_dir'
57
58
  require 'rb1drv/onedrive_file'
59
+ require 'rb1drv/onedrive_404'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rb1drv
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Xinyue Lu
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-05-18 00:00:00.000000000 Z
11
+ date: 2018-05-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: oauth2
@@ -82,6 +82,7 @@ files:
82
82
  - lib/rb1drv.rb
83
83
  - lib/rb1drv/auth.rb
84
84
  - lib/rb1drv/onedrive.rb
85
+ - lib/rb1drv/onedrive_404.rb
85
86
  - lib/rb1drv/onedrive_dir.rb
86
87
  - lib/rb1drv/onedrive_file.rb
87
88
  - lib/rb1drv/onedrive_item.rb