rb1drv 0.1.7 → 0.1.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- sleep 60 # wait for server to process the previous request
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
- # and retry the whole process
190
- end
191
-
192
- # upload completed
193
- File.unlink(resume_file)
194
- return new_file.set_mtime(File.mtime(filename))
195
- end
196
-
197
- # Uploads a local file into current remote directory using simple upload mode.
198
- #
199
- # @return [OneDriveFile,nil] uploaded file
200
- def upload_simple(filename, overwrite:, target_name:)
201
- target_file = get(filename)
202
- exist = target_file.file?
203
- return if exist && !overwrite
204
- path = nil
205
- if exist
206
- path = "#{target_file.api_path}/content"
207
- else
208
- path = "#{api_path}:/#{target_name}:/content"
209
- end
210
-
211
- query = {
212
- path: File.join('v1.0/me/', path),
213
- headers: {
214
- 'Authorization': "Bearer #{@od.access_token.token}",
215
- 'Content-Type': 'application/octet-stream'
216
- },
217
- body: File.read(filename)
218
- }
219
- result = @od.conn.put(query)
220
- result = JSON.parse(result.body)
221
- file = OneDriveFile.new(@od, result)
222
- file.set_mtime(File.mtime(filename))
223
- end
224
- end
225
- end
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
@@ -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