rb1drv 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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