rb1drv 0.1.7 → 0.1.8
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/.gitignore +8 -8
- data/.travis.yml +6 -6
- data/Gemfile +3 -3
- data/Gemfile.lock +36 -36
- data/README.md +78 -78
- data/lib/rb1drv/auth.rb +33 -33
- data/lib/rb1drv/onedrive.rb +20 -20
- data/lib/rb1drv/onedrive_404.rb +20 -20
- data/lib/rb1drv/onedrive_dir.rb +228 -225
- data/lib/rb1drv/onedrive_file.rb +79 -79
- data/lib/rb1drv/onedrive_item.rb +67 -68
- data/lib/rb1drv/sliced_io.rb +38 -38
- data/lib/rb1drv/version.rb +3 -3
- data/lib/rb1drv.rb +59 -59
- data/rb1drv.gemspec +28 -28
- metadata +2 -2
data/lib/rb1drv/onedrive_dir.rb
CHANGED
@@ -1,225 +1,228 @@
|
|
1
|
-
require 'time'
|
2
|
-
require 'rb1drv/sliced_io'
|
3
|
-
|
4
|
-
module Rb1drv
|
5
|
-
class OneDriveDir < OneDriveItem
|
6
|
-
attr_reader :child_count
|
7
|
-
def initialize(od, api_hash)
|
8
|
-
super
|
9
|
-
@child_count = api_hash.dig('folder', 'childCount')
|
10
|
-
@cached_gets = {}
|
11
|
-
end
|
12
|
-
|
13
|
-
# Lists contents of current directory.
|
14
|
-
#
|
15
|
-
# @return [Array<OneDriveDir,OneDriveFile>] directories and files whose parent is current directory
|
16
|
-
def children
|
17
|
-
return [] if child_count <= 0
|
18
|
-
@cached_children ||= @od.request("#{api_path}/children?$top=1000")['value'].map do |child|
|
19
|
-
OneDriveItem.smart_new(@od, child)
|
20
|
-
end
|
21
|
-
end
|
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
|
-
|
32
|
-
# Get an object by an arbitary path related to current directory.
|
33
|
-
#
|
34
|
-
# To get an absolute path, make use of OneDrive#get and not this.
|
35
|
-
#
|
36
|
-
# @param path [String] path relative to current directory
|
37
|
-
#
|
38
|
-
# @return [OneDriveDir,OneDriveFile,OneDrive404] the drive item you asked
|
39
|
-
def get(path)
|
40
|
-
path = "/#{path}" unless path[0] == '/'
|
41
|
-
@cached_gets[path] ||=
|
42
|
-
OneDriveItem.smart_new(@od, @od.request("#{api_path}:#{path}"))
|
43
|
-
end
|
44
|
-
|
45
|
-
# Yes
|
46
|
-
def dir?
|
47
|
-
true
|
48
|
-
end
|
49
|
-
|
50
|
-
# No
|
51
|
-
def file?
|
52
|
-
false
|
53
|
-
end
|
54
|
-
|
55
|
-
# @return [String] absolute path of current item
|
56
|
-
def absolute_path
|
57
|
-
if @parent_path
|
58
|
-
File.join(@parent_path, @name)
|
59
|
-
else
|
60
|
-
'/'
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
# Recursively creates empty directories.
|
65
|
-
#
|
66
|
-
# @param name [String] directories you'd like to create
|
67
|
-
# @return [OneDriveDir] the directory you created
|
68
|
-
def mkdir(name)
|
69
|
-
return self if name == '.'
|
70
|
-
name = name[1..-1] if name[0] == '/'
|
71
|
-
newdir, *remainder = name.split('/')
|
72
|
-
subdir = get(newdir)
|
73
|
-
unless subdir.dir?
|
74
|
-
result = @od.request("#{api_path}/children",
|
75
|
-
name: newdir,
|
76
|
-
folder: {},
|
77
|
-
'@microsoft.graph.conflictBehavior': 'rename'
|
78
|
-
)
|
79
|
-
subdir = OneDriveDir.new(@od, result)
|
80
|
-
end
|
81
|
-
remainder.any? ? subdir.mkdir(remainder.join('/')) : subdir
|
82
|
-
end
|
83
|
-
|
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.
|
87
|
-
#
|
88
|
-
# Unfinished download is stored as +target_name.incomplete+ and renamed upon completion.
|
89
|
-
#
|
90
|
-
# @param filename [String] local filename you'd like to upload
|
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
|
96
|
-
# @param fragment_size [Integer] fragment size for each upload session, recommended to be multiple of 320KiB
|
97
|
-
# @param chunk_size [Integer] IO size for each disk read request and progress notification
|
98
|
-
# @param target_name [String] desired remote filename, a relative path to current directory
|
99
|
-
# @return [OneDriveFile,nil] uploaded file
|
100
|
-
#
|
101
|
-
# @yield [event, status] for receive progress notification
|
102
|
-
# @yieldparam event [Symbol] event of this notification
|
103
|
-
# @yieldparam status [{Symbol => String,Integer}] details
|
104
|
-
def upload(filename, overwrite: false, fragment_size: 41_943_040, chunk_size: 1_048_576, target_name: nil, &block)
|
105
|
-
raise ArgumentError.new('File not found') unless File.exist?(filename)
|
106
|
-
conn = nil
|
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
|
-
|
111
|
-
resume_file = "#{filename}.1drv_upload"
|
112
|
-
resume_session = JSON.parse(File.read(resume_file)) rescue nil if File.exist?(resume_file)
|
113
|
-
old_file = OneDriveItem.smart_new(@od, @od.request("#{api_path}:/#{target_name}"))
|
114
|
-
new_file = nil
|
115
|
-
|
116
|
-
result = nil
|
117
|
-
loop do
|
118
|
-
catch :restart do
|
119
|
-
if resume_session && resume_session['session_url']
|
120
|
-
conn = Excon.new(resume_session['session_url'], idempotent: true)
|
121
|
-
loop do
|
122
|
-
result = JSON.parse(conn.get.body)
|
123
|
-
break unless result.dig('error', 'code') == 'accessDenied'
|
124
|
-
sleep 5
|
125
|
-
end
|
126
|
-
resume_position = result.dig('nextExpectedRanges', 0)&.split('-')&.first&.to_i or resume_session = nil
|
127
|
-
end
|
128
|
-
|
129
|
-
resume_position ||= 0
|
130
|
-
|
131
|
-
if resume_session
|
132
|
-
file_size == resume_session['source_size'] or resume_session = nil
|
133
|
-
end
|
134
|
-
|
135
|
-
until resume_session && resume_session['session_url'] do
|
136
|
-
result = @od.request("#{api_path}:/#{target_name}:/createUploadSession", item: {'@microsoft.graph.conflictBehavior': overwrite ? 'replace' : 'rename'})
|
137
|
-
if result['uploadUrl']
|
138
|
-
resume_session = {
|
139
|
-
'session_url' => result['uploadUrl'],
|
140
|
-
'source_size' => File.size(filename),
|
141
|
-
'fragment_size' => fragment_size
|
142
|
-
}
|
143
|
-
File.write(resume_file, JSON.pretty_generate(resume_session))
|
144
|
-
conn = Excon.new(resume_session['session_url'], idempotent: true)
|
145
|
-
break
|
146
|
-
end
|
147
|
-
sleep 15
|
148
|
-
end
|
149
|
-
|
150
|
-
new_file = nil
|
151
|
-
File.open(filename, mode: 'rb', external_encoding: Encoding::BINARY) do |f|
|
152
|
-
resume_position.step(file_size - 1, resume_session['fragment_size']) do |from|
|
153
|
-
to = [from + resume_session['fragment_size'], file_size].min - 1
|
154
|
-
len = to - from + 1
|
155
|
-
headers = {
|
156
|
-
'Content-Length': len.to_s,
|
157
|
-
'Content-Range': "bytes #{from}-#{to}/#{file_size}"
|
158
|
-
}
|
159
|
-
@od.logger.info "Uploading #{from}-#{to}/#{file_size}" if @od.logger
|
160
|
-
yield :new_segment, file: filename, from: from, to: to if block_given?
|
161
|
-
sliced_io = SlicedIO.new(f, from, to) do |progress, total|
|
162
|
-
yield :progress, file: filename, from: from, to: to, progress: progress, total: total if block_given?
|
163
|
-
end
|
164
|
-
begin
|
165
|
-
result = conn.put headers: headers, chunk_size: chunk_size, body: sliced_io, retry_limit: 2
|
166
|
-
raise IOError if result.body.include? 'accessDenied'
|
167
|
-
rescue Excon::Error::Socket, IOError
|
168
|
-
# Probably server rejected this request
|
169
|
-
throw :restart
|
170
|
-
rescue Excon::Error::Timeout
|
171
|
-
conn = Excon.new(resume_session['session_url'], idempotent: true)
|
172
|
-
yield :retry, file: filename, from: from, to: to if block_given?
|
173
|
-
retry
|
174
|
-
ensure
|
175
|
-
yield :finish_segment, file: filename, from: from, to: to if block_given?
|
176
|
-
end
|
177
|
-
throw :restart if result.body.include?('</html>')
|
178
|
-
result = JSON.parse(result.body)
|
179
|
-
new_file = OneDriveFile.new(@od, result) if result.dig('file')
|
180
|
-
end
|
181
|
-
end
|
182
|
-
throw :restart unless new_file&.file?
|
183
|
-
break
|
184
|
-
end
|
185
|
-
# catch :restart here
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
1
|
+
require 'time'
|
2
|
+
require 'rb1drv/sliced_io'
|
3
|
+
|
4
|
+
module Rb1drv
|
5
|
+
class OneDriveDir < OneDriveItem
|
6
|
+
attr_reader :child_count
|
7
|
+
def initialize(od, api_hash)
|
8
|
+
super
|
9
|
+
@child_count = api_hash.dig('folder', 'childCount')
|
10
|
+
@cached_gets = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
# Lists contents of current directory.
|
14
|
+
#
|
15
|
+
# @return [Array<OneDriveDir,OneDriveFile>] directories and files whose parent is current directory
|
16
|
+
def children
|
17
|
+
return [] if child_count <= 0
|
18
|
+
@cached_children ||= @od.request("#{api_path}/children?$top=1000")['value'].map do |child|
|
19
|
+
OneDriveItem.smart_new(@od, child)
|
20
|
+
end
|
21
|
+
end
|
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
|
+
|
32
|
+
# Get an object by an arbitary path related to current directory.
|
33
|
+
#
|
34
|
+
# To get an absolute path, make use of OneDrive#get and not this.
|
35
|
+
#
|
36
|
+
# @param path [String] path relative to current directory
|
37
|
+
#
|
38
|
+
# @return [OneDriveDir,OneDriveFile,OneDrive404] the drive item you asked
|
39
|
+
def get(path)
|
40
|
+
path = "/#{path}" unless path[0] == '/'
|
41
|
+
@cached_gets[path] ||=
|
42
|
+
OneDriveItem.smart_new(@od, @od.request("#{api_path}:#{path}"))
|
43
|
+
end
|
44
|
+
|
45
|
+
# Yes
|
46
|
+
def dir?
|
47
|
+
true
|
48
|
+
end
|
49
|
+
|
50
|
+
# No
|
51
|
+
def file?
|
52
|
+
false
|
53
|
+
end
|
54
|
+
|
55
|
+
# @return [String] absolute path of current item
|
56
|
+
def absolute_path
|
57
|
+
if @parent_path
|
58
|
+
File.join(@parent_path, @name)
|
59
|
+
else
|
60
|
+
'/'
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Recursively creates empty directories.
|
65
|
+
#
|
66
|
+
# @param name [String] directories you'd like to create
|
67
|
+
# @return [OneDriveDir] the directory you created
|
68
|
+
def mkdir(name)
|
69
|
+
return self if name == '.'
|
70
|
+
name = name[1..-1] if name[0] == '/'
|
71
|
+
newdir, *remainder = name.split('/')
|
72
|
+
subdir = get(newdir)
|
73
|
+
unless subdir.dir?
|
74
|
+
result = @od.request("#{api_path}/children",
|
75
|
+
name: newdir,
|
76
|
+
folder: {},
|
77
|
+
'@microsoft.graph.conflictBehavior': 'rename'
|
78
|
+
)
|
79
|
+
subdir = OneDriveDir.new(@od, result)
|
80
|
+
end
|
81
|
+
remainder.any? ? subdir.mkdir(remainder.join('/')) : subdir
|
82
|
+
end
|
83
|
+
|
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.
|
87
|
+
#
|
88
|
+
# Unfinished download is stored as +target_name.incomplete+ and renamed upon completion.
|
89
|
+
#
|
90
|
+
# @param filename [String] local filename you'd like to upload
|
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
|
96
|
+
# @param fragment_size [Integer] fragment size for each upload session, recommended to be multiple of 320KiB
|
97
|
+
# @param chunk_size [Integer] IO size for each disk read request and progress notification
|
98
|
+
# @param target_name [String] desired remote filename, a relative path to current directory
|
99
|
+
# @return [OneDriveFile,nil] uploaded file
|
100
|
+
#
|
101
|
+
# @yield [event, status] for receive progress notification
|
102
|
+
# @yieldparam event [Symbol] event of this notification
|
103
|
+
# @yieldparam status [{Symbol => String,Integer}] details
|
104
|
+
def upload(filename, overwrite: false, fragment_size: 41_943_040, chunk_size: 1_048_576, target_name: nil, &block)
|
105
|
+
raise ArgumentError.new('File not found') unless File.exist?(filename)
|
106
|
+
conn = nil
|
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
|
+
|
111
|
+
resume_file = "#{filename}.1drv_upload"
|
112
|
+
resume_session = JSON.parse(File.read(resume_file)) rescue nil if File.exist?(resume_file)
|
113
|
+
old_file = OneDriveItem.smart_new(@od, @od.request("#{api_path}:/#{target_name}"))
|
114
|
+
new_file = nil
|
115
|
+
|
116
|
+
result = nil
|
117
|
+
loop do
|
118
|
+
catch :restart do
|
119
|
+
if resume_session && resume_session['session_url']
|
120
|
+
conn = Excon.new(resume_session['session_url'], idempotent: true)
|
121
|
+
loop do
|
122
|
+
result = JSON.parse(conn.get.body)
|
123
|
+
break unless result.dig('error', 'code') == 'accessDenied'
|
124
|
+
sleep 5
|
125
|
+
end
|
126
|
+
resume_position = result.dig('nextExpectedRanges', 0)&.split('-')&.first&.to_i or resume_session = nil
|
127
|
+
end
|
128
|
+
|
129
|
+
resume_position ||= 0
|
130
|
+
|
131
|
+
if resume_session
|
132
|
+
file_size == resume_session['source_size'] or resume_session = nil
|
133
|
+
end
|
134
|
+
|
135
|
+
until resume_session && resume_session['session_url'] do
|
136
|
+
result = @od.request("#{api_path}:/#{target_name}:/createUploadSession", item: {'@microsoft.graph.conflictBehavior': overwrite ? 'replace' : 'rename'})
|
137
|
+
if result['uploadUrl']
|
138
|
+
resume_session = {
|
139
|
+
'session_url' => result['uploadUrl'],
|
140
|
+
'source_size' => File.size(filename),
|
141
|
+
'fragment_size' => fragment_size
|
142
|
+
}
|
143
|
+
File.write(resume_file, JSON.pretty_generate(resume_session))
|
144
|
+
conn = Excon.new(resume_session['session_url'], idempotent: true)
|
145
|
+
break
|
146
|
+
end
|
147
|
+
sleep 15
|
148
|
+
end
|
149
|
+
|
150
|
+
new_file = nil
|
151
|
+
File.open(filename, mode: 'rb', external_encoding: Encoding::BINARY) do |f|
|
152
|
+
resume_position.step(file_size - 1, resume_session['fragment_size']) do |from|
|
153
|
+
to = [from + resume_session['fragment_size'], file_size].min - 1
|
154
|
+
len = to - from + 1
|
155
|
+
headers = {
|
156
|
+
'Content-Length': len.to_s,
|
157
|
+
'Content-Range': "bytes #{from}-#{to}/#{file_size}"
|
158
|
+
}
|
159
|
+
@od.logger.info "Uploading #{from}-#{to}/#{file_size}" if @od.logger
|
160
|
+
yield :new_segment, file: filename, from: from, to: to if block_given?
|
161
|
+
sliced_io = SlicedIO.new(f, from, to) do |progress, total|
|
162
|
+
yield :progress, file: filename, from: from, to: to, progress: progress, total: total if block_given?
|
163
|
+
end
|
164
|
+
begin
|
165
|
+
result = conn.put headers: headers, chunk_size: chunk_size, body: sliced_io, retry_limit: 2
|
166
|
+
raise IOError if result.body.include? 'accessDenied'
|
167
|
+
rescue Excon::Error::Socket, IOError
|
168
|
+
# Probably server rejected this request
|
169
|
+
throw :restart
|
170
|
+
rescue Excon::Error::Timeout
|
171
|
+
conn = Excon.new(resume_session['session_url'], idempotent: true)
|
172
|
+
yield :retry, file: filename, from: from, to: to if block_given?
|
173
|
+
retry
|
174
|
+
ensure
|
175
|
+
yield :finish_segment, file: filename, from: from, to: to if block_given?
|
176
|
+
end
|
177
|
+
throw :restart if result.body.include?('</html>')
|
178
|
+
result = JSON.parse(result.body)
|
179
|
+
new_file = OneDriveFile.new(@od, result) if result.dig('file')
|
180
|
+
end
|
181
|
+
end
|
182
|
+
throw :restart unless new_file&.file?
|
183
|
+
break
|
184
|
+
end
|
185
|
+
# catch :restart here
|
186
|
+
6.times do
|
187
|
+
new_file = OneDriveItem.smart_new(@od, @od.request("#{api_path}:/#{target_name}"))
|
188
|
+
break if new_file.file? && new_file.id != old_file.id
|
189
|
+
sleep 10 # wait for server to process the previous request
|
190
|
+
end
|
191
|
+
break if new_file.file? && new_file.id != old_file.id
|
192
|
+
# and retry the whole process
|
193
|
+
end
|
194
|
+
|
195
|
+
# upload completed
|
196
|
+
File.unlink(resume_file)
|
197
|
+
return new_file.set_mtime(File.mtime(filename))
|
198
|
+
end
|
199
|
+
|
200
|
+
# Uploads a local file into current remote directory using simple upload mode.
|
201
|
+
#
|
202
|
+
# @return [OneDriveFile,nil] uploaded file
|
203
|
+
def upload_simple(filename, overwrite:, target_name:)
|
204
|
+
target_file = get(filename)
|
205
|
+
exist = target_file.file?
|
206
|
+
return if exist && !overwrite
|
207
|
+
path = nil
|
208
|
+
if exist
|
209
|
+
path = "#{target_file.api_path}/content"
|
210
|
+
else
|
211
|
+
path = "#{api_path}:/#{target_name}:/content"
|
212
|
+
end
|
213
|
+
|
214
|
+
query = {
|
215
|
+
path: File.join('v1.0/me/', path),
|
216
|
+
headers: {
|
217
|
+
'Authorization': "Bearer #{@od.access_token.token}",
|
218
|
+
'Content-Type': 'application/octet-stream'
|
219
|
+
},
|
220
|
+
body: File.read(filename)
|
221
|
+
}
|
222
|
+
result = @od.conn.put(query)
|
223
|
+
result = JSON.parse(result.body)
|
224
|
+
file = OneDriveFile.new(@od, result)
|
225
|
+
file.set_mtime(File.mtime(filename))
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
data/lib/rb1drv/onedrive_file.rb
CHANGED
@@ -1,79 +1,79 @@
|
|
1
|
-
module Rb1drv
|
2
|
-
class OneDriveFile < OneDriveItem
|
3
|
-
attr_reader :download_url
|
4
|
-
def initialize(od, api_hash)
|
5
|
-
super
|
6
|
-
@download_url = api_hash.dig('@microsoft.graph.downloadUrl')
|
7
|
-
end
|
8
|
-
|
9
|
-
# No
|
10
|
-
def dir?
|
11
|
-
false
|
12
|
-
end
|
13
|
-
|
14
|
-
# Yes
|
15
|
-
def file?
|
16
|
-
true
|
17
|
-
end
|
18
|
-
|
19
|
-
# Saves current remote file as local file.
|
20
|
-
#
|
21
|
-
# Unfinished download is stored as +target_name.incomplete+ and renamed upon completion.
|
22
|
-
#
|
23
|
-
# @param target_name [String] desired local filename, a relative path to current directory or an absolute path
|
24
|
-
# @param overwrite [Boolean] whether to overwrite local file, or skip this
|
25
|
-
# @param resume [Boolean] whether to resume an unfinished download, or start over anyway
|
26
|
-
#
|
27
|
-
# @yield [event, status] for receive progress notification
|
28
|
-
# @yieldparam event [Symbol] event of this notification
|
29
|
-
# @yieldparam status [{Symbol => String,Integer}] details
|
30
|
-
def save_as(target_name=nil, overwrite: false, resume: true, &block)
|
31
|
-
target_name ||= @name
|
32
|
-
tmpfile = "#{target_name}.incomplete"
|
33
|
-
|
34
|
-
return if !overwrite && File.exist?(target_name)
|
35
|
-
|
36
|
-
if resume && File.size(tmpfile) > 0
|
37
|
-
from = File.size(tmpfile)
|
38
|
-
len = @size - from
|
39
|
-
fmode = 'ab'
|
40
|
-
headers = {
|
41
|
-
'Range': "bytes=#{from}-"
|
42
|
-
}
|
43
|
-
else
|
44
|
-
from = 0
|
45
|
-
len = @size
|
46
|
-
fmode = 'wb'
|
47
|
-
headers = {}
|
48
|
-
end
|
49
|
-
|
50
|
-
yield :new_segment, file: target_name, from: from if block_given?
|
51
|
-
File.open(tmpfile, mode: fmode, external_encoding: Encoding::BINARY) do |f|
|
52
|
-
Excon.get download_url, headers: headers, response_block: ->(chunk, remaining_bytes, total_bytes) do
|
53
|
-
f.write(chunk)
|
54
|
-
yield :progress, file: target_name, from: from, progress: total_bytes - remaining_bytes, total: total_bytes if block_given?
|
55
|
-
end
|
56
|
-
end
|
57
|
-
yield :finish_segment, file: target_name if block_given?
|
58
|
-
FileUtils.mv(tmpfile, filename)
|
59
|
-
end
|
60
|
-
|
61
|
-
# Change last modified time for a remote file.
|
62
|
-
#
|
63
|
-
# NOTICE: OneDrive by default keeps multiple history version for this operation.
|
64
|
-
# NOTICE: You must turn off versioning to prevent multiple counts on disk quota.
|
65
|
-
#
|
66
|
-
# 3 attempts will be made due to delay after a file upload.
|
67
|
-
#
|
68
|
-
# @param time [Time] desired last modified time
|
69
|
-
# @return [OneDriveFile] new file object returned by API
|
70
|
-
def set_mtime(time)
|
71
|
-
attempt = 0
|
72
|
-
OneDriveFile.new(@od, @od.request(api_path, {fileSystemInfo: {lastModifiedDateTime: time.utc.iso8601}}, :patch))
|
73
|
-
rescue
|
74
|
-
sleep 10
|
75
|
-
attempt += 1
|
76
|
-
retry if attempt <= 3
|
77
|
-
end
|
78
|
-
end
|
79
|
-
end
|
1
|
+
module Rb1drv
|
2
|
+
class OneDriveFile < OneDriveItem
|
3
|
+
attr_reader :download_url
|
4
|
+
def initialize(od, api_hash)
|
5
|
+
super
|
6
|
+
@download_url = api_hash.dig('@microsoft.graph.downloadUrl')
|
7
|
+
end
|
8
|
+
|
9
|
+
# No
|
10
|
+
def dir?
|
11
|
+
false
|
12
|
+
end
|
13
|
+
|
14
|
+
# Yes
|
15
|
+
def file?
|
16
|
+
true
|
17
|
+
end
|
18
|
+
|
19
|
+
# Saves current remote file as local file.
|
20
|
+
#
|
21
|
+
# Unfinished download is stored as +target_name.incomplete+ and renamed upon completion.
|
22
|
+
#
|
23
|
+
# @param target_name [String] desired local filename, a relative path to current directory or an absolute path
|
24
|
+
# @param overwrite [Boolean] whether to overwrite local file, or skip this
|
25
|
+
# @param resume [Boolean] whether to resume an unfinished download, or start over anyway
|
26
|
+
#
|
27
|
+
# @yield [event, status] for receive progress notification
|
28
|
+
# @yieldparam event [Symbol] event of this notification
|
29
|
+
# @yieldparam status [{Symbol => String,Integer}] details
|
30
|
+
def save_as(target_name=nil, overwrite: false, resume: true, &block)
|
31
|
+
target_name ||= @name
|
32
|
+
tmpfile = "#{target_name}.incomplete"
|
33
|
+
|
34
|
+
return if !overwrite && File.exist?(target_name)
|
35
|
+
|
36
|
+
if resume && File.size(tmpfile) > 0
|
37
|
+
from = File.size(tmpfile)
|
38
|
+
len = @size - from
|
39
|
+
fmode = 'ab'
|
40
|
+
headers = {
|
41
|
+
'Range': "bytes=#{from}-"
|
42
|
+
}
|
43
|
+
else
|
44
|
+
from = 0
|
45
|
+
len = @size
|
46
|
+
fmode = 'wb'
|
47
|
+
headers = {}
|
48
|
+
end
|
49
|
+
|
50
|
+
yield :new_segment, file: target_name, from: from if block_given?
|
51
|
+
File.open(tmpfile, mode: fmode, external_encoding: Encoding::BINARY) do |f|
|
52
|
+
Excon.get download_url, headers: headers, response_block: ->(chunk, remaining_bytes, total_bytes) do
|
53
|
+
f.write(chunk)
|
54
|
+
yield :progress, file: target_name, from: from, progress: total_bytes - remaining_bytes, total: total_bytes if block_given?
|
55
|
+
end
|
56
|
+
end
|
57
|
+
yield :finish_segment, file: target_name if block_given?
|
58
|
+
FileUtils.mv(tmpfile, filename)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Change last modified time for a remote file.
|
62
|
+
#
|
63
|
+
# NOTICE: OneDrive by default keeps multiple history version for this operation.
|
64
|
+
# NOTICE: You must turn off versioning to prevent multiple counts on disk quota.
|
65
|
+
#
|
66
|
+
# 3 attempts will be made due to delay after a file upload.
|
67
|
+
#
|
68
|
+
# @param time [Time] desired last modified time
|
69
|
+
# @return [OneDriveFile] new file object returned by API
|
70
|
+
def set_mtime(time)
|
71
|
+
attempt = 0
|
72
|
+
OneDriveFile.new(@od, @od.request(api_path, {fileSystemInfo: {lastModifiedDateTime: time.utc.iso8601}}, :patch))
|
73
|
+
rescue
|
74
|
+
sleep 10
|
75
|
+
attempt += 1
|
76
|
+
retry if attempt <= 3
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|