net-sftp 4.0.2 → 4.1.1
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
- checksums.yaml.gz.sig +0 -0
- data/CHANGES.txt +18 -0
- data/README.rdoc +3 -0
- data/lib/net/sftp/operations/download.rb +220 -34
- data/lib/net/sftp/operations/upload.rb +126 -25
- data/lib/net/sftp/protocol/01/attributes.rb +5 -1
- data/lib/net/sftp/protocol/01/base.rb +2 -1
- data/lib/net/sftp/protocol/04/attributes.rb +2 -1
- data/lib/net/sftp/protocol/04/base.rb +2 -1
- data/lib/net/sftp/protocol/base.rb +2 -0
- data/lib/net/sftp/protocol/bounded_read.rb +25 -0
- data/lib/net/sftp/session.rb +16 -3
- data/lib/net/sftp/version.rb +2 -2
- data.tar.gz.sig +0 -0
- metadata +5 -16
- metadata.gz.sig +0 -0
- data/.github/pull_request_template.md +0 -1
- data/.github/workflows/changelog.yml +0 -18
- data/.github/workflows/ci.yml +0 -36
- data/.gitignore +0 -7
- data/CONTRIBUTING.md +0 -33
- data/Gemfile +0 -20
- data/Manifest +0 -55
- data/Rakefile +0 -152
- data/changelog.d/README.md +0 -1
- data/net-sftp-public_cert.pem +0 -21
- data/net-sftp.gemspec +0 -48
- data/setup.rb +0 -1331
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 618cad45c3de8f52cf2579f2616d27df2ecd69d35dce35c6b80822dab1a0f8ac
|
|
4
|
+
data.tar.gz: 7b07b32f66e9109e037582827bc2dc602b19479d93b6be3e526f263cd499062e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 47d88176bf4883648da4339a5d250e69c84921bc96a5424ff9bb5bb65d4c83b97fa2543e2a11f03d3043c6a04bf8bde5026c167ef6ccd9c1d880f7e8dcb08d10
|
|
7
|
+
data.tar.gz: c7568857412af07782a37dbc7516fb59af76e47bff76e9229a9fa2429076fca96ca06d92b6f9706de20c700917ecb887ea8e793d23d03c969fa698ad79c37280
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/CHANGES.txt
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
=== 4.1.1
|
|
2
|
+
|
|
3
|
+
* A read error during a single-file download is raised once, after the outstanding reads complete and the handle is closed, instead of once per in-flight read [#212]
|
|
4
|
+
* `abort!` now stops uploads and downloads: no new write or read is sent, the requests in flight finish and the open files are closed, so `wait` returns once their replies arrive. Previously the transfer kept running to the end, and an upload reported `:finish` twice. [#211]
|
|
5
|
+
* Single-file uploads keep up to `:requests` writes in flight (default 64, like OpenSSH's sftp client, previously 2) so large uploads are no longer bound by round-trip time. The file is closed once every write is acknowledged, and a failed write is raised once, after the close. [#210]
|
|
6
|
+
* Package only `lib`, `CHANGES.txt`, `LICENSE.txt` and `README.rdoc` in the gem, about 20% smaller. Thanks @yuri-zubov [#199]
|
|
7
|
+
|
|
8
|
+
=== 4.1.0
|
|
9
|
+
|
|
10
|
+
* BREAKING: Require Ruby 2.6 or later. Bundler on older Rubies resolves to 4.0.2. [#207]
|
|
11
|
+
* BREAKING: `upload` and `download` no longer close an IO object passed in as the local side; `download` flushes it instead. Callers that relied on the close must close it themselves. [#202]
|
|
12
|
+
* Security: An `FXP_VERSION` packet after the session is open now raises `Net::SFTP::Exception` instead of switching protocol version mid-session and dropping pending requests. [#193]
|
|
13
|
+
* Security: A server can no longer make the client allocate without bound, or hang, by sending an ACL, extended attribute or directory listing count larger than the packet holds; such packets now raise `Net::SFTP::Exception`. [#192]
|
|
14
|
+
* Single-file downloads keep up to `:requests` reads in flight (default 64) instead of waiting for each 32 KB reply, so large downloads are no longer bound by round-trip time. [#204]
|
|
15
|
+
* `upload!` and `download!` now give new files and directories the source's permissions (masked to 0777, subject to the umask) like OpenSSH's sftp client, so executables stay executable and private files are no longer created world-readable. The new `:preserve` option also copies access and modification times and sets permissions exactly. As with OpenSSH, attributes are best effort: if they can't be read the transfer uses default permissions, and if they can't be set a warning is logged and the transfer still succeeds. [#203]
|
|
16
|
+
* Added `Net::SFTP::Session#extensions`, the extensions the server advertised in its `FXP_VERSION` packet (such as `posix-rename@openssh.com`). [#195]
|
|
17
|
+
* The `:version` option now caps the negotiated SFTP protocol version; servers that always reply with their own version (OpenSSH replies 3) used to be negotiated at that version instead of the one requested. [#194]
|
|
18
|
+
|
|
1
19
|
=== 4.0.2
|
|
2
20
|
|
|
3
21
|
* Security: Recursive downloads refuse to follow an existing local symlink at a server-named path instead of writing through it. The local path passed by the caller may still be a symlink. Suggested in #162 by srosenhamer [#186]
|
data/README.rdoc
CHANGED
|
@@ -35,6 +35,9 @@ In a nutshell:
|
|
|
35
35
|
# download a file or directory from the remote host
|
|
36
36
|
sftp.download!("/path/to/remote", "/path/to/local")
|
|
37
37
|
|
|
38
|
+
# also copy access and modification times (permissions are always copied)
|
|
39
|
+
sftp.download!("/path/to/remote", "/path/to/local", :preserve => true)
|
|
40
|
+
|
|
38
41
|
# grab data off the remote host directly to a buffer
|
|
39
42
|
data = sftp.download!("/path/to/remote")
|
|
40
43
|
|
|
@@ -49,12 +49,26 @@ module Net; module SFTP; module Operations
|
|
|
49
49
|
# * <tt>:progress</tt> - either a block or an object to act as a progress
|
|
50
50
|
# callback. See the discussion of "progress monitoring" below.
|
|
51
51
|
# * <tt>:requests</tt> - the number of pending SFTP requests to allow at
|
|
52
|
-
# any given time. When downloading
|
|
53
|
-
#
|
|
54
|
-
#
|
|
52
|
+
# any given time. When downloading a single file, this is the number of
|
|
53
|
+
# reads kept in flight and defaults to 64. When downloading an entire
|
|
54
|
+
# directory tree recursively, it is the number of files and directories
|
|
55
|
+
# transferred at once and defaults to 16. Setting this higher might
|
|
56
|
+
# improve throughput, particularly on high-latency links. Reducing it
|
|
57
|
+
# will reduce throughput.
|
|
55
58
|
# * <tt>:read_size</tt> - the maximum number of bytes to read at a time
|
|
56
59
|
# from the source. Increasing this value might improve throughput. It
|
|
57
60
|
# defaults to 32,000 bytes.
|
|
61
|
+
# * <tt>:preserve</tt> - if true, the local files and directories get the
|
|
62
|
+
# access and modification times of the remote ones, and their permissions
|
|
63
|
+
# are set exactly, even on files that already exist and regardless of the
|
|
64
|
+
# umask.
|
|
65
|
+
#
|
|
66
|
+
# Like OpenSSH's sftp client, new local files and directories are created
|
|
67
|
+
# with the permissions of the remote ones, without setuid, setgid and sticky
|
|
68
|
+
# bits and subject to the umask. Nothing is set when +local+ is an IO object.
|
|
69
|
+
# If the remote attributes can't be read, the download goes ahead with
|
|
70
|
+
# default permissions; if they can't be applied locally, a warning is logged
|
|
71
|
+
# and the download still succeeds.
|
|
58
72
|
#
|
|
59
73
|
# == Progress Monitoring
|
|
60
74
|
#
|
|
@@ -155,6 +169,8 @@ module Net; module SFTP; module Operations
|
|
|
155
169
|
@progress = progress || options[:progress]
|
|
156
170
|
@options = options
|
|
157
171
|
@active = 0
|
|
172
|
+
@aborted = false
|
|
173
|
+
@directories = []
|
|
158
174
|
@properties = options[:properties] || {}
|
|
159
175
|
requests
|
|
160
176
|
read_size
|
|
@@ -181,9 +197,10 @@ module Net; module SFTP; module Operations
|
|
|
181
197
|
@active > 0 || stack.any?
|
|
182
198
|
end
|
|
183
199
|
|
|
184
|
-
# Forces the transfer to stop.
|
|
200
|
+
# Forces the transfer to stop. Reads already sent are left to finish,
|
|
201
|
+
# then each open file is closed, so #wait returns once that is done.
|
|
185
202
|
def abort!
|
|
186
|
-
@
|
|
203
|
+
@aborted = true
|
|
187
204
|
@stack.clear
|
|
188
205
|
end
|
|
189
206
|
|
|
@@ -210,7 +227,8 @@ module Net; module SFTP; module Operations
|
|
|
210
227
|
|
|
211
228
|
# A simple struct for encapsulating information about a single remote
|
|
212
229
|
# file or directory that needs to be downloaded.
|
|
213
|
-
Entry = Struct.new(:remote, :local, :directory, :size, :handle, :offset, :sink
|
|
230
|
+
Entry = Struct.new(:remote, :local, :directory, :size, :handle, :offset, :sink,
|
|
231
|
+
:next_read, :pending, :window, :chunks, :eof, :attributes, :error)
|
|
214
232
|
|
|
215
233
|
#--
|
|
216
234
|
# "ruby -w" hates private attributes, so we have to do these longhand
|
|
@@ -231,10 +249,19 @@ module Net; module SFTP; module Operations
|
|
|
231
249
|
@read_size ||= positive_option(:read_size, DEFAULT_READ_SIZE)
|
|
232
250
|
end
|
|
233
251
|
|
|
234
|
-
# The number of
|
|
235
|
-
# Defaults to 16 for recursive downloads.
|
|
252
|
+
# The number of simultaneous SFTP requests to use to effect the download.
|
|
253
|
+
# Defaults to 16 for recursive downloads and 64 for single files.
|
|
236
254
|
def requests
|
|
237
|
-
@requests ||= positive_option(:requests, recursive? ? 16 :
|
|
255
|
+
@requests ||= positive_option(:requests, recursive? ? 16 : 64)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# The most reads a single file may have outstanding at once.
|
|
259
|
+
def read_window_limit
|
|
260
|
+
recursive? ? 1 : requests
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def preserve?
|
|
264
|
+
options[:preserve]
|
|
238
265
|
end
|
|
239
266
|
|
|
240
267
|
def positive_option(name, default)
|
|
@@ -254,7 +281,12 @@ module Net; module SFTP; module Operations
|
|
|
254
281
|
if entry.directory
|
|
255
282
|
update_progress(:mkdir, entry.local)
|
|
256
283
|
refuse_symlink!(entry.local) if server_named?(entry) && ::File.symlink?(entry.local)
|
|
257
|
-
|
|
284
|
+
created = !::File.directory?(entry.local)
|
|
285
|
+
::Dir.mkdir(entry.local, local_permissions(entry, 0777) | 0300) if created
|
|
286
|
+
if created || preserve?
|
|
287
|
+
@directories << entry
|
|
288
|
+
stat_directory(entry) unless entry.attributes
|
|
289
|
+
end
|
|
258
290
|
request = sftp.opendir(entry.remote, &method(:on_opendir))
|
|
259
291
|
request[:entry] = entry
|
|
260
292
|
else
|
|
@@ -262,7 +294,64 @@ module Net; module SFTP; module Operations
|
|
|
262
294
|
end
|
|
263
295
|
end
|
|
264
296
|
|
|
265
|
-
|
|
297
|
+
if !active?
|
|
298
|
+
set_directory_attributes
|
|
299
|
+
update_progress(:finish)
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Only entries listed by readdir come with attributes.
|
|
304
|
+
def stat_directory(entry)
|
|
305
|
+
@active += 1
|
|
306
|
+
sftp.stat(entry.remote) do |response|
|
|
307
|
+
@active -= 1
|
|
308
|
+
entry.attributes = remote_attributes(response, "stat #{entry.remote}")
|
|
309
|
+
process_next_entry
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Without the remote attributes the download goes ahead with default
|
|
314
|
+
# permissions and no times, as it would for a server that never sent any.
|
|
315
|
+
def remote_attributes(response, operation)
|
|
316
|
+
return response[:attrs] if response.ok?
|
|
317
|
+
|
|
318
|
+
debug { "#{operation} failed, using default attributes: #{response}" }
|
|
319
|
+
nil
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# Deepest directories first, after their contents; setting attributes on
|
|
323
|
+
# a directory doesn't change its parent's modification time.
|
|
324
|
+
def set_directory_attributes
|
|
325
|
+
@directories.reverse_each do |entry|
|
|
326
|
+
next unless entry.attributes
|
|
327
|
+
|
|
328
|
+
permissions = local_permissions(entry, 0777)
|
|
329
|
+
permissions &= ~::File.umask unless preserve?
|
|
330
|
+
set_local_attributes(entry, permissions)
|
|
331
|
+
end
|
|
332
|
+
@directories.clear
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# setuid, setgid and sticky bits are never applied, matching OpenSSH.
|
|
336
|
+
def local_permissions(entry, default)
|
|
337
|
+
permissions = entry.attributes && entry.attributes.permissions
|
|
338
|
+
permissions ? permissions & 0777 : default
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# Like OpenSSH's sftp client, failing to set attributes only warns; the
|
|
342
|
+
# data has already been transferred.
|
|
343
|
+
def set_local_attributes(entry, permissions)
|
|
344
|
+
attempt_local("chmod", entry) { ::File.chmod(permissions, entry.local) } if permissions
|
|
345
|
+
|
|
346
|
+
attributes = entry.attributes
|
|
347
|
+
return unless preserve? && attributes && attributes.atime && attributes.mtime
|
|
348
|
+
attempt_local("utime", entry) { ::File.utime(attributes.atime, attributes.mtime, entry.local) }
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def attempt_local(operation, entry)
|
|
352
|
+
yield
|
|
353
|
+
rescue SystemCallError => e
|
|
354
|
+
lwarn { "#{operation} #{entry.local} failed: #{e.message}" }
|
|
266
355
|
end
|
|
267
356
|
|
|
268
357
|
# Called when a remote directory is "opened" for reading, e.g. to
|
|
@@ -281,7 +370,7 @@ module Net; module SFTP; module Operations
|
|
|
281
370
|
# and #process_next_entry is called.
|
|
282
371
|
def on_readdir(response)
|
|
283
372
|
entry = response.request[:parent]
|
|
284
|
-
if response.eof?
|
|
373
|
+
if response.eof? || @aborted
|
|
285
374
|
request = sftp.close(entry.handle, &method(:on_closedir))
|
|
286
375
|
request[:parent] = entry
|
|
287
376
|
elsif !response.ok?
|
|
@@ -290,7 +379,9 @@ module Net; module SFTP; module Operations
|
|
|
290
379
|
response[:names].each do |item|
|
|
291
380
|
next if item.name == "." || item.name == ".."
|
|
292
381
|
validate_entry_name!(entry, item.name)
|
|
293
|
-
|
|
382
|
+
child = Entry.new(::File.join(entry.remote, item.name), ::File.join(entry.local, item.name), item.directory?, item.attributes.size)
|
|
383
|
+
child.attributes = item.attributes
|
|
384
|
+
stack << child
|
|
294
385
|
end
|
|
295
386
|
|
|
296
387
|
# take this opportunity to enqueue more requests
|
|
@@ -316,11 +407,13 @@ module Net; module SFTP; module Operations
|
|
|
316
407
|
entry.local != local
|
|
317
408
|
end
|
|
318
409
|
|
|
410
|
+
# Like OpenSSH, the file is always created writable by its owner.
|
|
319
411
|
def open_local_file(entry)
|
|
320
|
-
|
|
412
|
+
permissions = local_permissions(entry, 0666) | 0200
|
|
413
|
+
return ::File.open(entry.local, "wb", permissions) unless server_named?(entry) && defined?(::File::NOFOLLOW)
|
|
321
414
|
|
|
322
415
|
begin
|
|
323
|
-
::File.open(entry.local, "wb", :flags => ::File::NOFOLLOW)
|
|
416
|
+
::File.open(entry.local, "wb", permissions, :flags => ::File::NOFOLLOW)
|
|
324
417
|
rescue SystemCallError
|
|
325
418
|
raise unless ::File.symlink?(entry.local)
|
|
326
419
|
refuse_symlink!(entry.local)
|
|
@@ -346,55 +439,148 @@ module Net; module SFTP; module Operations
|
|
|
346
439
|
process_next_entry
|
|
347
440
|
end
|
|
348
441
|
|
|
349
|
-
# Called when a file has been opened. This will call #
|
|
442
|
+
# Called when a file has been opened. This will call #request_reads
|
|
350
443
|
# to initiate the data transfer.
|
|
351
444
|
def on_open(response)
|
|
352
445
|
entry = response.request[:entry]
|
|
353
446
|
raise StatusException.new(response, "open #{entry.remote}") unless response.ok?
|
|
354
447
|
|
|
355
448
|
entry.handle = response[:handle]
|
|
449
|
+
if @aborted || entry.attributes || entry.local.respond_to?(:write)
|
|
450
|
+
start_download(entry)
|
|
451
|
+
else
|
|
452
|
+
request = sftp.fstat(entry.handle, &method(:on_fstat))
|
|
453
|
+
request[:entry] = entry
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
# Called when the attributes of a file opened by #open_file arrive.
|
|
458
|
+
def on_fstat(response)
|
|
459
|
+
entry = response.request[:entry]
|
|
460
|
+
entry.attributes = remote_attributes(response, "fstat #{entry.remote}")
|
|
461
|
+
start_download(entry)
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def start_download(entry)
|
|
465
|
+
return close_aborted_file(entry) if @aborted
|
|
466
|
+
|
|
356
467
|
entry.sink = entry.local.respond_to?(:write) ? entry.local : open_local_file(entry)
|
|
357
468
|
entry.offset = 0
|
|
469
|
+
entry.next_read = 0
|
|
470
|
+
entry.pending = 0
|
|
471
|
+
entry.window = 1
|
|
472
|
+
entry.chunks = {}
|
|
473
|
+
|
|
474
|
+
request_reads(entry)
|
|
475
|
+
end
|
|
358
476
|
|
|
359
|
-
|
|
477
|
+
# Keeps up to entry.window reads of #read_size bytes outstanding until
|
|
478
|
+
# the server reports EOF. The window starts at one and grows with each
|
|
479
|
+
# data reply, so small files don't cost a burst of reads past their end.
|
|
480
|
+
def request_reads(entry)
|
|
481
|
+
while !entry.eof && !@aborted && entry.pending < entry.window
|
|
482
|
+
read_range(entry, entry.next_read, read_size)
|
|
483
|
+
entry.next_read += read_size
|
|
484
|
+
end
|
|
360
485
|
end
|
|
361
486
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
request = sftp.read(entry.handle,
|
|
487
|
+
def read_range(entry, offset, length)
|
|
488
|
+
entry.pending += 1
|
|
489
|
+
request = sftp.read(entry.handle, offset, length, &method(:on_read))
|
|
365
490
|
request[:entry] = entry
|
|
366
|
-
request[:offset] =
|
|
491
|
+
request[:offset] = offset
|
|
492
|
+
request[:length] = length
|
|
367
493
|
end
|
|
368
494
|
|
|
369
|
-
# Called when a read from a file finishes.
|
|
370
|
-
#
|
|
371
|
-
#
|
|
495
|
+
# Called when a read from a file finishes. Replies can arrive out of
|
|
496
|
+
# order, so data is buffered until everything before it has been
|
|
497
|
+
# written. Once the server has reported EOF or a read has failed, and
|
|
498
|
+
# no reads are left outstanding, the file is closed.
|
|
372
499
|
def on_read(response)
|
|
373
500
|
entry = response.request[:entry]
|
|
501
|
+
entry.pending -= 1
|
|
374
502
|
|
|
375
|
-
if
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
request[:entry] = entry
|
|
503
|
+
if entry.error
|
|
504
|
+
# The rest of the file is no longer wanted.
|
|
505
|
+
elsif response.eof?
|
|
506
|
+
entry.eof = true
|
|
380
507
|
elsif !response.ok?
|
|
381
|
-
|
|
508
|
+
entry.error = StatusException.new(response, "read #{entry.remote}")
|
|
509
|
+
elsif response[:data].empty? || response[:data].bytesize > response.request[:length]
|
|
510
|
+
entry.error = Net::SFTP::Exception.new("read #{entry.remote} returned #{response[:data].bytesize} bytes for a #{response.request[:length]} byte request")
|
|
382
511
|
else
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
512
|
+
offset = response.request[:offset]
|
|
513
|
+
length = response.request[:length]
|
|
514
|
+
data = response[:data]
|
|
515
|
+
|
|
516
|
+
# Servers may return less than asked for without being at EOF, and
|
|
517
|
+
# the reads after this one have already been sent.
|
|
518
|
+
read_range(entry, offset + data.bytesize, length - data.bytesize) if data.bytesize < length && !@aborted
|
|
519
|
+
|
|
520
|
+
entry.chunks[offset] = data
|
|
521
|
+
write_buffered_chunks(entry)
|
|
522
|
+
entry.window += 1 if entry.window < read_window_limit
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
if entry.error
|
|
526
|
+
close_failed_file(entry) if entry.pending.zero?
|
|
527
|
+
else
|
|
528
|
+
request_reads(entry)
|
|
529
|
+
close_file(entry) if entry.pending.zero?
|
|
530
|
+
end
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
def write_buffered_chunks(entry)
|
|
534
|
+
while (data = entry.chunks.delete(entry.offset))
|
|
535
|
+
update_progress(:get, entry, entry.offset, data)
|
|
536
|
+
entry.sink.write(data)
|
|
537
|
+
entry.offset += data.bytesize
|
|
387
538
|
end
|
|
388
539
|
end
|
|
389
540
|
|
|
541
|
+
def close_file(entry)
|
|
542
|
+
if entry.chunks.any? && !@aborted
|
|
543
|
+
raise Net::SFTP::Exception, "read #{entry.remote} hit EOF at #{entry.offset} but the server returned data beyond it"
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
update_progress(:close, entry)
|
|
547
|
+
# The caller's IO stays open, but is flushed so its data is written when download! returns.
|
|
548
|
+
if entry.sink.equal?(local)
|
|
549
|
+
entry.sink.flush if entry.sink.respond_to?(:flush)
|
|
550
|
+
else
|
|
551
|
+
entry.sink.close
|
|
552
|
+
end
|
|
553
|
+
unless entry.local.respond_to?(:write)
|
|
554
|
+
set_local_attributes(entry, preserve? ? local_permissions(entry, nil) : nil)
|
|
555
|
+
end
|
|
556
|
+
request = sftp.close(entry.handle, &method(:on_close))
|
|
557
|
+
request[:entry] = entry
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
# The local file is left untouched: it may hold an earlier copy.
|
|
561
|
+
def close_aborted_file(entry)
|
|
562
|
+
update_progress(:close, entry)
|
|
563
|
+
request = sftp.close(entry.handle, &method(:on_close))
|
|
564
|
+
request[:entry] = entry
|
|
565
|
+
end
|
|
566
|
+
|
|
390
567
|
# Called when a file handle is closed.
|
|
391
568
|
def on_close(response)
|
|
392
569
|
@active -= 1
|
|
393
570
|
entry = response.request[:entry]
|
|
571
|
+
raise entry.error if entry.error
|
|
394
572
|
raise StatusException.new(response, "close #{entry.remote}") unless response.ok?
|
|
395
573
|
process_next_entry
|
|
396
574
|
end
|
|
397
575
|
|
|
576
|
+
# Waiting for every reply before raising keeps the remote handle from
|
|
577
|
+
# leaking and the error from being raised again by each later reply.
|
|
578
|
+
def close_failed_file(entry)
|
|
579
|
+
entry.sink.close unless entry.sink.equal?(local)
|
|
580
|
+
request = sftp.close(entry.handle, &method(:on_close))
|
|
581
|
+
request[:entry] = entry
|
|
582
|
+
end
|
|
583
|
+
|
|
398
584
|
# If a progress callback or object has been set, this will report
|
|
399
585
|
# the progress to that callback or object.
|
|
400
586
|
def update_progress(hook, *args)
|
|
@@ -47,8 +47,9 @@ module Net; module SFTP; module Operations
|
|
|
47
47
|
# callback. See the discussion of "progress monitoring" below.
|
|
48
48
|
# * <tt>:requests</tt> - the number of pending SFTP requests to allow at
|
|
49
49
|
# any given time. When uploading an entire directory tree recursively,
|
|
50
|
-
# this will default to 16, otherwise it will default to
|
|
51
|
-
# higher might improve throughput. Reducing it
|
|
50
|
+
# this will default to 16, otherwise it will default to 64, like OpenSSH's
|
|
51
|
+
# sftp client. Setting this higher might improve throughput. Reducing it
|
|
52
|
+
# will reduce throughput.
|
|
52
53
|
# * <tt>:read_size</tt> - the maximum number of bytes to read at a time
|
|
53
54
|
# from the source. Increasing this value might improve throughput. It
|
|
54
55
|
# defaults to 32,000 bytes. Each chunk is sent as one write request, so
|
|
@@ -56,6 +57,15 @@ module Net; module SFTP; module Operations
|
|
|
56
57
|
# connection on packets over 256 KiB).
|
|
57
58
|
# * <tt>:name</tt> - the filename to report to the progress monitor when
|
|
58
59
|
# an IO object is given as +local+. This defaults to "<memory>".
|
|
60
|
+
# * <tt>:preserve</tt> - if true, the remote files and directories get the
|
|
61
|
+
# access and modification times of the local ones, and their permissions
|
|
62
|
+
# are set even on files that already exist remotely.
|
|
63
|
+
#
|
|
64
|
+
# Like OpenSSH's sftp client, new remote files and directories are created
|
|
65
|
+
# with the permissions of the local ones, without setuid, setgid and sticky
|
|
66
|
+
# bits and subject to the server's umask. Nothing is set when +local+ is an
|
|
67
|
+
# IO object. If the server refuses to set the attributes after the data has
|
|
68
|
+
# been written, a warning is logged and the upload still succeeds.
|
|
59
69
|
#
|
|
60
70
|
# == Progress Monitoring
|
|
61
71
|
#
|
|
@@ -154,6 +164,8 @@ module Net; module SFTP; module Operations
|
|
|
154
164
|
@options = options
|
|
155
165
|
@properties = options[:properties] || {}
|
|
156
166
|
@active = 0
|
|
167
|
+
@aborted = false
|
|
168
|
+
@directories = []
|
|
157
169
|
|
|
158
170
|
self.logger = sftp.logger
|
|
159
171
|
|
|
@@ -169,7 +181,7 @@ module Net; module SFTP; module Operations
|
|
|
169
181
|
|
|
170
182
|
@active += 1
|
|
171
183
|
if @options[:mkdir]
|
|
172
|
-
sftp.mkdir(remote) do |response|
|
|
184
|
+
sftp.mkdir(remote, directory_attributes(local, remote)) do |response|
|
|
173
185
|
@active -= 1
|
|
174
186
|
raise StatusException.new(response, "mkdir `#{remote}'") unless response.ok?
|
|
175
187
|
requests.times do
|
|
@@ -199,11 +211,11 @@ module Net; module SFTP; module Operations
|
|
|
199
211
|
@active > 0 || @stack.any?
|
|
200
212
|
end
|
|
201
213
|
|
|
202
|
-
# Forces the transfer to stop.
|
|
214
|
+
# Forces the transfer to stop. Writes already sent are left to finish,
|
|
215
|
+
# then each open file is closed, so #wait returns once that is done.
|
|
203
216
|
def abort!
|
|
204
|
-
@
|
|
217
|
+
@aborted = true
|
|
205
218
|
@stack.clear
|
|
206
|
-
@uploads.clear
|
|
207
219
|
end
|
|
208
220
|
|
|
209
221
|
# Blocks until the upload has completed.
|
|
@@ -248,15 +260,19 @@ module Net; module SFTP; module Operations
|
|
|
248
260
|
value
|
|
249
261
|
end
|
|
250
262
|
|
|
263
|
+
def preserve?
|
|
264
|
+
options[:preserve]
|
|
265
|
+
end
|
|
266
|
+
|
|
251
267
|
# A simple struct for recording metadata about the file currently being
|
|
252
268
|
# uploaded.
|
|
253
|
-
LiveFile = Struct.new(:local, :remote, :io, :size, :handle)
|
|
269
|
+
LiveFile = Struct.new(:local, :remote, :io, :size, :handle, :attributes, :pending, :error)
|
|
254
270
|
|
|
255
271
|
# The default # of bytes to read from disk at a time.
|
|
256
272
|
DEFAULT_READ_SIZE = 32_000
|
|
257
273
|
|
|
258
274
|
# The number of readers to use when uploading a single file.
|
|
259
|
-
SINGLE_FILE_READERS =
|
|
275
|
+
SINGLE_FILE_READERS = 64
|
|
260
276
|
|
|
261
277
|
# The number of readers to use when uploading a directory.
|
|
262
278
|
RECURSIVE_READERS = 16
|
|
@@ -268,7 +284,11 @@ module Net; module SFTP; module Operations
|
|
|
268
284
|
if @uploads.any?
|
|
269
285
|
write_next_chunk(@uploads.first)
|
|
270
286
|
elsif !active?
|
|
271
|
-
|
|
287
|
+
if @directories.any?
|
|
288
|
+
set_directory_attributes
|
|
289
|
+
else
|
|
290
|
+
update_progress(:finish)
|
|
291
|
+
end
|
|
272
292
|
end
|
|
273
293
|
return false
|
|
274
294
|
elsif @stack.last.empty?
|
|
@@ -288,7 +308,7 @@ module Net; module SFTP; module Operations
|
|
|
288
308
|
|
|
289
309
|
@active += 1
|
|
290
310
|
update_progress(:mkdir, rpath)
|
|
291
|
-
request = sftp.mkdir(rpath, &method(:on_mkdir))
|
|
311
|
+
request = sftp.mkdir(rpath, directory_attributes(lpath, rpath), &method(:on_mkdir))
|
|
292
312
|
request[:dir] = rpath
|
|
293
313
|
else
|
|
294
314
|
open_file(lpath, rpath)
|
|
@@ -317,10 +337,11 @@ module Net; module SFTP; module Operations
|
|
|
317
337
|
size = file.size
|
|
318
338
|
end
|
|
319
339
|
|
|
320
|
-
|
|
340
|
+
attributes = local.respond_to?(:read) ? {} : attributes_for(file.stat)
|
|
341
|
+
metafile = LiveFile.new(name, remote, file, size, nil, attributes, 0)
|
|
321
342
|
update_progress(:open, metafile)
|
|
322
343
|
|
|
323
|
-
request = sftp.open(remote, "w", &method(:on_open))
|
|
344
|
+
request = sftp.open(remote, "w", attributes, &method(:on_open))
|
|
324
345
|
request[:file] = metafile
|
|
325
346
|
end
|
|
326
347
|
|
|
@@ -353,14 +374,25 @@ module Net; module SFTP; module Operations
|
|
|
353
374
|
end
|
|
354
375
|
end
|
|
355
376
|
|
|
356
|
-
# Called when a +write+ request finishes.
|
|
357
|
-
#
|
|
358
|
-
#
|
|
377
|
+
# Called when a +write+ request finishes. The first failed write stops
|
|
378
|
+
# the file from being read further and is recorded, to be raised once
|
|
379
|
+
# the file is closed. The file is closed once the whole file has been
|
|
380
|
+
# sent or a write has failed, and no writes are left outstanding.
|
|
359
381
|
def on_write(response)
|
|
360
382
|
@active -= 1
|
|
361
383
|
file = response.request[:file]
|
|
362
|
-
|
|
363
|
-
|
|
384
|
+
file.pending -= 1
|
|
385
|
+
|
|
386
|
+
if !response.ok? && !file.error
|
|
387
|
+
file.error = StatusException.new(response, "write #{file.remote}")
|
|
388
|
+
stop_reading(file)
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
if file.io.nil? && file.pending.zero?
|
|
392
|
+
close_file(file)
|
|
393
|
+
elsif !file.error
|
|
394
|
+
write_next_chunk(file)
|
|
395
|
+
end
|
|
364
396
|
end
|
|
365
397
|
|
|
366
398
|
# Called when a +close+ request finishes. Raises a StatusException if the
|
|
@@ -369,27 +401,72 @@ module Net; module SFTP; module Operations
|
|
|
369
401
|
def on_close(response)
|
|
370
402
|
@active -= 1
|
|
371
403
|
file = response.request[:file]
|
|
404
|
+
raise file.error if file.error
|
|
372
405
|
raise StatusException.new(response, "close #{file.remote}") unless response.ok?
|
|
373
406
|
process_next_entry
|
|
374
407
|
end
|
|
375
408
|
|
|
409
|
+
# setuid, setgid and sticky bits are never sent, matching OpenSSH.
|
|
410
|
+
def attributes_for(stat)
|
|
411
|
+
attributes = { :permissions => stat.mode & 0777 }
|
|
412
|
+
attributes.update(:atime => stat.atime.to_i, :mtime => stat.mtime.to_i) if preserve?
|
|
413
|
+
attributes
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# The directory is created writable and searchable by its owner so its
|
|
417
|
+
# contents can be uploaded; its real attributes are set once everything
|
|
418
|
+
# else is done (see #set_directory_attributes).
|
|
419
|
+
def directory_attributes(local, remote)
|
|
420
|
+
stat = ::File.stat(local)
|
|
421
|
+
@directories << [remote, attributes_for(stat)]
|
|
422
|
+
{ :permissions => (stat.mode & 0777) | 0300 }
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
# Deepest directories first; setting attributes on a directory doesn't
|
|
426
|
+
# change its parent's modification time.
|
|
427
|
+
def set_directory_attributes
|
|
428
|
+
@directories.reverse_each do |path, attributes|
|
|
429
|
+
@active += 1
|
|
430
|
+
sftp.setstat(path, attributes) do |response|
|
|
431
|
+
@active -= 1
|
|
432
|
+
warn_unless_ok(response, "setstat #{path}")
|
|
433
|
+
process_next_entry
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
@directories.clear
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
# Called when the +fsetstat+ for :preserve finishes.
|
|
440
|
+
def on_fsetstat(response)
|
|
441
|
+
@active -= 1
|
|
442
|
+
file = response.request[:file]
|
|
443
|
+
warn_unless_ok(response, "fsetstat #{file.remote}")
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# Like OpenSSH's sftp client, failing to set attributes only warns; the
|
|
447
|
+
# data has already been transferred.
|
|
448
|
+
def warn_unless_ok(response, operation)
|
|
449
|
+
lwarn { "#{operation} failed: #{response}" } unless response.ok?
|
|
450
|
+
end
|
|
451
|
+
|
|
376
452
|
# Attempts to send the next chunk from the given file (where +file+ is
|
|
377
453
|
# a LiveFile instance).
|
|
378
454
|
def write_next_chunk(file)
|
|
379
455
|
if file.io.nil?
|
|
380
456
|
process_next_entry
|
|
381
457
|
else
|
|
382
|
-
@active += 1
|
|
383
458
|
offset = file.io.pos
|
|
384
|
-
data = file.io.read(read_size)
|
|
459
|
+
data = file.io.read(read_size) unless @aborted
|
|
385
460
|
if data.nil?
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
461
|
+
stop_reading(file)
|
|
462
|
+
if file.pending.zero?
|
|
463
|
+
close_file(file)
|
|
464
|
+
else
|
|
465
|
+
process_next_entry
|
|
466
|
+
end
|
|
392
467
|
else
|
|
468
|
+
@active += 1
|
|
469
|
+
file.pending += 1
|
|
393
470
|
update_progress(:put, file, offset, data)
|
|
394
471
|
request = sftp.write(file.handle, offset, data, &method(:on_write))
|
|
395
472
|
request[:file] = file
|
|
@@ -397,6 +474,30 @@ module Net; module SFTP; module Operations
|
|
|
397
474
|
end
|
|
398
475
|
end
|
|
399
476
|
|
|
477
|
+
def stop_reading(file)
|
|
478
|
+
# An IO passed in by the caller is theirs to close.
|
|
479
|
+
file.io.close unless file.io.equal?(local)
|
|
480
|
+
file.io = nil
|
|
481
|
+
@uploads.delete(file)
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
# Like OpenSSH's sftp client, CLOSE waits for every write to be
|
|
485
|
+
# acknowledged, so a failed write is raised once and the remote handle
|
|
486
|
+
# is not leaked.
|
|
487
|
+
def close_file(file)
|
|
488
|
+
@active += 1
|
|
489
|
+
unless file.error
|
|
490
|
+
update_progress(:close, file)
|
|
491
|
+
if preserve? && file.attributes.any?
|
|
492
|
+
@active += 1
|
|
493
|
+
request = sftp.fsetstat(file.handle, file.attributes, &method(:on_fsetstat))
|
|
494
|
+
request[:file] = file
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
request = sftp.close(file.handle, &method(:on_close))
|
|
498
|
+
request[:file] = file
|
|
499
|
+
end
|
|
500
|
+
|
|
400
501
|
# Returns all directory entries for the given path, removing the '.'
|
|
401
502
|
# and '..' relative paths.
|
|
402
503
|
def entries_for(local)
|