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 +4 -4
- data/Gemfile.lock +1 -1
- data/lib/rb1drv/auth.rb +4 -0
- data/lib/rb1drv/onedrive.rb +2 -0
- data/lib/rb1drv/onedrive_404.rb +16 -0
- data/lib/rb1drv/onedrive_dir.rb +125 -47
- data/lib/rb1drv/onedrive_item.rb +39 -4
- data/lib/rb1drv/sliced_io.rb +13 -4
- data/lib/rb1drv/version.rb +1 -1
- data/lib/rb1drv.rb +10 -8
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 554c9b9b3a7969cffb6d024ca89d77fd2725a449bdc1fec2d06eeec87ecba026
|
4
|
+
data.tar.gz: 6a4e906dbb95f1036cea931b0748b404e3ad9f5a42cfd40fa9a29773ab25fd19
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 331d44af9844787776f36f3ba638a7d8e65e1761875189bcb482a78ea7276559456711b945c4d29085a7aac34c7cf6fdb05f5d16d2c39265c75e568781f77cb1
|
7
|
+
data.tar.gz: 9a2bc0126d20e02ed2d35ccd0ec3ca620a92404e634d424c2e1289314ae021e88697a9296c68860324c1dde02d013637ddf089f6c118325583d1c0b520bb2336
|
data/Gemfile.lock
CHANGED
data/lib/rb1drv/auth.rb
CHANGED
data/lib/rb1drv/onedrive.rb
CHANGED
data/lib/rb1drv/onedrive_dir.rb
CHANGED
@@ -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("
|
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
|
-
|
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 =
|
59
|
-
unless subdir
|
60
|
-
|
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
|
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
|
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
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
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
|
-
|
127
|
+
resume_position ||= 0
|
96
128
|
|
97
|
-
|
98
|
-
|
99
|
-
|
129
|
+
if resume_session
|
130
|
+
file_size == resume_session['source_size'] or resume_session = nil
|
131
|
+
end
|
100
132
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
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
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
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
|
-
|
127
|
-
|
128
|
-
|
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
|
-
|
177
|
+
# catch :restart here
|
178
|
+
sleep 60 # and retry the whole process
|
132
179
|
end
|
133
|
-
|
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
|
data/lib/rb1drv/onedrive_item.rb
CHANGED
@@ -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
|
-
@
|
11
|
-
@
|
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['
|
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
|
data/lib/rb1drv/sliced_io.rb
CHANGED
@@ -5,11 +5,11 @@ class SlicedIO
|
|
5
5
|
@from = from
|
6
6
|
@to = to
|
7
7
|
@block = block
|
8
|
-
|
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,
|
22
|
+
len = [len, size - @current].min
|
23
23
|
# Notify before we read
|
24
24
|
@block.call(@current, size)
|
25
|
-
|
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
|
data/lib/rb1drv/version.rb
CHANGED
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=
|
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.
|
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-
|
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
|