net-sftp 4.0.0 → 4.1.0
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/.github/pull_request_template.md +1 -0
- data/.github/workflows/changelog.yml +18 -0
- data/.github/workflows/ci.yml +25 -23
- data/.gitignore +1 -0
- data/CHANGES.txt +45 -0
- data/CONTRIBUTING.md +33 -0
- data/Gemfile +7 -2
- data/README.rdoc +3 -0
- data/Rakefile +124 -1
- data/changelog.d/README.md +1 -0
- data/lib/net/sftp/operations/dir.rb +5 -4
- data/lib/net/sftp/operations/download.rb +236 -29
- data/lib/net/sftp/operations/file.rb +60 -26
- data/lib/net/sftp/operations/upload.rb +102 -11
- data/lib/net/sftp/protocol/01/attributes.rb +6 -2
- data/lib/net/sftp/protocol/01/base.rb +2 -1
- data/lib/net/sftp/protocol/01/name.rb +1 -1
- data/lib/net/sftp/protocol/03/base.rb +11 -1
- data/lib/net/sftp/protocol/04/attributes.rb +12 -2
- data/lib/net/sftp/protocol/04/base.rb +2 -1
- data/lib/net/sftp/protocol/04/name.rb +23 -14
- data/lib/net/sftp/protocol/05/base.rb +9 -6
- data/lib/net/sftp/protocol/base.rb +10 -2
- data/lib/net/sftp/protocol/bounded_read.rb +25 -0
- data/lib/net/sftp/session.rb +21 -3
- data/lib/net/sftp/version.rb +1 -1
- data/lib/net/sftp.rb +16 -10
- data/net-sftp-public_cert.pem +19 -18
- data/net-sftp.gemspec +4 -3
- data.tar.gz.sig +1 -1
- metadata +33 -27
- metadata.gz.sig +4 -4
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require 'pathname'
|
|
1
2
|
require 'net/ssh/loggable'
|
|
2
3
|
|
|
3
4
|
module Net; module SFTP; module Operations
|
|
@@ -48,12 +49,26 @@ module Net; module SFTP; module Operations
|
|
|
48
49
|
# * <tt>:progress</tt> - either a block or an object to act as a progress
|
|
49
50
|
# callback. See the discussion of "progress monitoring" below.
|
|
50
51
|
# * <tt>:requests</tt> - the number of pending SFTP requests to allow at
|
|
51
|
-
# any given time. When downloading
|
|
52
|
-
#
|
|
53
|
-
#
|
|
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.
|
|
54
58
|
# * <tt>:read_size</tt> - the maximum number of bytes to read at a time
|
|
55
59
|
# from the source. Increasing this value might improve throughput. It
|
|
56
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.
|
|
57
72
|
#
|
|
58
73
|
# == Progress Monitoring
|
|
59
74
|
#
|
|
@@ -145,13 +160,19 @@ module Net; module SFTP; module Operations
|
|
|
145
160
|
# This will return immediately, and requires that the SSH event loop be
|
|
146
161
|
# run in order to effect the download. (See #wait.)
|
|
147
162
|
def initialize(sftp, local, remote, options={}, &progress)
|
|
163
|
+
local = local.to_path if local.is_a?(Pathname)
|
|
164
|
+
remote = remote.to_path if remote.is_a?(Pathname)
|
|
165
|
+
|
|
148
166
|
@sftp = sftp
|
|
149
167
|
@local = local
|
|
150
168
|
@remote = remote
|
|
151
169
|
@progress = progress || options[:progress]
|
|
152
170
|
@options = options
|
|
153
171
|
@active = 0
|
|
172
|
+
@directories = []
|
|
154
173
|
@properties = options[:properties] || {}
|
|
174
|
+
requests
|
|
175
|
+
read_size
|
|
155
176
|
|
|
156
177
|
self.logger = sftp.logger
|
|
157
178
|
|
|
@@ -204,7 +225,8 @@ module Net; module SFTP; module Operations
|
|
|
204
225
|
|
|
205
226
|
# A simple struct for encapsulating information about a single remote
|
|
206
227
|
# file or directory that needs to be downloaded.
|
|
207
|
-
Entry = Struct.new(:remote, :local, :directory, :size, :handle, :offset, :sink
|
|
228
|
+
Entry = Struct.new(:remote, :local, :directory, :size, :handle, :offset, :sink,
|
|
229
|
+
:next_read, :pending, :window, :chunks, :eof, :attributes)
|
|
208
230
|
|
|
209
231
|
#--
|
|
210
232
|
# "ruby -w" hates private attributes, so we have to do these longhand
|
|
@@ -222,13 +244,29 @@ module Net; module SFTP; module Operations
|
|
|
222
244
|
|
|
223
245
|
# The number of bytes to read at a time from remote files.
|
|
224
246
|
def read_size
|
|
225
|
-
|
|
247
|
+
@read_size ||= positive_option(:read_size, DEFAULT_READ_SIZE)
|
|
226
248
|
end
|
|
227
249
|
|
|
228
|
-
# The number of
|
|
229
|
-
# Defaults to 16 for recursive downloads.
|
|
250
|
+
# The number of simultaneous SFTP requests to use to effect the download.
|
|
251
|
+
# Defaults to 16 for recursive downloads and 64 for single files.
|
|
230
252
|
def requests
|
|
231
|
-
|
|
253
|
+
@requests ||= positive_option(:requests, recursive? ? 16 : 64)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# The most reads a single file may have outstanding at once.
|
|
257
|
+
def read_window_limit
|
|
258
|
+
recursive? ? 1 : requests
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def preserve?
|
|
262
|
+
options[:preserve]
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def positive_option(name, default)
|
|
266
|
+
value = (options[name] || default).to_i
|
|
267
|
+
raise ArgumentError, ":#{name} must be positive" unless value > 0
|
|
268
|
+
|
|
269
|
+
value
|
|
232
270
|
end
|
|
233
271
|
|
|
234
272
|
# Enqueues as many files and directories from the stack as possible
|
|
@@ -240,7 +278,13 @@ module Net; module SFTP; module Operations
|
|
|
240
278
|
|
|
241
279
|
if entry.directory
|
|
242
280
|
update_progress(:mkdir, entry.local)
|
|
243
|
-
|
|
281
|
+
refuse_symlink!(entry.local) if server_named?(entry) && ::File.symlink?(entry.local)
|
|
282
|
+
created = !::File.directory?(entry.local)
|
|
283
|
+
::Dir.mkdir(entry.local, local_permissions(entry, 0777) | 0300) if created
|
|
284
|
+
if created || preserve?
|
|
285
|
+
@directories << entry
|
|
286
|
+
stat_directory(entry) unless entry.attributes
|
|
287
|
+
end
|
|
244
288
|
request = sftp.opendir(entry.remote, &method(:on_opendir))
|
|
245
289
|
request[:entry] = entry
|
|
246
290
|
else
|
|
@@ -248,7 +292,64 @@ module Net; module SFTP; module Operations
|
|
|
248
292
|
end
|
|
249
293
|
end
|
|
250
294
|
|
|
251
|
-
|
|
295
|
+
if !active?
|
|
296
|
+
set_directory_attributes
|
|
297
|
+
update_progress(:finish)
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Only entries listed by readdir come with attributes.
|
|
302
|
+
def stat_directory(entry)
|
|
303
|
+
@active += 1
|
|
304
|
+
sftp.stat(entry.remote) do |response|
|
|
305
|
+
@active -= 1
|
|
306
|
+
entry.attributes = remote_attributes(response, "stat #{entry.remote}")
|
|
307
|
+
process_next_entry
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Without the remote attributes the download goes ahead with default
|
|
312
|
+
# permissions and no times, as it would for a server that never sent any.
|
|
313
|
+
def remote_attributes(response, operation)
|
|
314
|
+
return response[:attrs] if response.ok?
|
|
315
|
+
|
|
316
|
+
debug { "#{operation} failed, using default attributes: #{response}" }
|
|
317
|
+
nil
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Deepest directories first, after their contents; setting attributes on
|
|
321
|
+
# a directory doesn't change its parent's modification time.
|
|
322
|
+
def set_directory_attributes
|
|
323
|
+
@directories.reverse_each do |entry|
|
|
324
|
+
next unless entry.attributes
|
|
325
|
+
|
|
326
|
+
permissions = local_permissions(entry, 0777)
|
|
327
|
+
permissions &= ~::File.umask unless preserve?
|
|
328
|
+
set_local_attributes(entry, permissions)
|
|
329
|
+
end
|
|
330
|
+
@directories.clear
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# setuid, setgid and sticky bits are never applied, matching OpenSSH.
|
|
334
|
+
def local_permissions(entry, default)
|
|
335
|
+
permissions = entry.attributes && entry.attributes.permissions
|
|
336
|
+
permissions ? permissions & 0777 : default
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# Like OpenSSH's sftp client, failing to set attributes only warns; the
|
|
340
|
+
# data has already been transferred.
|
|
341
|
+
def set_local_attributes(entry, permissions)
|
|
342
|
+
attempt_local("chmod", entry) { ::File.chmod(permissions, entry.local) } if permissions
|
|
343
|
+
|
|
344
|
+
attributes = entry.attributes
|
|
345
|
+
return unless preserve? && attributes && attributes.atime && attributes.mtime
|
|
346
|
+
attempt_local("utime", entry) { ::File.utime(attributes.atime, attributes.mtime, entry.local) }
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def attempt_local(operation, entry)
|
|
350
|
+
yield
|
|
351
|
+
rescue SystemCallError => e
|
|
352
|
+
lwarn { "#{operation} #{entry.local} failed: #{e.message}" }
|
|
252
353
|
end
|
|
253
354
|
|
|
254
355
|
# Called when a remote directory is "opened" for reading, e.g. to
|
|
@@ -275,7 +376,10 @@ module Net; module SFTP; module Operations
|
|
|
275
376
|
else
|
|
276
377
|
response[:names].each do |item|
|
|
277
378
|
next if item.name == "." || item.name == ".."
|
|
278
|
-
|
|
379
|
+
validate_entry_name!(entry, item.name)
|
|
380
|
+
child = Entry.new(::File.join(entry.remote, item.name), ::File.join(entry.local, item.name), item.directory?, item.attributes.size)
|
|
381
|
+
child.attributes = item.attributes
|
|
382
|
+
stack << child
|
|
279
383
|
end
|
|
280
384
|
|
|
281
385
|
# take this opportunity to enqueue more requests
|
|
@@ -286,6 +390,38 @@ module Net; module SFTP; module Operations
|
|
|
286
390
|
end
|
|
287
391
|
end
|
|
288
392
|
|
|
393
|
+
# The name comes from the server, so anything but a single path component
|
|
394
|
+
# would let a malicious server write outside the target directory.
|
|
395
|
+
def validate_entry_name!(entry, name)
|
|
396
|
+
if name.empty? || name.include?("/") || name.include?("\0") ||
|
|
397
|
+
(::File::ALT_SEPARATOR && name.include?(::File::ALT_SEPARATOR))
|
|
398
|
+
raise Net::SFTP::Exception, "readdir #{entry.remote} returned an invalid file name: #{name.inspect}"
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
# The caller's own +local+ may be a symlink on purpose; anything below it
|
|
403
|
+
# was named by the server (net-sftp#162).
|
|
404
|
+
def server_named?(entry)
|
|
405
|
+
entry.local != local
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# Like OpenSSH, the file is always created writable by its owner.
|
|
409
|
+
def open_local_file(entry)
|
|
410
|
+
permissions = local_permissions(entry, 0666) | 0200
|
|
411
|
+
return ::File.open(entry.local, "wb", permissions) unless server_named?(entry) && defined?(::File::NOFOLLOW)
|
|
412
|
+
|
|
413
|
+
begin
|
|
414
|
+
::File.open(entry.local, "wb", permissions, :flags => ::File::NOFOLLOW)
|
|
415
|
+
rescue SystemCallError
|
|
416
|
+
raise unless ::File.symlink?(entry.local)
|
|
417
|
+
refuse_symlink!(entry.local)
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def refuse_symlink!(path)
|
|
422
|
+
raise Net::SFTP::Exception, "#{path} is a symlink, refusing to follow it"
|
|
423
|
+
end
|
|
424
|
+
|
|
289
425
|
# Called when a file is to be opened for reading from the remote server.
|
|
290
426
|
def open_file(entry)
|
|
291
427
|
update_progress(:open, entry)
|
|
@@ -301,45 +437,116 @@ module Net; module SFTP; module Operations
|
|
|
301
437
|
process_next_entry
|
|
302
438
|
end
|
|
303
439
|
|
|
304
|
-
# Called when a file has been opened. This will call #
|
|
440
|
+
# Called when a file has been opened. This will call #request_reads
|
|
305
441
|
# to initiate the data transfer.
|
|
306
442
|
def on_open(response)
|
|
307
443
|
entry = response.request[:entry]
|
|
308
444
|
raise StatusException.new(response, "open #{entry.remote}") unless response.ok?
|
|
309
445
|
|
|
310
446
|
entry.handle = response[:handle]
|
|
311
|
-
entry.
|
|
447
|
+
if entry.attributes || entry.local.respond_to?(:write)
|
|
448
|
+
start_download(entry)
|
|
449
|
+
else
|
|
450
|
+
request = sftp.fstat(entry.handle, &method(:on_fstat))
|
|
451
|
+
request[:entry] = entry
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
# Called when the attributes of a file opened by #open_file arrive.
|
|
456
|
+
def on_fstat(response)
|
|
457
|
+
entry = response.request[:entry]
|
|
458
|
+
entry.attributes = remote_attributes(response, "fstat #{entry.remote}")
|
|
459
|
+
start_download(entry)
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
def start_download(entry)
|
|
463
|
+
entry.sink = entry.local.respond_to?(:write) ? entry.local : open_local_file(entry)
|
|
312
464
|
entry.offset = 0
|
|
465
|
+
entry.next_read = 0
|
|
466
|
+
entry.pending = 0
|
|
467
|
+
entry.window = 1
|
|
468
|
+
entry.chunks = {}
|
|
469
|
+
|
|
470
|
+
request_reads(entry)
|
|
471
|
+
end
|
|
313
472
|
|
|
314
|
-
|
|
473
|
+
# Keeps up to entry.window reads of #read_size bytes outstanding until
|
|
474
|
+
# the server reports EOF. The window starts at one and grows with each
|
|
475
|
+
# data reply, so small files don't cost a burst of reads past their end.
|
|
476
|
+
def request_reads(entry)
|
|
477
|
+
while !entry.eof && entry.pending < entry.window
|
|
478
|
+
read_range(entry, entry.next_read, read_size)
|
|
479
|
+
entry.next_read += read_size
|
|
480
|
+
end
|
|
315
481
|
end
|
|
316
482
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
request = sftp.read(entry.handle,
|
|
483
|
+
def read_range(entry, offset, length)
|
|
484
|
+
entry.pending += 1
|
|
485
|
+
request = sftp.read(entry.handle, offset, length, &method(:on_read))
|
|
320
486
|
request[:entry] = entry
|
|
321
|
-
request[:offset] =
|
|
487
|
+
request[:offset] = offset
|
|
488
|
+
request[:length] = length
|
|
322
489
|
end
|
|
323
490
|
|
|
324
|
-
# Called when a read from a file finishes.
|
|
325
|
-
#
|
|
326
|
-
#
|
|
491
|
+
# Called when a read from a file finishes. Replies can arrive out of
|
|
492
|
+
# order, so data is buffered until everything before it has been
|
|
493
|
+
# written. Once the server has reported EOF and no reads are left
|
|
494
|
+
# outstanding, the file is closed.
|
|
327
495
|
def on_read(response)
|
|
328
496
|
entry = response.request[:entry]
|
|
497
|
+
entry.pending -= 1
|
|
329
498
|
|
|
330
499
|
if response.eof?
|
|
331
|
-
|
|
332
|
-
entry.sink.close
|
|
333
|
-
request = sftp.close(entry.handle, &method(:on_close))
|
|
334
|
-
request[:entry] = entry
|
|
500
|
+
entry.eof = true
|
|
335
501
|
elsif !response.ok?
|
|
336
502
|
raise StatusException.new(response, "read #{entry.remote}")
|
|
337
503
|
else
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
504
|
+
offset = response.request[:offset]
|
|
505
|
+
length = response.request[:length]
|
|
506
|
+
data = response[:data]
|
|
507
|
+
|
|
508
|
+
if data.empty? || data.bytesize > length
|
|
509
|
+
raise Net::SFTP::Exception, "read #{entry.remote} returned #{data.bytesize} bytes for a #{length} byte request"
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
# Servers may return less than asked for without being at EOF, and
|
|
513
|
+
# the reads after this one have already been sent.
|
|
514
|
+
read_range(entry, offset + data.bytesize, length - data.bytesize) if data.bytesize < length
|
|
515
|
+
|
|
516
|
+
entry.chunks[offset] = data
|
|
517
|
+
write_buffered_chunks(entry)
|
|
518
|
+
entry.window += 1 if entry.window < read_window_limit
|
|
342
519
|
end
|
|
520
|
+
|
|
521
|
+
request_reads(entry)
|
|
522
|
+
close_file(entry) if entry.pending.zero?
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
def write_buffered_chunks(entry)
|
|
526
|
+
while (data = entry.chunks.delete(entry.offset))
|
|
527
|
+
update_progress(:get, entry, entry.offset, data)
|
|
528
|
+
entry.sink.write(data)
|
|
529
|
+
entry.offset += data.bytesize
|
|
530
|
+
end
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
def close_file(entry)
|
|
534
|
+
if entry.chunks.any?
|
|
535
|
+
raise Net::SFTP::Exception, "read #{entry.remote} hit EOF at #{entry.offset} but the server returned data beyond it"
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
update_progress(:close, entry)
|
|
539
|
+
# The caller's IO stays open, but is flushed so its data is written when download! returns.
|
|
540
|
+
if entry.sink.equal?(local)
|
|
541
|
+
entry.sink.flush if entry.sink.respond_to?(:flush)
|
|
542
|
+
else
|
|
543
|
+
entry.sink.close
|
|
544
|
+
end
|
|
545
|
+
unless entry.local.respond_to?(:write)
|
|
546
|
+
set_local_attributes(entry, preserve? ? local_permissions(entry, nil) : nil)
|
|
547
|
+
end
|
|
548
|
+
request = sftp.close(entry.handle, &method(:on_close))
|
|
549
|
+
request[:entry] = entry
|
|
343
550
|
end
|
|
344
551
|
|
|
345
552
|
# Called when a file handle is closed.
|
|
@@ -12,6 +12,11 @@ module Net; module SFTP; module Operations
|
|
|
12
12
|
# puts file.gets
|
|
13
13
|
# file.close
|
|
14
14
|
class File
|
|
15
|
+
# The largest amount of data sent in a single write request. Servers drop
|
|
16
|
+
# the connection on oversized packets (OpenSSH's limit is 256 KiB including
|
|
17
|
+
# headers), and the SFTP drafts only guarantee packets of about 34000 bytes.
|
|
18
|
+
MAX_WRITE_SIZE = 32 * 1024
|
|
19
|
+
|
|
15
20
|
# A reference to the Net::SFTP::Session instance that drives this wrapper
|
|
16
21
|
attr_reader :sftp
|
|
17
22
|
|
|
@@ -30,14 +35,14 @@ module Net; module SFTP; module Operations
|
|
|
30
35
|
@pos = 0
|
|
31
36
|
@real_pos = 0
|
|
32
37
|
@real_eof = false
|
|
33
|
-
@buffer = ""
|
|
38
|
+
@buffer = +""
|
|
34
39
|
end
|
|
35
40
|
|
|
36
41
|
# Repositions the file pointer to the given offset (relative to the
|
|
37
42
|
# start of the file). This will also reset the EOF flag.
|
|
38
43
|
def pos=(offset)
|
|
39
44
|
@real_pos = @pos = offset
|
|
40
|
-
@buffer = ""
|
|
45
|
+
@buffer = +""
|
|
41
46
|
@real_eof = false
|
|
42
47
|
end
|
|
43
48
|
|
|
@@ -63,15 +68,17 @@ module Net; module SFTP; module Operations
|
|
|
63
68
|
#
|
|
64
69
|
# This will advance the file pointer (#pos).
|
|
65
70
|
def read(n=nil)
|
|
71
|
+
raise ArgumentError, "negative length #{n} given" if n && n < 0
|
|
72
|
+
|
|
66
73
|
loop do
|
|
67
74
|
break if n && @buffer.length >= n
|
|
68
75
|
break unless fill
|
|
69
76
|
end
|
|
70
77
|
|
|
71
78
|
if n
|
|
72
|
-
result, @buffer = @buffer[0,n], (@buffer[n..-1] || "")
|
|
79
|
+
result, @buffer = @buffer[0,n], (@buffer[n..-1] || +"")
|
|
73
80
|
else
|
|
74
|
-
result, @buffer = @buffer, ""
|
|
81
|
+
result, @buffer = @buffer, +""
|
|
75
82
|
end
|
|
76
83
|
|
|
77
84
|
@pos += result.length
|
|
@@ -92,6 +99,7 @@ module Net; module SFTP; module Operations
|
|
|
92
99
|
sep_string = sep_or_limit
|
|
93
100
|
lim = limit
|
|
94
101
|
end
|
|
102
|
+
lim = Float::INFINITY if lim.nil? || lim < 0
|
|
95
103
|
|
|
96
104
|
delim = if sep_string && sep_string.length == 0
|
|
97
105
|
"#{$/}#{$/}"
|
|
@@ -99,22 +107,26 @@ module Net; module SFTP; module Operations
|
|
|
99
107
|
sep_string
|
|
100
108
|
end
|
|
101
109
|
|
|
110
|
+
search_from = 0
|
|
102
111
|
loop do
|
|
103
|
-
at = @buffer.index(delim) if delim
|
|
112
|
+
at = @buffer.index(delim, search_from) if delim
|
|
104
113
|
if at
|
|
105
114
|
offset = [at + delim.length, lim].min
|
|
106
115
|
@pos += offset
|
|
107
116
|
line, @buffer = @buffer[0,offset], @buffer[offset..-1]
|
|
108
117
|
return line
|
|
109
|
-
elsif lim
|
|
118
|
+
elsif lim <= @buffer.length
|
|
110
119
|
@pos += lim
|
|
111
120
|
line, @buffer = @buffer[0,lim], @buffer[lim..-1]
|
|
112
121
|
return line
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
122
|
+
else
|
|
123
|
+
search_from = [@buffer.length - delim.length + 1, 0].max if delim
|
|
124
|
+
unless fill
|
|
125
|
+
return nil if @buffer.empty?
|
|
126
|
+
@pos += @buffer.length
|
|
127
|
+
line, @buffer = @buffer, +""
|
|
128
|
+
return line
|
|
129
|
+
end
|
|
118
130
|
end
|
|
119
131
|
end
|
|
120
132
|
end
|
|
@@ -128,13 +140,21 @@ module Net; module SFTP; module Operations
|
|
|
128
140
|
end
|
|
129
141
|
|
|
130
142
|
# Writes the given data to the stream, incrementing the file position and
|
|
131
|
-
# returning the number of bytes written.
|
|
143
|
+
# returning the number of bytes written. Data larger than MAX_WRITE_SIZE
|
|
144
|
+
# is sent as several sequential write requests; if one fails, the
|
|
145
|
+
# position reflects the bytes written before it.
|
|
132
146
|
def write(data)
|
|
133
147
|
data = data.to_s
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
148
|
+
size = data.bytesize
|
|
149
|
+
written = 0
|
|
150
|
+
loop do
|
|
151
|
+
chunk = data.byteslice(written, MAX_WRITE_SIZE)
|
|
152
|
+
sftp.write!(handle, @pos, chunk)
|
|
153
|
+
written += chunk.bytesize
|
|
154
|
+
self.pos = @pos + chunk.bytesize unless chunk.empty?
|
|
155
|
+
break if written >= size
|
|
156
|
+
end
|
|
157
|
+
size
|
|
138
158
|
end
|
|
139
159
|
|
|
140
160
|
# Writes each argument to the stream. If +$\+ is set, it will be written
|
|
@@ -155,16 +175,12 @@ module Net; module SFTP; module Operations
|
|
|
155
175
|
end
|
|
156
176
|
|
|
157
177
|
# Writes each argument to the stream, appending a newline to any item
|
|
158
|
-
# that does not already end in a newline. Array arguments
|
|
178
|
+
# that does not already end in a newline, like IO#puts. Array arguments
|
|
179
|
+
# are flattened, and with no arguments a single newline is written.
|
|
159
180
|
def puts(*items)
|
|
160
|
-
items
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
else
|
|
164
|
-
write(item)
|
|
165
|
-
write("\n") unless item[-1] == ?\n
|
|
166
|
-
end
|
|
167
|
-
end
|
|
181
|
+
items = [""] if items.empty?
|
|
182
|
+
recursion = {}.compare_by_identity
|
|
183
|
+
items.each { |item| write_puts_item(item, recursion) }
|
|
168
184
|
nil
|
|
169
185
|
end
|
|
170
186
|
|
|
@@ -178,12 +194,30 @@ module Net; module SFTP; module Operations
|
|
|
178
194
|
|
|
179
195
|
private
|
|
180
196
|
|
|
197
|
+
def write_puts_item(item, recursion)
|
|
198
|
+
array = Array.try_convert(item)
|
|
199
|
+
if array
|
|
200
|
+
if recursion.key?(item)
|
|
201
|
+
item = "[...]"
|
|
202
|
+
else
|
|
203
|
+
recursion[item] = true
|
|
204
|
+
array.each { |child| write_puts_item(child, recursion) }
|
|
205
|
+
recursion.delete(item)
|
|
206
|
+
return
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
item = item.to_s
|
|
211
|
+
write(item) unless item.empty?
|
|
212
|
+
write("\n") unless item[-1] == ?\n
|
|
213
|
+
end
|
|
214
|
+
|
|
181
215
|
# Fills the buffer. Returns +true+ if it succeeded, and +false+ if
|
|
182
216
|
# EOF was encountered before any data was read.
|
|
183
217
|
def fill
|
|
184
218
|
data = sftp.read!(handle, @real_pos, 8192)
|
|
185
219
|
|
|
186
|
-
if data.nil?
|
|
220
|
+
if data.nil? || data.empty?
|
|
187
221
|
@real_eof = true
|
|
188
222
|
return false
|
|
189
223
|
else
|