net-sftp 4.0.0 → 4.0.2
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 +24 -23
- data/.gitignore +1 -0
- data/CHANGES.txt +34 -0
- data/CONTRIBUTING.md +33 -0
- data/Gemfile +7 -2
- data/Rakefile +100 -1
- data/changelog.d/README.md +1 -0
- data/lib/net/sftp/operations/dir.rb +5 -4
- data/lib/net/sftp/operations/download.rb +48 -3
- data/lib/net/sftp/operations/file.rb +60 -26
- data/lib/net/sftp/operations/upload.rb +27 -4
- data/lib/net/sftp/protocol/01/attributes.rb +1 -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 +10 -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 +8 -2
- data/lib/net/sftp/session.rb +5 -0
- 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 +3 -3
- data.tar.gz.sig +0 -0
- metadata +31 -26
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6e7dc5adb1f352eecedf9c4d5f6150b32e0e26ccdd76cb6cb242c66fbe636fd4
|
|
4
|
+
data.tar.gz: e9b3ade5165aa0c1dbb61e64acd303794b16a31aa385472c3c513d983322fb3a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8534ab3b4109bd1e5333c1e4814e5e01a855c2f84c40f60d58bfb2d6303bdc1fab20bf8102b31ef2a1b8c408e6565a7a0a26ff1dbf1b1c48445dd2f9aa49bee8
|
|
7
|
+
data.tar.gz: b36ad98cdca42bdf48a29957105f4a104c2965280f983f937bfd64e8958b86a2354dc612e52404bba88374944bdda86016f4e06449e8fb541e26f74a0e0eeacc
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<!-- After opening the PR, add a changelog.d/<PR number>.<type>.txt fragment (none, change, security, breaking or breaking-security) instead of editing CHANGES.txt; see CONTRIBUTING.md#changelog. -->
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
name: Changelog
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
types: [opened, synchronize, reopened, labeled, unlabeled]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
changelog:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
with:
|
|
16
|
+
# HEAD is the PR merged into the base branch; HEAD^1 is the base it was merged into.
|
|
17
|
+
fetch-depth: 2
|
|
18
|
+
- uses: net-ssh/changelogd@v1
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -1,35 +1,36 @@
|
|
|
1
1
|
name: CI
|
|
2
|
-
on:
|
|
2
|
+
on:
|
|
3
|
+
pull_request:
|
|
4
|
+
push: { branches: master }
|
|
5
|
+
permissions:
|
|
6
|
+
contents: read
|
|
3
7
|
jobs:
|
|
4
8
|
test:
|
|
5
|
-
runs-on: ubuntu-
|
|
6
|
-
continue-on-error: ${{ matrix.flaky }}
|
|
9
|
+
runs-on: ubuntu-latest
|
|
7
10
|
strategy:
|
|
11
|
+
fail-fast: false
|
|
8
12
|
matrix:
|
|
9
|
-
ruby-version: [
|
|
10
|
-
|
|
11
|
-
include:
|
|
12
|
-
- ruby-version: "ruby-head"
|
|
13
|
-
flaky: true
|
|
14
|
-
- ruby-version: "jruby-9.2"
|
|
15
|
-
flaky: true
|
|
16
|
-
- ruby-version: "jruby-9.3"
|
|
17
|
-
flaky: true
|
|
18
|
-
- ruby-version: "jruby-head"
|
|
19
|
-
flaky: true
|
|
20
|
-
- ruby-version: "truffleruby-head"
|
|
21
|
-
flaky: true
|
|
13
|
+
ruby-version: ['2.6', '2.7', '3.0', '3.1', '3.2', '3.3', '3.4', '4.0', 'ruby-head', 'jruby', 'truffleruby']
|
|
14
|
+
continue-on-error: ${{ contains(fromJSON('["ruby-head", "jruby", "truffleruby"]'), matrix.ruby-version) }}
|
|
22
15
|
steps:
|
|
23
|
-
- uses: actions/checkout@
|
|
24
|
-
|
|
16
|
+
- uses: actions/checkout@v4
|
|
25
17
|
- name: Set up Ruby ${{ matrix.ruby-version }}
|
|
26
18
|
uses: ruby/setup-ruby@v1
|
|
27
19
|
with:
|
|
28
20
|
ruby-version: ${{ matrix.ruby-version }}
|
|
29
|
-
|
|
30
|
-
- name: Bundle install
|
|
31
|
-
run: |
|
|
32
|
-
gem install bundler
|
|
33
|
-
bundle install
|
|
21
|
+
bundler-cache: true
|
|
34
22
|
- name: Run Tests
|
|
35
23
|
run: bundle exec rake test
|
|
24
|
+
frozen-string-literal:
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@v4
|
|
28
|
+
- name: Set up Ruby
|
|
29
|
+
uses: ruby/setup-ruby@v1
|
|
30
|
+
with:
|
|
31
|
+
ruby-version: '4.0'
|
|
32
|
+
bundler-cache: true
|
|
33
|
+
- name: Run Tests with frozen string literals
|
|
34
|
+
run: bundle exec rake test
|
|
35
|
+
env:
|
|
36
|
+
RUBYOPT: --enable-frozen-string-literal
|
data/.gitignore
CHANGED
data/CHANGES.txt
CHANGED
|
@@ -1,3 +1,37 @@
|
|
|
1
|
+
=== 4.0.2
|
|
2
|
+
|
|
3
|
+
* 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]
|
|
4
|
+
* Security: Recursive downloads reject file names from the server that are not a single path component (such as `../x` or `a/b`) instead of writing outside the target directory. Reported in #162 by srosenhamer [#185, #162]
|
|
5
|
+
* Net::SFTP now works with frozen string literals enabled: `File` reads and `Dir#glob` no longer mutate frozen strings, and `glob` no longer modifies entry names in place (adds `Name#name=`). Based on #157 by @frederikspang [#198, #157]
|
|
6
|
+
* `File#read` and `File#gets` no longer loop forever when the server returns an empty data packet, and `read` with a negative length raises `ArgumentError` like `IO#read`. Based on #174 by @OskarEichler [#189, #174]
|
|
7
|
+
* Show fields an SFTP v4+ server omitted as `?` in directory listings (`longname`), like `ls -l` does, instead of blank, `-` or `---------` [#188]
|
|
8
|
+
* Fixed `File#write` hanging on data of 256 KiB or more by sending it in 32 KiB write requests [#99, #187]
|
|
9
|
+
* SFTP request ids now wrap around at the uint32 limit, skipping ids still awaiting a response, instead of growing past it so that responses on very long-lived sessions could no longer be matched to their requests. Thanks @OskarEichler [#179]
|
|
10
|
+
* Uploads keep exactly `:requests` writes in flight (one more was sent before, so the default is now 2 rather than 3), and a zero, negative or non-numeric `:requests` or `:read_size` on `upload` or `download` now raises `ArgumentError` when the transfer is set up instead of stalling. Thanks @OskarEichler [#178]
|
|
11
|
+
* Reconnecting a closed `Net::SFTP::Session` now discards partially received packet data from the old channel, so stale bytes are no longer parsed as the start of the new channel's first packet. Thanks @OskarEichler [#177]
|
|
12
|
+
* Don't crash formatting a directory listing (`longname`) when an SFTP v4+ server omits the modification time, size or permissions, and don't show omitted permissions as `rwxrwxrwx`. Thanks @OskarEichler [#175, #146]
|
|
13
|
+
* `Dir#glob` no longer lists every subdirectory for a pattern without `/` (which can only match top-level names), saving a round trip per directory and no longer failing on unreadable subdirectories. Thanks @OskarEichler [#173, #180]
|
|
14
|
+
* `File#gets` no longer rescans the whole line after each chunk, so reading very long lines is no longer quadratic (a 16 MiB line ending in `"\r\n"` went from about 5 s to 0.01 s). Thanks @OskarEichler [#172]
|
|
15
|
+
* `File#gets` treats a nil or negative limit as no limit, like `IO#gets`, instead of returning nil and corrupting the position (or raising for nil). Thanks @OskarEichler [#171]
|
|
16
|
+
* Fixed SFTP v5/v6 `open` ignoring `IO::TRUNC` and `IO::CREAT` in numeric flags: `IO::WRONLY | IO::TRUNC` now requests `TRUNCATE_EXISTING` instead of opening without truncation, and read-only opens with `IO::CREAT` now request `OPEN_OR_CREATE`. Thanks @OskarEichler [#170]
|
|
17
|
+
* Include the server's error message in `StatusException` and `Response#to_s` for SFTP protocol v3 and later, instead of only the status code. Thanks @OskarEichler [#169, #156]
|
|
18
|
+
* Fixed SFTP v4+ attribute serialization raising `TypeError` when nanoseconds were given for only some timestamps (e.g. `atime_nseconds` with `mtime` but no `mtime_nseconds`); the missing subsecond fields are now sent as 0. Thanks @OskarEichler [#168]
|
|
19
|
+
* `Net::SFTP.start` with a block now closes the SSH session when the block exits with `break`, `return` or `throw`, instead of leaking it. Thanks @OskarEichler [#167]
|
|
20
|
+
* Attributes objects now copy the hash they are given, so a frozen hash no longer raises `FrozenError` and the caller's hash is no longer modified by the default `:type` or by cached owner/group/uid/gid lookups. Thanks @OskarEichler [#166]
|
|
21
|
+
* `File#gets(limit)` no longer makes an extra remote read when the buffered data already satisfies the limit, and `gets(0)` returns an empty string like `IO#gets`. Thanks @OskarEichler [#165]
|
|
22
|
+
* `File#puts` now behaves like `IO#puts`: with no arguments it writes a newline (it used to write nothing), `nil` and `false` are written instead of raising, objects with `to_ary` are expanded, and a recursive array no longer overflows the stack. Thanks @OskarEichler [#164]
|
|
23
|
+
* Fixed `Net::SFTP::Operations::File#write` writing at the read-ahead offset instead of the current position after a buffered read (e.g. `read(2)` on an `r+` file), which corrupted data. Thanks @OskarEichler [#163]
|
|
24
|
+
* Accept a `Pathname` as the local or remote path for uploads and downloads. Thanks @Tietew and @til [#153, #102]
|
|
25
|
+
|
|
26
|
+
=== 4.0.1
|
|
27
|
+
|
|
28
|
+
* Allow net-ssh 8 [#181]
|
|
29
|
+
|
|
30
|
+
=== 4.0.0
|
|
31
|
+
|
|
32
|
+
* Allow net-ssh 7
|
|
33
|
+
* Fix multibyte string write [#110]
|
|
34
|
+
|
|
1
35
|
=== 3.0.0
|
|
2
36
|
|
|
3
37
|
* Pass protocol version via Net::SFTP.start [#107]
|
data/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Contributing to net-sftp
|
|
2
|
+
|
|
3
|
+
Run the tests with `bundle exec rake test`.
|
|
4
|
+
|
|
5
|
+
## Changelog
|
|
6
|
+
|
|
7
|
+
Don't edit `CHANGES.txt`. Instead, add a fragment file to `changelog.d/` in your PR; the release's version bump (`rake vbump:*`) moves all fragments into a new `=== <version>` section at the top of `CHANGES.txt` and deletes them.
|
|
8
|
+
|
|
9
|
+
A fragment is named `<number>.<type>.txt`, where `<number>` is the PR number (so open the PR first, then add the fragment), and contains a single UTF-8 paragraph: the entry as it should read in `CHANGES.txt`, without the bullet or prefix. Lines are joined, so it can be wrapped. If it doesn't end with a reference list such as `[#106, #107]` or `[GHSA-xxxx-xxxx-xxxx]`, ` [#<number>]` is appended.
|
|
10
|
+
|
|
11
|
+
| Type | Written as | Use for |
|
|
12
|
+
| ------------------- | ------------------------------ | ------------------------------------------------------------ |
|
|
13
|
+
| `breaking-security` | `* BREAKING: Security: <text>` | an incompatible change made for security |
|
|
14
|
+
| `breaking` | `* BREAKING: <text>` | an incompatible change |
|
|
15
|
+
| `security` | `* Security: <text>` | a security fix or hardening |
|
|
16
|
+
| `change` | `* <text>` | any other user-visible fix or feature |
|
|
17
|
+
| `none` | nothing | PRs with no user-visible change (tests, CI, docs, refactors) |
|
|
18
|
+
|
|
19
|
+
Examples:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
rake "changelog:new[184,change,Fixed downloading files with multibyte names]"
|
|
23
|
+
# changelog.d/184.change.txt -> * Fixed downloading files with multibyte names [#184]
|
|
24
|
+
|
|
25
|
+
echo 'Refuse symlinks that point outside the download directory [GHSA-xxxx-xxxx-xxxx]' > changelog.d/185.security.txt
|
|
26
|
+
touch changelog.d/186.none.txt
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
For several entries from one PR add a suffix: `184.change.txt`, `184.change.2.txt` (`changelog:new` picks the next free one).
|
|
30
|
+
|
|
31
|
+
`rake changelog:check` validates the fragments and `rake changelog:preview` prints the section the next release would add. CI fails a PR that edits `CHANGES.txt` or doesn't add or modify a `changelog.d/<PR number>.<type>.txt` fragment, unless it only touches `changelog.d/` or a maintainer adds the `skip-changelog` label; Dependabot PRs are skipped.
|
|
32
|
+
|
|
33
|
+
The full rules, including how a release orders the entries, are in the [changelogd README](https://github.com/net-ssh/changelogd#readme).
|
data/Gemfile
CHANGED
|
@@ -4,8 +4,13 @@ source 'https://rubygems.org'
|
|
|
4
4
|
gemspec
|
|
5
5
|
|
|
6
6
|
# TODO: add to gemspec
|
|
7
|
-
gem "bundler", "
|
|
8
|
-
gem "rake", "
|
|
7
|
+
gem "bundler", ">= 1.11"
|
|
8
|
+
gem "rake", ">= 12.0"
|
|
9
|
+
gem "rdoc"
|
|
10
|
+
gem "logger" # no longer a default gem in Ruby 4.0+
|
|
11
|
+
|
|
12
|
+
# git: rather than github:, which Bundler 1.x (the default on Ruby 2.6) expands to an unsupported git:// URL.
|
|
13
|
+
gem "changelogd", git: "https://github.com/net-ssh/changelogd.git", tag: "v1", group: :development
|
|
9
14
|
|
|
10
15
|
gem 'byebug', group: %i[development test] if !Gem.win_platform? && RUBY_ENGINE == "ruby"
|
|
11
16
|
|
data/Rakefile
CHANGED
|
@@ -19,6 +19,16 @@ name = "net-sftp"
|
|
|
19
19
|
require_relative "lib/net/sftp/version"
|
|
20
20
|
version = Net::SFTP::Version::CURRENT
|
|
21
21
|
|
|
22
|
+
begin
|
|
23
|
+
require "changelogd/rake"
|
|
24
|
+
rescue LoadError
|
|
25
|
+
# A git gem isn't on the load path under plain `rake`, as the release workflow runs it; bundler/setup could clash with the running rake.
|
|
26
|
+
require "bundler"
|
|
27
|
+
$LOAD_PATH.unshift(*Bundler.definition.specs["changelogd"].flat_map(&:full_require_paths))
|
|
28
|
+
require "changelogd/rake"
|
|
29
|
+
end
|
|
30
|
+
Changelogd::RakeTasks.new(version_file: "lib/net/sftp/version.rb")
|
|
31
|
+
|
|
22
32
|
namespace :cert do
|
|
23
33
|
desc "Update public cert from private - only run if public is expired"
|
|
24
34
|
task :update_public_when_expired do
|
|
@@ -27,7 +37,7 @@ namespace :cert do
|
|
|
27
37
|
raw = File.read "net-sftp-public_cert.pem"
|
|
28
38
|
certificate = OpenSSL::X509::Certificate.new raw
|
|
29
39
|
raise Exception, "Not yet expired: #{certificate.not_after}" unless certificate.not_after < Time.now
|
|
30
|
-
sh "gem cert --build netssh@solutious.com --days
|
|
40
|
+
sh "gem cert --build netssh@solutious.com --days 1825 --private-key /mnt/gem/net-ssh-private_key.pem"
|
|
31
41
|
sh "mv gem-public_cert.pem net-sftp-public_cert.pem"
|
|
32
42
|
sh "gem cert --add net-sftp-public_cert.pem"
|
|
33
43
|
end
|
|
@@ -51,3 +61,92 @@ RDoc::Task.new do |rdoc|
|
|
|
51
61
|
rdoc.rdoc_files.include(file) if File.exist?(file)
|
|
52
62
|
}
|
|
53
63
|
end
|
|
64
|
+
|
|
65
|
+
def change_version(&block)
|
|
66
|
+
Rake::Task["changelog:check"].invoke
|
|
67
|
+
version_file = 'lib/net/sftp/version.rb'
|
|
68
|
+
require_relative version_file
|
|
69
|
+
pre = Net::SFTP::Version::PRE
|
|
70
|
+
tiny = Net::SFTP::Version::TINY
|
|
71
|
+
result = block[pre: pre, tiny: Net::SFTP::Version::TINY]
|
|
72
|
+
raise ArgumentError, "Version change logic should always return a pre" unless result.key?(:pre)
|
|
73
|
+
|
|
74
|
+
new_pre = result[:pre]
|
|
75
|
+
new_tiny = result[:tiny] || tiny
|
|
76
|
+
found = { pre: false, tiny: false }
|
|
77
|
+
File.open("#{version_file}.new", "w") do |f|
|
|
78
|
+
File.readlines(version_file).each do |line|
|
|
79
|
+
match =
|
|
80
|
+
if pre.nil?
|
|
81
|
+
/^(\s+PRE\s+=\s+)nil(\s*)$/.match(line)
|
|
82
|
+
else
|
|
83
|
+
/^(\s+PRE\s+=\s+")#{pre}("\s*)$/.match(line)
|
|
84
|
+
end
|
|
85
|
+
if match
|
|
86
|
+
prefix = match[1]
|
|
87
|
+
postfix = match[2]
|
|
88
|
+
prefix.delete_suffix!('"')
|
|
89
|
+
postfix.delete_prefix!('"')
|
|
90
|
+
new_line = "#{prefix}#{new_pre.inspect}#{postfix}"
|
|
91
|
+
puts "Changing:\n - #{line} + #{new_line}"
|
|
92
|
+
line = new_line
|
|
93
|
+
found[:pre] = true
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
if new_tiny != tiny
|
|
97
|
+
match = /^(\s+TINY\s+=\s+)#{tiny}(\s*)$/.match(line)
|
|
98
|
+
if match
|
|
99
|
+
prefix = match[1]
|
|
100
|
+
postfix = match[2]
|
|
101
|
+
new_line = "#{prefix}#{new_tiny}#{postfix}"
|
|
102
|
+
puts "Changing:\n - #{line} + #{new_line}"
|
|
103
|
+
line = new_line
|
|
104
|
+
found[:tiny] = true
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
f.write(line)
|
|
109
|
+
end
|
|
110
|
+
raise ArgumentError, "Cound not find line: PRE = \"#{pre}\" in #{version_file}" unless found[:pre]
|
|
111
|
+
raise ArgumentError, "Cound not find line: TINY = \"#{tiny}\" in #{version_file}" unless found[:tiny] || new_tiny == tiny
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
FileUtils.mv version_file, "#{version_file}.old"
|
|
115
|
+
FileUtils.mv "#{version_file}.new", version_file
|
|
116
|
+
Rake::Task["changelog:release"].invoke
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
namespace :vbump do
|
|
120
|
+
desc "Final release"
|
|
121
|
+
task :final do
|
|
122
|
+
change_version do |pre:, tiny:|
|
|
123
|
+
_ = tiny
|
|
124
|
+
if pre.nil?
|
|
125
|
+
{ tiny: tiny + 1, pre: nil }
|
|
126
|
+
else
|
|
127
|
+
raise ArgumentError, "Unexpected pre: #{pre}" if pre.nil?
|
|
128
|
+
|
|
129
|
+
{ pre: nil }
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
desc "Increment prerelease"
|
|
135
|
+
task :pre, [:type] do |_t, args|
|
|
136
|
+
change_version do |pre:, tiny:|
|
|
137
|
+
puts " PRE => #{pre.inspect}"
|
|
138
|
+
match = /^([a-z]+)(\d+)/.match(pre)
|
|
139
|
+
raise ArgumentError, "Unexpected pre: #{pre}" if match.nil? && args[:type].nil?
|
|
140
|
+
|
|
141
|
+
if match.nil? || (!args[:type].nil? && args[:type] != match[1])
|
|
142
|
+
if pre.nil?
|
|
143
|
+
{ pre: "#{args[:type]}1", tiny: tiny + 1 }
|
|
144
|
+
else
|
|
145
|
+
{ pre: "#{args[:type]}1" }
|
|
146
|
+
end
|
|
147
|
+
else
|
|
148
|
+
{ pre: "#{match[1]}#{match[2].to_i + 1}" }
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Changelog fragments: one `<PR number>.<type>.txt` file per user-visible change, assembled into `CHANGES.txt` when the version is bumped. See [CONTRIBUTING.md](../CONTRIBUTING.md#changelog).
|
|
@@ -58,17 +58,18 @@ module Net; module SFTP; module Operations
|
|
|
58
58
|
def glob(path, pattern, flags=0)
|
|
59
59
|
flags |= ::File::FNM_PATHNAME
|
|
60
60
|
path = path.chop if path.end_with?('/') && path != '/'
|
|
61
|
+
recursive = !pattern.is_a?(String) || pattern.include?('/')
|
|
61
62
|
|
|
62
63
|
results = [] unless block_given?
|
|
63
64
|
queue = entries(path).reject { |e| %w(. ..).include?(e.name) }
|
|
64
65
|
while queue.any?
|
|
65
66
|
entry = queue.shift
|
|
66
67
|
|
|
67
|
-
if entry.directory? && !%w(. ..).include?(::File.basename(entry.name))
|
|
68
|
-
queue
|
|
69
|
-
e.name
|
|
68
|
+
if recursive && entry.directory? && !%w(. ..).include?(::File.basename(entry.name))
|
|
69
|
+
queue.concat(entries("#{path}/#{entry.name}").map do |e|
|
|
70
|
+
e.name = "#{entry.name}/#{e.name}"
|
|
70
71
|
e
|
|
71
|
-
end
|
|
72
|
+
end)
|
|
72
73
|
end
|
|
73
74
|
|
|
74
75
|
if ::File.fnmatch(pattern, entry.name, flags)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require 'pathname'
|
|
1
2
|
require 'net/ssh/loggable'
|
|
2
3
|
|
|
3
4
|
module Net; module SFTP; module Operations
|
|
@@ -145,6 +146,9 @@ module Net; module SFTP; module Operations
|
|
|
145
146
|
# This will return immediately, and requires that the SSH event loop be
|
|
146
147
|
# run in order to effect the download. (See #wait.)
|
|
147
148
|
def initialize(sftp, local, remote, options={}, &progress)
|
|
149
|
+
local = local.to_path if local.is_a?(Pathname)
|
|
150
|
+
remote = remote.to_path if remote.is_a?(Pathname)
|
|
151
|
+
|
|
148
152
|
@sftp = sftp
|
|
149
153
|
@local = local
|
|
150
154
|
@remote = remote
|
|
@@ -152,6 +156,8 @@ module Net; module SFTP; module Operations
|
|
|
152
156
|
@options = options
|
|
153
157
|
@active = 0
|
|
154
158
|
@properties = options[:properties] || {}
|
|
159
|
+
requests
|
|
160
|
+
read_size
|
|
155
161
|
|
|
156
162
|
self.logger = sftp.logger
|
|
157
163
|
|
|
@@ -222,13 +228,20 @@ module Net; module SFTP; module Operations
|
|
|
222
228
|
|
|
223
229
|
# The number of bytes to read at a time from remote files.
|
|
224
230
|
def read_size
|
|
225
|
-
|
|
231
|
+
@read_size ||= positive_option(:read_size, DEFAULT_READ_SIZE)
|
|
226
232
|
end
|
|
227
233
|
|
|
228
234
|
# The number of simultaneou SFTP requests to use to effect the download.
|
|
229
235
|
# Defaults to 16 for recursive downloads.
|
|
230
236
|
def requests
|
|
231
|
-
|
|
237
|
+
@requests ||= positive_option(:requests, recursive? ? 16 : 2)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def positive_option(name, default)
|
|
241
|
+
value = (options[name] || default).to_i
|
|
242
|
+
raise ArgumentError, ":#{name} must be positive" unless value > 0
|
|
243
|
+
|
|
244
|
+
value
|
|
232
245
|
end
|
|
233
246
|
|
|
234
247
|
# Enqueues as many files and directories from the stack as possible
|
|
@@ -240,6 +253,7 @@ module Net; module SFTP; module Operations
|
|
|
240
253
|
|
|
241
254
|
if entry.directory
|
|
242
255
|
update_progress(:mkdir, entry.local)
|
|
256
|
+
refuse_symlink!(entry.local) if server_named?(entry) && ::File.symlink?(entry.local)
|
|
243
257
|
::Dir.mkdir(entry.local) unless ::File.directory?(entry.local)
|
|
244
258
|
request = sftp.opendir(entry.remote, &method(:on_opendir))
|
|
245
259
|
request[:entry] = entry
|
|
@@ -275,6 +289,7 @@ module Net; module SFTP; module Operations
|
|
|
275
289
|
else
|
|
276
290
|
response[:names].each do |item|
|
|
277
291
|
next if item.name == "." || item.name == ".."
|
|
292
|
+
validate_entry_name!(entry, item.name)
|
|
278
293
|
stack << Entry.new(::File.join(entry.remote, item.name), ::File.join(entry.local, item.name), item.directory?, item.attributes.size)
|
|
279
294
|
end
|
|
280
295
|
|
|
@@ -286,6 +301,36 @@ module Net; module SFTP; module Operations
|
|
|
286
301
|
end
|
|
287
302
|
end
|
|
288
303
|
|
|
304
|
+
# The name comes from the server, so anything but a single path component
|
|
305
|
+
# would let a malicious server write outside the target directory.
|
|
306
|
+
def validate_entry_name!(entry, name)
|
|
307
|
+
if name.empty? || name.include?("/") || name.include?("\0") ||
|
|
308
|
+
(::File::ALT_SEPARATOR && name.include?(::File::ALT_SEPARATOR))
|
|
309
|
+
raise Net::SFTP::Exception, "readdir #{entry.remote} returned an invalid file name: #{name.inspect}"
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# The caller's own +local+ may be a symlink on purpose; anything below it
|
|
314
|
+
# was named by the server (net-sftp#162).
|
|
315
|
+
def server_named?(entry)
|
|
316
|
+
entry.local != local
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def open_local_file(entry)
|
|
320
|
+
return ::File.open(entry.local, "wb") unless server_named?(entry) && defined?(::File::NOFOLLOW)
|
|
321
|
+
|
|
322
|
+
begin
|
|
323
|
+
::File.open(entry.local, "wb", :flags => ::File::NOFOLLOW)
|
|
324
|
+
rescue SystemCallError
|
|
325
|
+
raise unless ::File.symlink?(entry.local)
|
|
326
|
+
refuse_symlink!(entry.local)
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def refuse_symlink!(path)
|
|
331
|
+
raise Net::SFTP::Exception, "#{path} is a symlink, refusing to follow it"
|
|
332
|
+
end
|
|
333
|
+
|
|
289
334
|
# Called when a file is to be opened for reading from the remote server.
|
|
290
335
|
def open_file(entry)
|
|
291
336
|
update_progress(:open, entry)
|
|
@@ -308,7 +353,7 @@ module Net; module SFTP; module Operations
|
|
|
308
353
|
raise StatusException.new(response, "open #{entry.remote}") unless response.ok?
|
|
309
354
|
|
|
310
355
|
entry.handle = response[:handle]
|
|
311
|
-
entry.sink = entry.local.respond_to?(:write) ? entry.local :
|
|
356
|
+
entry.sink = entry.local.respond_to?(:write) ? entry.local : open_local_file(entry)
|
|
312
357
|
entry.offset = 0
|
|
313
358
|
|
|
314
359
|
download_next_chunk(entry)
|
|
@@ -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
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require 'pathname'
|
|
1
2
|
require 'net/ssh/loggable'
|
|
2
3
|
|
|
3
4
|
module Net; module SFTP; module Operations
|
|
@@ -50,7 +51,9 @@ module Net; module SFTP; module Operations
|
|
|
50
51
|
# higher might improve throughput. Reducing it will reduce throughput.
|
|
51
52
|
# * <tt>:read_size</tt> - the maximum number of bytes to read at a time
|
|
52
53
|
# from the source. Increasing this value might improve throughput. It
|
|
53
|
-
# defaults to 32,000 bytes.
|
|
54
|
+
# defaults to 32,000 bytes. Each chunk is sent as one write request, so
|
|
55
|
+
# it must stay within the server's packet limit (OpenSSH drops the
|
|
56
|
+
# connection on packets over 256 KiB).
|
|
54
57
|
# * <tt>:name</tt> - the filename to report to the progress monitor when
|
|
55
58
|
# an IO object is given as +local+. This defaults to "<memory>".
|
|
56
59
|
#
|
|
@@ -141,6 +144,9 @@ module Net; module SFTP; module Operations
|
|
|
141
144
|
# This will return immediately, and requires that the SSH event loop be
|
|
142
145
|
# run in order to effect the upload. (See #wait.)
|
|
143
146
|
def initialize(sftp, local, remote, options={}, &progress) #:nodoc:
|
|
147
|
+
local = local.to_path if local.is_a?(Pathname)
|
|
148
|
+
remote = remote.to_path if remote.is_a?(Pathname)
|
|
149
|
+
|
|
144
150
|
@sftp = sftp
|
|
145
151
|
@local = local
|
|
146
152
|
@remote = remote
|
|
@@ -153,6 +159,8 @@ module Net; module SFTP; module Operations
|
|
|
153
159
|
|
|
154
160
|
@uploads = []
|
|
155
161
|
@recursive = local.respond_to?(:read) ? false : ::File.directory?(local)
|
|
162
|
+
requests
|
|
163
|
+
read_size
|
|
156
164
|
|
|
157
165
|
if recursive?
|
|
158
166
|
@stack = [entries_for(local)]
|
|
@@ -164,7 +172,7 @@ module Net; module SFTP; module Operations
|
|
|
164
172
|
sftp.mkdir(remote) do |response|
|
|
165
173
|
@active -= 1
|
|
166
174
|
raise StatusException.new(response, "mkdir `#{remote}'") unless response.ok?
|
|
167
|
-
|
|
175
|
+
requests.times do
|
|
168
176
|
break unless process_next_entry
|
|
169
177
|
end
|
|
170
178
|
end
|
|
@@ -225,6 +233,21 @@ module Net; module SFTP; module Operations
|
|
|
225
233
|
# The progress handler for this instance. Possibly nil.
|
|
226
234
|
def progress; @progress; end
|
|
227
235
|
|
|
236
|
+
def requests
|
|
237
|
+
@requests ||= positive_option(:requests, recursive? ? RECURSIVE_READERS : SINGLE_FILE_READERS)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def read_size
|
|
241
|
+
@read_size ||= positive_option(:read_size, DEFAULT_READ_SIZE)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def positive_option(name, default)
|
|
245
|
+
value = (options[name] || default).to_i
|
|
246
|
+
raise ArgumentError, ":#{name} must be positive" unless value > 0
|
|
247
|
+
|
|
248
|
+
value
|
|
249
|
+
end
|
|
250
|
+
|
|
228
251
|
# A simple struct for recording metadata about the file currently being
|
|
229
252
|
# uploaded.
|
|
230
253
|
LiveFile = Struct.new(:local, :remote, :io, :size, :handle)
|
|
@@ -326,7 +349,7 @@ module Net; module SFTP; module Operations
|
|
|
326
349
|
write_next_chunk(file)
|
|
327
350
|
|
|
328
351
|
if !recursive?
|
|
329
|
-
(
|
|
352
|
+
(requests - 1).times { write_next_chunk(file) }
|
|
330
353
|
end
|
|
331
354
|
end
|
|
332
355
|
|
|
@@ -358,7 +381,7 @@ module Net; module SFTP; module Operations
|
|
|
358
381
|
else
|
|
359
382
|
@active += 1
|
|
360
383
|
offset = file.io.pos
|
|
361
|
-
data = file.io.read(
|
|
384
|
+
data = file.io.read(read_size)
|
|
362
385
|
if data.nil?
|
|
363
386
|
update_progress(:close, file)
|
|
364
387
|
request = sftp.close(file.handle, &method(:on_close))
|
|
@@ -150,7 +150,7 @@ module Net; module SFTP; module Protocol; module V01
|
|
|
150
150
|
# * :mtime:: the modification time of the file (integer, seconds since epoch)
|
|
151
151
|
# * :extended:: a hash of name/value pairs identifying extended info
|
|
152
152
|
def initialize(attributes={})
|
|
153
|
-
@attributes = attributes
|
|
153
|
+
@attributes = attributes.dup
|
|
154
154
|
end
|
|
155
155
|
|
|
156
156
|
# Returns the user-id of the user that owns the file, or +nil+ if that
|
|
@@ -5,7 +5,7 @@ module Net; module SFTP; module Protocol; module V01
|
|
|
5
5
|
# for use when displaying directory data, and has no specified format.
|
|
6
6
|
class Name
|
|
7
7
|
# The name of the item on the remote server.
|
|
8
|
-
|
|
8
|
+
attr_accessor :name
|
|
9
9
|
|
|
10
10
|
# The display-ready name of the item, possibly with other attributes.
|
|
11
11
|
attr_reader :longname
|
|
@@ -18,6 +18,16 @@ module Net; module SFTP; module Protocol; module V03
|
|
|
18
18
|
3
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
# Version 3 adds a message and language tag to status responses.
|
|
22
|
+
def parse_status_packet(packet)
|
|
23
|
+
data = super
|
|
24
|
+
unless packet.eof?
|
|
25
|
+
data[:message] = packet.read_string
|
|
26
|
+
data[:language] = packet.read_string
|
|
27
|
+
end
|
|
28
|
+
data
|
|
29
|
+
end
|
|
30
|
+
|
|
21
31
|
# Sends a FXP_READLINK packet to the server to request that the target of
|
|
22
32
|
# the given symlink on the remote host (+path+) be returned.
|
|
23
33
|
def readlink(path)
|
|
@@ -32,4 +42,4 @@ module Net; module SFTP; module Protocol; module V03
|
|
|
32
42
|
|
|
33
43
|
end
|
|
34
44
|
|
|
35
|
-
end; end; end; end
|
|
45
|
+
end; end; end; end
|
|
@@ -123,7 +123,7 @@ module Net; module SFTP; module Protocol; module V04
|
|
|
123
123
|
# to T_REGULAR.
|
|
124
124
|
def initialize(attributes={})
|
|
125
125
|
super
|
|
126
|
-
|
|
126
|
+
self.type ||= T_REGULAR
|
|
127
127
|
end
|
|
128
128
|
|
|
129
129
|
private
|
|
@@ -134,6 +134,15 @@ module Net; module SFTP; module Protocol; module V04
|
|
|
134
134
|
# were given on instantiation
|
|
135
135
|
owner
|
|
136
136
|
group
|
|
137
|
+
|
|
138
|
+
# The subsecond flag applies to every timestamp present in the packet.
|
|
139
|
+
subseconds = self.class.elements.select { |_name, _type, flags| flags & F_SUBSECOND_TIMES != 0 }
|
|
140
|
+
if subseconds.any? { |name, _type, _flags| attributes[name] }
|
|
141
|
+
subseconds.each do |name, _type, _flags|
|
|
142
|
+
seconds = name.to_s.sub(/_nseconds\z/, "").to_sym
|
|
143
|
+
attributes[name] ||= 0 if attributes[seconds]
|
|
144
|
+
end
|
|
145
|
+
end
|
|
137
146
|
end
|
|
138
147
|
|
|
139
148
|
# Performs protocol-version-specific encoding of the access control
|
|
@@ -9,7 +9,7 @@ module Net; module SFTP; module Protocol; module V04
|
|
|
9
9
|
# a directory listing.
|
|
10
10
|
class Name
|
|
11
11
|
# The name of the item on the remote server.
|
|
12
|
-
|
|
12
|
+
attr_accessor :name
|
|
13
13
|
|
|
14
14
|
# Attributes instance describing this item.
|
|
15
15
|
attr_reader :attributes
|
|
@@ -44,24 +44,33 @@ module Net; module SFTP; module Protocol; module V04
|
|
|
44
44
|
"l"
|
|
45
45
|
else
|
|
46
46
|
"-"
|
|
47
|
-
end
|
|
47
|
+
end.dup
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
49
|
+
# Fields the server omitted are shown as "?", like GNU ls -l does for entries it cannot stat.
|
|
50
|
+
if (permissions = attributes.permissions)
|
|
51
|
+
longname << (permissions & 0400 != 0 ? "r" : "-")
|
|
52
|
+
longname << (permissions & 0200 != 0 ? "w" : "-")
|
|
53
|
+
longname << (permissions & 0100 != 0 ? "x" : "-")
|
|
54
|
+
longname << (permissions & 0040 != 0 ? "r" : "-")
|
|
55
|
+
longname << (permissions & 0020 != 0 ? "w" : "-")
|
|
56
|
+
longname << (permissions & 0010 != 0 ? "x" : "-")
|
|
57
|
+
longname << (permissions & 0004 != 0 ? "r" : "-")
|
|
58
|
+
longname << (permissions & 0002 != 0 ? "w" : "-")
|
|
59
|
+
longname << (permissions & 0001 != 0 ? "x" : "-")
|
|
60
|
+
else
|
|
61
|
+
longname << "?" * 9
|
|
62
|
+
end
|
|
58
63
|
|
|
59
|
-
longname << (" %-8s %-8s %
|
|
64
|
+
longname << (" %-8s %-8s %8s " % [attributes.owner || "?", attributes.group || "?", attributes.size || "?"])
|
|
60
65
|
|
|
61
|
-
longname <<
|
|
66
|
+
longname << if attributes.mtime
|
|
67
|
+
Time.at(attributes.mtime).strftime("%b %e %H:%M ")
|
|
68
|
+
else
|
|
69
|
+
"%12s " % "?"
|
|
70
|
+
end
|
|
62
71
|
longname << name
|
|
63
72
|
end
|
|
64
73
|
end
|
|
65
74
|
end
|
|
66
75
|
|
|
67
|
-
end; end; end; end
|
|
76
|
+
end; end; end; end
|
|
@@ -35,25 +35,28 @@ module Net; module SFTP; module Protocol; module V05
|
|
|
35
35
|
def open(path, flags, options)
|
|
36
36
|
flags = normalize_open_flags(flags)
|
|
37
37
|
|
|
38
|
-
sftp_flags
|
|
39
|
-
open = if flags & (IO::CREAT | IO::EXCL) == (IO::CREAT | IO::EXCL)
|
|
38
|
+
sftp_flags = if flags & (IO::CREAT | IO::EXCL) == (IO::CREAT | IO::EXCL)
|
|
40
39
|
FV5::CREATE_NEW
|
|
41
40
|
elsif flags & (IO::CREAT | IO::TRUNC) == (IO::CREAT | IO::TRUNC)
|
|
42
41
|
FV5::CREATE_TRUNCATE
|
|
43
42
|
elsif flags & IO::CREAT == IO::CREAT
|
|
44
43
|
FV5::OPEN_OR_CREATE
|
|
44
|
+
elsif flags & IO::TRUNC == IO::TRUNC
|
|
45
|
+
FV5::TRUNCATE_EXISTING
|
|
45
46
|
else
|
|
46
47
|
FV5::OPEN_EXISTING
|
|
47
48
|
end
|
|
49
|
+
|
|
50
|
+
desired_access = if flags & (IO::WRONLY | IO::RDWR) != 0
|
|
48
51
|
access = ACE::Mask::WRITE_DATA | ACE::Mask::WRITE_ATTRIBUTES
|
|
49
52
|
access |= ACE::Mask::READ_DATA | ACE::Mask::READ_ATTRIBUTES if (flags & IO::RDWR) == IO::RDWR
|
|
50
53
|
if flags & IO::APPEND == IO::APPEND
|
|
51
|
-
|
|
54
|
+
sftp_flags |= FV5::APPEND_DATA
|
|
52
55
|
access |= ACE::Mask::APPEND_DATA
|
|
53
56
|
end
|
|
54
|
-
|
|
57
|
+
access
|
|
55
58
|
else
|
|
56
|
-
|
|
59
|
+
ACE::Mask::READ_DATA | ACE::Mask::READ_ATTRIBUTES
|
|
57
60
|
end
|
|
58
61
|
|
|
59
62
|
attributes = attribute_factory.new(options)
|
|
@@ -63,4 +66,4 @@ module Net; module SFTP; module Protocol; module V05
|
|
|
63
66
|
|
|
64
67
|
end
|
|
65
68
|
|
|
66
|
-
end; end; end; end
|
|
69
|
+
end; end; end; end
|
|
@@ -19,6 +19,7 @@ module Net; module SFTP; module Protocol
|
|
|
19
19
|
@session = session
|
|
20
20
|
self.logger = session.logger
|
|
21
21
|
@request_id_counter = -1
|
|
22
|
+
@request_ids_wrapped = false
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
# Attept to parse the given packet. If the packet is of an unsupported
|
|
@@ -41,10 +42,15 @@ module Net; module SFTP; module Protocol
|
|
|
41
42
|
# A new request identifier will be allocated to this request, and will
|
|
42
43
|
# be returned.
|
|
43
44
|
def send_request(type, *args)
|
|
44
|
-
|
|
45
|
+
begin
|
|
46
|
+
previous_request_id = @request_id_counter
|
|
47
|
+
@request_id_counter = (@request_id_counter + 1) & 0xffffffff
|
|
48
|
+
@request_ids_wrapped = true if previous_request_id == 0xffffffff
|
|
49
|
+
end while @request_ids_wrapped && session.pending_requests.key?(@request_id_counter)
|
|
50
|
+
|
|
45
51
|
session.send_packet(type, :long, @request_id_counter, *args)
|
|
46
52
|
return @request_id_counter
|
|
47
53
|
end
|
|
48
54
|
end
|
|
49
55
|
|
|
50
|
-
end; end; end
|
|
56
|
+
end; end; end
|
data/lib/net/sftp/session.rb
CHANGED
|
@@ -269,6 +269,10 @@ module Net; module SFTP
|
|
|
269
269
|
# Request object. If a block is given, it will be invoked when the
|
|
270
270
|
# server responds.
|
|
271
271
|
#
|
|
272
|
+
# +data+ is sent as a single request, so it must stay within the
|
|
273
|
+
# server's packet limit (OpenSSH drops the connection on packets over
|
|
274
|
+
# 256 KiB). Net::SFTP::Operations::File#write splits large data for you.
|
|
275
|
+
#
|
|
272
276
|
# request = sftp.write(handle, 0, "hello, world!\n")
|
|
273
277
|
# request.wait
|
|
274
278
|
def write(handle, offset, data, &callback)
|
|
@@ -772,6 +776,7 @@ module Net; module SFTP
|
|
|
772
776
|
when :closed
|
|
773
777
|
@state = :opening
|
|
774
778
|
@channel = session.open_channel(&method(:when_channel_confirmed))
|
|
779
|
+
@input = Net::SSH::Buffer.new
|
|
775
780
|
@packet_length = nil
|
|
776
781
|
@protocol = nil
|
|
777
782
|
@on_ready = Array(block)
|
data/lib/net/sftp/version.rb
CHANGED
data/lib/net/sftp.rb
CHANGED
|
@@ -34,18 +34,24 @@ module Net
|
|
|
34
34
|
# set the SFTP protocol version to be used)
|
|
35
35
|
def self.start(host, user, ssh_options={}, sftp_options={}, &block)
|
|
36
36
|
session = Net::SSH.start(host, user, ssh_options)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
begin
|
|
38
|
+
# We only use a single option here, but this leaves room for more later
|
|
39
|
+
# without breaking the external API.
|
|
40
|
+
version = sftp_options.fetch(:version, nil)
|
|
41
|
+
sftp = Net::SFTP::Session.new(session, version, &block).connect!
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
end
|
|
43
|
+
if block_given?
|
|
44
|
+
sftp.loop
|
|
45
|
+
return nil
|
|
46
|
+
end
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
sftp
|
|
49
|
+
rescue Object
|
|
50
|
+
failed = true
|
|
51
|
+
raise
|
|
52
|
+
ensure
|
|
53
|
+
session.close if block_given? && !failed
|
|
54
|
+
end
|
|
49
55
|
rescue Object => anything
|
|
50
56
|
begin
|
|
51
57
|
session.shutdown!
|
data/net-sftp-public_cert.pem
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
-----BEGIN CERTIFICATE-----
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
2
|
+
MIIDeDCCAmCgAwIBAgIBATANBgkqhkiG9w0BAQsFADBBMQ8wDQYDVQQDDAZuZXRz
|
|
3
|
+
c2gxGTAXBgoJkiaJk/IsZAEZFglzb2x1dGlvdXMxEzARBgoJkiaJk/IsZAEZFgNj
|
|
4
|
+
b20wHhcNMjYxMDIyMDk1NDI1WhcNMjcxMDIyMDk1NDI1WjBBMQ8wDQYDVQQDDAZu
|
|
5
|
+
ZXRzc2gxGTAXBgoJkiaJk/IsZAEZFglzb2x1dGlvdXMxEzARBgoJkiaJk/IsZAEZ
|
|
6
|
+
FgNjb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGJ4TbZ9H+qZ08
|
|
7
|
+
pQfJhPJTHaDCyQvCsKTFrL5O9z3tllQ7B/zksMMM+qFBpNYu9HCcg4yBATacE/PB
|
|
8
|
+
qVVyUrpr6lbH/XwoN5ljXm+bdCfmnjZvTCL2FTE6o+bcnaF0IsJyC0Q2B1fbWdXN
|
|
9
|
+
6Off1ZWoUk6We2BIM1bn6QJLxBpGyYhvOPXsYoqSuzDf2SJDDsWFZ8kV5ON13Ohm
|
|
10
|
+
JbBzn0oD8HF8FuYOewwsC0C1q4w7E5GtvHcQ5juweS7+RKsyDcVcVrLuNzoGRttS
|
|
11
|
+
KP4yMn+TzaXijyjRg7gECfJr3TGASaA4bQsILFGG5dAWcwO4OMrZedR7SHj/o0Kf
|
|
12
|
+
3gL7P0axAgMBAAGjezB5MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0GA1UdDgQW
|
|
13
|
+
BBQF8qLA7Z4zg0SJGtUbv3eoQ8tjIzAfBgNVHREEGDAWgRRuZXRzc2hAc29sdXRp
|
|
14
|
+
b3VzLmNvbTAfBgNVHRIEGDAWgRRuZXRzc2hAc29sdXRpb3VzLmNvbTANBgkqhkiG
|
|
15
|
+
9w0BAQsFAAOCAQEAbVrRpiCFmHDMLNU+SCjpg43snWYksQOtejPPTEMdOc9cCmmv
|
|
16
|
+
ctJbaPIySLUgzwFbQDZStDPbLJmIujh0fm5dHp/sDJpvPZ3Mm3FfzWQMJqzBx4uk
|
|
17
|
+
OMx00yJK8VeFrwUViE3zO5raWvw85dUybp25pgaKGW4AA5mJ/2UHqYLc4lefKFKK
|
|
18
|
+
9xFsyYSciMEKyAsUNSNTJzPKUo2vn7TNF/pS0ZL+h8Qkr0A0F0ncfj7uM1kOWjO0
|
|
19
|
+
mMdUtmMoVbjbik3hqkY/18G03c3PoFNpL38VLKuyyROYz1DaCqOaRju3sbOoirD+
|
|
20
|
+
piQTZDCWxeKuX0Iuxq02uxkYHfoGB6In0y3OOQ==
|
|
20
21
|
-----END CERTIFICATE-----
|
data/net-sftp.gemspec
CHANGED
|
@@ -31,16 +31,16 @@ Gem::Specification.new do |spec|
|
|
|
31
31
|
spec.specification_version = 3
|
|
32
32
|
|
|
33
33
|
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
|
34
|
-
spec.add_runtime_dependency(%q<net-ssh>, [">= 5.0.0", "<
|
|
34
|
+
spec.add_runtime_dependency(%q<net-ssh>, [">= 5.0.0", "< 9.0.0"])
|
|
35
35
|
spec.add_development_dependency(%q<minitest>, [">= 5"])
|
|
36
36
|
spec.add_development_dependency(%q<mocha>, [">= 0"])
|
|
37
37
|
else
|
|
38
|
-
spec.add_dependency(%q<net-ssh>, [">= 5.0.0", "<
|
|
38
|
+
spec.add_dependency(%q<net-ssh>, [">= 5.0.0", "< 9.0.0"])
|
|
39
39
|
spec.add_dependency(%q<minitest>, [">= 5"])
|
|
40
40
|
spec.add_dependency(%q<mocha>, [">= 0"])
|
|
41
41
|
end
|
|
42
42
|
else
|
|
43
|
-
spec.add_dependency(%q<net-ssh>, [">= 5.0.0", "<
|
|
43
|
+
spec.add_dependency(%q<net-ssh>, [">= 5.0.0", "< 9.0.0"])
|
|
44
44
|
spec.add_dependency(%q<minitest>, [">= 5"])
|
|
45
45
|
spec.add_dependency(%q<test-unit>, [">= 0"])
|
|
46
46
|
spec.add_dependency(%q<mocha>, [">= 0"])
|
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,37 +1,38 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: net-sftp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.0.
|
|
4
|
+
version: 4.0.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jamis Buck
|
|
8
8
|
- Delano Mandelbaum
|
|
9
9
|
- Miklós Fazekas
|
|
10
|
-
autorequire:
|
|
10
|
+
autorequire:
|
|
11
11
|
bindir: exe
|
|
12
12
|
cert_chain:
|
|
13
13
|
- |
|
|
14
14
|
-----BEGIN CERTIFICATE-----
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
15
|
+
MIIDeDCCAmCgAwIBAgIBATANBgkqhkiG9w0BAQsFADBBMQ8wDQYDVQQDDAZuZXRz
|
|
16
|
+
c2gxGTAXBgoJkiaJk/IsZAEZFglzb2x1dGlvdXMxEzARBgoJkiaJk/IsZAEZFgNj
|
|
17
|
+
b20wHhcNMjYxMDIyMDk1NDI1WhcNMjcxMDIyMDk1NDI1WjBBMQ8wDQYDVQQDDAZu
|
|
18
|
+
ZXRzc2gxGTAXBgoJkiaJk/IsZAEZFglzb2x1dGlvdXMxEzARBgoJkiaJk/IsZAEZ
|
|
19
|
+
FgNjb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGJ4TbZ9H+qZ08
|
|
20
|
+
pQfJhPJTHaDCyQvCsKTFrL5O9z3tllQ7B/zksMMM+qFBpNYu9HCcg4yBATacE/PB
|
|
21
|
+
qVVyUrpr6lbH/XwoN5ljXm+bdCfmnjZvTCL2FTE6o+bcnaF0IsJyC0Q2B1fbWdXN
|
|
22
|
+
6Off1ZWoUk6We2BIM1bn6QJLxBpGyYhvOPXsYoqSuzDf2SJDDsWFZ8kV5ON13Ohm
|
|
23
|
+
JbBzn0oD8HF8FuYOewwsC0C1q4w7E5GtvHcQ5juweS7+RKsyDcVcVrLuNzoGRttS
|
|
24
|
+
KP4yMn+TzaXijyjRg7gECfJr3TGASaA4bQsILFGG5dAWcwO4OMrZedR7SHj/o0Kf
|
|
25
|
+
3gL7P0axAgMBAAGjezB5MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0GA1UdDgQW
|
|
26
|
+
BBQF8qLA7Z4zg0SJGtUbv3eoQ8tjIzAfBgNVHREEGDAWgRRuZXRzc2hAc29sdXRp
|
|
27
|
+
b3VzLmNvbTAfBgNVHRIEGDAWgRRuZXRzc2hAc29sdXRpb3VzLmNvbTANBgkqhkiG
|
|
28
|
+
9w0BAQsFAAOCAQEAbVrRpiCFmHDMLNU+SCjpg43snWYksQOtejPPTEMdOc9cCmmv
|
|
29
|
+
ctJbaPIySLUgzwFbQDZStDPbLJmIujh0fm5dHp/sDJpvPZ3Mm3FfzWQMJqzBx4uk
|
|
30
|
+
OMx00yJK8VeFrwUViE3zO5raWvw85dUybp25pgaKGW4AA5mJ/2UHqYLc4lefKFKK
|
|
31
|
+
9xFsyYSciMEKyAsUNSNTJzPKUo2vn7TNF/pS0ZL+h8Qkr0A0F0ncfj7uM1kOWjO0
|
|
32
|
+
mMdUtmMoVbjbik3hqkY/18G03c3PoFNpL38VLKuyyROYz1DaCqOaRju3sbOoirD+
|
|
33
|
+
piQTZDCWxeKuX0Iuxq02uxkYHfoGB6In0y3OOQ==
|
|
33
34
|
-----END CERTIFICATE-----
|
|
34
|
-
date:
|
|
35
|
+
date: 2026-10-25 00:00:00.000000000 Z
|
|
35
36
|
dependencies:
|
|
36
37
|
- !ruby/object:Gem::Dependency
|
|
37
38
|
name: net-ssh
|
|
@@ -42,7 +43,7 @@ dependencies:
|
|
|
42
43
|
version: 5.0.0
|
|
43
44
|
- - "<"
|
|
44
45
|
- !ruby/object:Gem::Version
|
|
45
|
-
version:
|
|
46
|
+
version: 9.0.0
|
|
46
47
|
type: :runtime
|
|
47
48
|
prerelease: false
|
|
48
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -52,7 +53,7 @@ dependencies:
|
|
|
52
53
|
version: 5.0.0
|
|
53
54
|
- - "<"
|
|
54
55
|
- !ruby/object:Gem::Version
|
|
55
|
-
version:
|
|
56
|
+
version: 9.0.0
|
|
56
57
|
- !ruby/object:Gem::Dependency
|
|
57
58
|
name: minitest
|
|
58
59
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -90,14 +91,18 @@ extra_rdoc_files:
|
|
|
90
91
|
- LICENSE.txt
|
|
91
92
|
- README.rdoc
|
|
92
93
|
files:
|
|
94
|
+
- ".github/pull_request_template.md"
|
|
95
|
+
- ".github/workflows/changelog.yml"
|
|
93
96
|
- ".github/workflows/ci.yml"
|
|
94
97
|
- ".gitignore"
|
|
95
98
|
- CHANGES.txt
|
|
99
|
+
- CONTRIBUTING.md
|
|
96
100
|
- Gemfile
|
|
97
101
|
- LICENSE.txt
|
|
98
102
|
- Manifest
|
|
99
103
|
- README.rdoc
|
|
100
104
|
- Rakefile
|
|
105
|
+
- changelog.d/README.md
|
|
101
106
|
- lib/net/sftp.rb
|
|
102
107
|
- lib/net/sftp/constants.rb
|
|
103
108
|
- lib/net/sftp/errors.rb
|
|
@@ -131,7 +136,7 @@ homepage: https://github.com/net-ssh/net-sftp
|
|
|
131
136
|
licenses:
|
|
132
137
|
- MIT
|
|
133
138
|
metadata: {}
|
|
134
|
-
post_install_message:
|
|
139
|
+
post_install_message:
|
|
135
140
|
rdoc_options: []
|
|
136
141
|
require_paths:
|
|
137
142
|
- lib
|
|
@@ -146,8 +151,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
146
151
|
- !ruby/object:Gem::Version
|
|
147
152
|
version: '0'
|
|
148
153
|
requirements: []
|
|
149
|
-
rubygems_version: 3.
|
|
150
|
-
signing_key:
|
|
154
|
+
rubygems_version: 3.3.3
|
|
155
|
+
signing_key:
|
|
151
156
|
specification_version: 3
|
|
152
157
|
summary: A pure Ruby implementation of the SFTP client protocol.
|
|
153
158
|
test_files: []
|
metadata.gz.sig
CHANGED
|
Binary file
|