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.
@@ -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