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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4319de630f665ee342eb3c367bf1fbe1e71a057206379c5251ab2c3b073d0cb7
4
- data.tar.gz: a35ca9f05cf5bd73f2e9b5ede7ed9f473d0025c7da530707a6470e0bdbcc1adb
3
+ metadata.gz: 4d3d330529ea03b189dc54e18be90d35bb2bfb79374917e9e22a2fcd3d7d79ca
4
+ data.tar.gz: feaf13c15e37ea24c5ed45f52a842b0a8d4413cc2a27f1ee6de5a03a0975d527
5
5
  SHA512:
6
- metadata.gz: 37774670d3e3a6627f1070e5043316ccc590fd0c5049971ff36a5f83391b266a129b3f660f1b75a1d71819baef95c104f3c22bb21a1c90cdab05ff4c96ddd7a9
7
- data.tar.gz: 0fd554d08be7d9151484f8fe8a5bb17a4f095918457a33ffc6dbebd06a7fbffc3fcbf6d79d159339e4f20c3bada94d1b3c9980936808012287b717296c4facf0
6
+ metadata.gz: 3da2ef173bd6e6ac212cf3ae73243a887ae0d136fc01f03a06b383f00a6ea19a55d2f461f5fadff10d6991dd295c6fb9c826edf6557f1d2a831fe32f2782dfdf
7
+ data.tar.gz: e12c6bdf4efc55e51fbf6dbe3ddb12494b8f3b090e9786bdf6deb002a6966505e9a6a63440fdb43fb23da9c20fce7b0c40d2b2e5425dd095a97dfe1648102dfb
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
@@ -1,35 +1,37 @@
1
1
  name: CI
2
- on: [push, pull_request]
2
+ on:
3
+ pull_request:
4
+ push: { branches: master }
5
+ permissions:
6
+ contents: read
3
7
  jobs:
4
8
  test:
5
- runs-on: ubuntu-18.04
6
- continue-on-error: ${{ matrix.flaky }}
9
+ # setup-ruby has no builds of Ruby 3.1 and older for ubuntu-latest (26.04).
10
+ runs-on: ubuntu-24.04
7
11
  strategy:
12
+ fail-fast: false
8
13
  matrix:
9
- ruby-version: ["2.5", "2.6", "2.7", "3.0", "3.1", "truffleruby-22", "truffleruby-21"]
10
- flaky: [false]
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
14
+ ruby-version: ['2.6', '2.7', '3.0', '3.1', '3.2', '3.3', '3.4', '4.0', 'ruby-head', 'jruby', 'truffleruby']
15
+ continue-on-error: ${{ contains(fromJSON('["ruby-head", "jruby", "truffleruby"]'), matrix.ruby-version) }}
22
16
  steps:
23
- - uses: actions/checkout@v1
24
-
17
+ - uses: actions/checkout@v4
25
18
  - name: Set up Ruby ${{ matrix.ruby-version }}
26
19
  uses: ruby/setup-ruby@v1
27
20
  with:
28
21
  ruby-version: ${{ matrix.ruby-version }}
29
-
30
- - name: Bundle install
31
- run: |
32
- gem install bundler
33
- bundle install
22
+ bundler-cache: true
34
23
  - name: Run Tests
35
24
  run: bundle exec rake test
25
+ frozen-string-literal:
26
+ runs-on: ubuntu-latest
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+ - name: Set up Ruby
30
+ uses: ruby/setup-ruby@v1
31
+ with:
32
+ ruby-version: '4.0'
33
+ bundler-cache: true
34
+ - name: Run Tests with frozen string literals
35
+ run: bundle exec rake test
36
+ env:
37
+ RUBYOPT: --enable-frozen-string-literal
data/.gitignore CHANGED
@@ -4,3 +4,4 @@ coverage
4
4
  ri
5
5
  *.swp
6
6
  Gemfile.lock
7
+ lib/net/sftp/version.rb.old
data/CHANGES.txt CHANGED
@@ -1,3 +1,48 @@
1
+ === 4.1.0
2
+
3
+ * BREAKING: Require Ruby 2.6 or later. Bundler on older Rubies resolves to 4.0.2. [#207]
4
+ * 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]
5
+ * 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]
6
+ * 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]
7
+ * 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]
8
+ * `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]
9
+ * Added `Net::SFTP::Session#extensions`, the extensions the server advertised in its `FXP_VERSION` packet (such as `posix-rename@openssh.com`). [#195]
10
+ * 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]
11
+
12
+ === 4.0.2
13
+
14
+ * 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]
15
+ * 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]
16
+ * 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]
17
+ * `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]
18
+ * Show fields an SFTP v4+ server omitted as `?` in directory listings (`longname`), like `ls -l` does, instead of blank, `-` or `---------` [#188]
19
+ * Fixed `File#write` hanging on data of 256 KiB or more by sending it in 32 KiB write requests [#99, #187]
20
+ * 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]
21
+ * 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]
22
+ * 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]
23
+ * 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]
24
+ * `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]
25
+ * `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]
26
+ * `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]
27
+ * 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]
28
+ * 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]
29
+ * 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]
30
+ * `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]
31
+ * 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]
32
+ * `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]
33
+ * `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]
34
+ * 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]
35
+ * Accept a `Pathname` as the local or remote path for uploads and downloads. Thanks @Tietew and @til [#153, #102]
36
+
37
+ === 4.0.1
38
+
39
+ * Allow net-ssh 8 [#181]
40
+
41
+ === 4.0.0
42
+
43
+ * Allow net-ssh 7
44
+ * Fix multibyte string write [#110]
45
+
1
46
  === 3.0.0
2
47
 
3
48
  * 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", "~> 2.1"
8
- gem "rake", "~> 12.0"
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/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
 
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 365*5 --private-key /mnt/gem/net-ssh-private_key.pem"
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,116 @@ 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
+ minor = Net::SFTP::Version::MINOR
72
+ result = block[pre: pre, tiny: tiny, minor: minor]
73
+ raise ArgumentError, "Version change logic should always return a pre" unless result.key?(:pre)
74
+
75
+ new_pre = result[:pre]
76
+ new_tiny = result[:tiny] || tiny
77
+ new_minor = result[:minor] || minor
78
+ found = { pre: false, tiny: false, minor: false }
79
+ File.open("#{version_file}.new", "w") do |f|
80
+ File.readlines(version_file).each do |line|
81
+ match =
82
+ if pre.nil?
83
+ /^(\s+PRE\s+=\s+)nil(\s*)$/.match(line)
84
+ else
85
+ /^(\s+PRE\s+=\s+")#{pre}("\s*)$/.match(line)
86
+ end
87
+ if match
88
+ prefix = match[1]
89
+ postfix = match[2]
90
+ prefix.delete_suffix!('"')
91
+ postfix.delete_prefix!('"')
92
+ new_line = "#{prefix}#{new_pre.inspect}#{postfix}"
93
+ puts "Changing:\n - #{line} + #{new_line}"
94
+ line = new_line
95
+ found[:pre] = true
96
+ end
97
+
98
+ if new_tiny != tiny
99
+ match = /^(\s+TINY\s+=\s+)#{tiny}(\s*)$/.match(line)
100
+ if match
101
+ prefix = match[1]
102
+ postfix = match[2]
103
+ new_line = "#{prefix}#{new_tiny}#{postfix}"
104
+ puts "Changing:\n - #{line} + #{new_line}"
105
+ line = new_line
106
+ found[:tiny] = true
107
+ end
108
+ end
109
+
110
+ if new_minor != minor
111
+ match = /^(\s+MINOR\s+=\s+)#{minor}(\s*)$/.match(line)
112
+ if match
113
+ prefix = match[1]
114
+ postfix = match[2]
115
+ new_line = "#{prefix}#{new_minor}#{postfix}"
116
+ puts "Changing:\n - #{line} + #{new_line}"
117
+ line = new_line
118
+ found[:minor] = true
119
+ end
120
+ end
121
+
122
+ f.write(line)
123
+ end
124
+ raise ArgumentError, "Cound not find line: PRE = \"#{pre}\" in #{version_file}" unless found[:pre]
125
+ raise ArgumentError, "Cound not find line: TINY = \"#{tiny}\" in #{version_file}" unless found[:tiny] || new_tiny == tiny
126
+ raise ArgumentError, "Cound not find line: MINOR = \"#{minor}\" in #{version_file}" unless found[:minor] || new_minor == minor
127
+ end
128
+
129
+ FileUtils.mv version_file, "#{version_file}.old"
130
+ FileUtils.mv "#{version_file}.new", version_file
131
+ Rake::Task["changelog:release"].invoke
132
+ end
133
+
134
+ namespace :vbump do
135
+ desc "Final release"
136
+ task :final do
137
+ change_version do |pre:, tiny:, **|
138
+ _ = tiny
139
+ if pre.nil?
140
+ { tiny: tiny + 1, pre: nil }
141
+ else
142
+ raise ArgumentError, "Unexpected pre: #{pre}" if pre.nil?
143
+
144
+ { pre: nil }
145
+ end
146
+ end
147
+ end
148
+
149
+ desc "Minor release"
150
+ task :minor do
151
+ change_version do |pre:, minor:, **|
152
+ raise ArgumentError, "PRE is #{pre.inspect}; release it with vbump:final first" unless pre.nil?
153
+
154
+ { minor: minor + 1, tiny: 0, pre: nil }
155
+ end
156
+ end
157
+
158
+ desc "Increment prerelease"
159
+ task :pre, [:type] do |_t, args|
160
+ change_version do |pre:, tiny:, **|
161
+ puts " PRE => #{pre.inspect}"
162
+ match = /^([a-z]+)(\d+)/.match(pre)
163
+ raise ArgumentError, "Unexpected pre: #{pre}" if match.nil? && args[:type].nil?
164
+
165
+ if match.nil? || (!args[:type].nil? && args[:type] != match[1])
166
+ if pre.nil?
167
+ { pre: "#{args[:type]}1", tiny: tiny + 1 }
168
+ else
169
+ { pre: "#{args[:type]}1" }
170
+ end
171
+ else
172
+ { pre: "#{match[1]}#{match[2].to_i + 1}" }
173
+ end
174
+ end
175
+ end
176
+ 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 += entries("#{path}/#{entry.name}").map do |e|
69
- e.name.replace("#{entry.name}/#{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)