bfs-scp 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 31c7f82a6aae8c2bd13d174bb267ba3a27d2635e03813fa99f2aa531be59f9be
4
+ data.tar.gz: 3b89574dc5e9b02693de9a6bae66fefea69605b5d75e83a17de062dd532a35a6
5
+ SHA512:
6
+ metadata.gz: 84c82c16c24daa41792c9a2509092f06a79bd080ae4b83dad44ab97b41730cc03b1af9921f25d6c5dcf9f137ec2260b47d814afbd52699a259eb6b0f07280498
7
+ data.tar.gz: 58d1ef53e369746d33b4c783c1bcaa67a72963e6b289f7f2b080362525197503eea5c4599e2b5d469b76fb3fe8b614585743806d48a8e9b62459591912a28b29
@@ -0,0 +1,22 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'bfs-scp'
3
+ s.version = File.read(File.expand_path('../.version', __dir__)).strip
4
+ s.platform = Gem::Platform::RUBY
5
+
6
+ s.licenses = ['Apache-2.0']
7
+ s.summary = 'SCP/SSH adapter for bfs'
8
+ s.description = 'https://github.com/bsm/bfs.rb'
9
+
10
+ s.authors = ['Dimitrij Denissenko']
11
+ s.email = 'dimitrij@blacksquaremedia.com'
12
+ s.homepage = 'https://github.com/bsm/bfs.rb'
13
+
14
+ s.executables = []
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files -- spec/*`.split("\n")
17
+ s.require_paths = ['lib']
18
+ s.required_ruby_version = '>= 2.4.0'
19
+
20
+ s.add_dependency 'bfs', s.version
21
+ s.add_dependency 'net-scp'
22
+ end
@@ -0,0 +1,190 @@
1
+ require 'bfs'
2
+ require 'net/scp'
3
+ require 'cgi'
4
+ require 'shellwords'
5
+
6
+ module BFS
7
+ module Bucket
8
+ # SCP buckets are operating on SCP/SSH connections.
9
+ class SCP < Abstract
10
+ class CommandError < RuntimeError
11
+ attr_reader :status
12
+
13
+ def initialize(cmd, status, extra=nil)
14
+ @status = status
15
+ super ["Command '#{cmd}' exited with status #{status}", extra].join(': ')
16
+ end
17
+ end
18
+
19
+ # Initializes a new bucket
20
+ # @param [String] host the host name
21
+ # @param [Hash] opts options
22
+ # @option opts [Integer] :port custom port. Default: 22.
23
+ # @option opts [String] :user user name for login.
24
+ # @option opts [String] :password password for login.
25
+ # @option opts [String] :prefix optional prefix.
26
+ # @option opts [Boolean] :compression use compression.
27
+ # @option opts [Boolean] :keepalive use keepalive.
28
+ # @option opts [Integer] :keepalive_interval interval if keepalive enabled. Default: 300.
29
+ # @option opts [Array<String>] :keys an array of file names of private keys to use for publickey and hostbased authentication.
30
+ # @option opts [Boolean|Symbol] :verify_host_key specifying how strict host-key verification should be, either false, true, :very, or :secure.
31
+ def initialize(host, opts={})
32
+ opts = opts.dup
33
+ opts.keys.each do |key|
34
+ val = opts.delete(key)
35
+ opts[key.to_sym] = val unless val.nil?
36
+ end
37
+ super(opts)
38
+
39
+ @prefix = opts.delete(:prefix)
40
+ @client = Net::SCP.start(host, nil, opts)
41
+
42
+ if @prefix # rubocop:disable Style/GuardClause
43
+ @prefix = norm_path(@prefix) + '/'
44
+ mkdir_p(@prefix)
45
+ end
46
+ end
47
+
48
+ # Lists the contents of a bucket using a glob pattern
49
+ def ls(pattern='**/*', _opts={})
50
+ prefix = @prefix || '.'
51
+ Enumerator.new do |y|
52
+ sh! 'find', prefix, '-type', 'f' do |out|
53
+ out.each_line do |line|
54
+ path = trim_prefix(line.strip)
55
+ y << path if File.fnmatch?(pattern, path, File::FNM_PATHNAME)
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ # Info returns the object info
62
+ def info(path, _opts={})
63
+ full = full_path(path)
64
+ path = norm_path(path)
65
+ out = sh! 'stat', '-c', '%s;%Z', full
66
+
67
+ size, epoch = out.strip.split(';', 2).map(&:to_i)
68
+ BFS::FileInfo.new(path, size, Time.at(epoch))
69
+ rescue CommandError => e
70
+ e.status == 1 ? raise(BFS::FileNotFound, path) : raise
71
+ end
72
+
73
+ # Creates a new file and opens it for writing
74
+ def create(path, opts={}, &block)
75
+ full = full_path(path)
76
+ enc = opts.delete(:encoding) || @encoding
77
+ temp = BFS::TempWriter.new(path, encoding: enc) do |temp_path|
78
+ mkdir_p File.dirname(full)
79
+ @client.upload!(temp_path, full)
80
+ end
81
+ return temp unless block
82
+
83
+ begin
84
+ yield temp
85
+ ensure
86
+ temp.close
87
+ end
88
+ end
89
+
90
+ # Opens an existing file for reading
91
+ def open(path, opts={}, &block)
92
+ full = full_path(path)
93
+ enc = opts.delete(:encoding) || @encoding
94
+ temp = Tempfile.new(File.basename(path), encoding: enc)
95
+ temp.close
96
+
97
+ @client.download!(full, temp.path)
98
+ File.open(temp.path, encoding: enc, &block)
99
+ rescue Net::SCP::Error
100
+ raise BFS::FileNotFound, path
101
+ end
102
+
103
+ # Deletes a file.
104
+ def rm(path, _opts={})
105
+ path = full_path(path)
106
+ sh! 'rm', '-f', path
107
+ end
108
+
109
+ # Copies src to dst
110
+ #
111
+ # @param [String] src The source path.
112
+ # @param [String] dst The destination path.
113
+ def cp(src, dst, _opts={})
114
+ full_src = full_path(src)
115
+ full_dst = full_path(dst)
116
+
117
+ mkdir_p File.dirname(full_dst)
118
+ sh! 'cp', '-a', '-f', full_src, full_dst
119
+ rescue CommandError => e
120
+ e.status == 1 ? raise(BFS::FileNotFound, src) : raise
121
+ end
122
+
123
+ # Moves src to dst
124
+ #
125
+ # @param [String] src The source path.
126
+ # @param [String] dst The destination path.
127
+ def mv(src, dst, _opts={})
128
+ full_src = full_path(src)
129
+ full_dst = full_path(dst)
130
+
131
+ mkdir_p File.dirname(full_dst)
132
+ sh! 'mv', '-f', full_src, full_dst
133
+ rescue CommandError => e
134
+ e.status == 1 ? raise(BFS::FileNotFound, src) : raise
135
+ end
136
+
137
+ # Closes the underlying connection
138
+ def close
139
+ @client.session.close unless @client.session.closed?
140
+ end
141
+
142
+ private
143
+
144
+ def mkdir_p(path)
145
+ sh! 'mkdir', '-p', path
146
+ end
147
+
148
+ def sh!(*cmd) # rubocop:disable Metrics/MethodLength
149
+ stdout = ''
150
+ stderr = nil
151
+ status = 0
152
+ cmdstr = cmd.map {|x| Shellwords.escape(x) }.join(' ')
153
+
154
+ @client.session.open_channel do |ch|
155
+ ch.exec(cmdstr) do |_, _success|
156
+ ch.on_data do |_, data|
157
+ if block_given?
158
+ yield data
159
+ else
160
+ stdout += data
161
+ end
162
+ end
163
+ ch.on_extended_data do |_, _, data|
164
+ stderr = data
165
+ end
166
+ ch.on_request('exit-status') do |_, buf|
167
+ status = buf.read_long
168
+ end
169
+ end
170
+ end
171
+ @client.session.loop
172
+ raise CommandError.new(cmdstr, status, stderr) unless status.zero?
173
+
174
+ stdout
175
+ end
176
+ end
177
+ end
178
+ end
179
+
180
+ BFS.register('scp', 'ssh') do |url|
181
+ opts = {}
182
+ CGI.parse(url.query.to_s).each do |key, values|
183
+ opts[key.to_sym] = values.first
184
+ end
185
+ opts[:user] ||= CGI.unescape(url.user) if url.user
186
+ opts[:password] ||= CGI.unescape(url.password) if url.password
187
+ opts[:port] ||= url.port if url.port
188
+
189
+ BFS::Bucket::SCP.new url.host, opts
190
+ end
@@ -0,0 +1 @@
1
+ require 'bfs/bucket/scp'
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+
3
+ sandbox = { host: '127.0.0.1', opts: { port: 7022, user: 'root', password: 'root' } }.freeze
4
+ run_spec = \
5
+ begin
6
+ Net::SCP.start sandbox[:host], nil, sandbox[:opts].merge(timeout: 1) do |scp|
7
+ scp.session.exec!('hostname')
8
+ end
9
+ true
10
+ rescue Net::SSH::Exception => e
11
+ warn "WARNING: unable to run #{File.basename __FILE__}: #{e.message}"
12
+ false
13
+ end
14
+
15
+ RSpec.describe BFS::Bucket::SCP, if: run_spec do
16
+ subject { described_class.new sandbox[:host], sandbox[:opts].merge(prefix: SecureRandom.uuid) }
17
+ after { subject.close }
18
+
19
+ it_behaves_like 'a bucket',
20
+ content_type: false,
21
+ metadata: false
22
+
23
+ it 'should resolve from URL' do
24
+ bucket = BFS.resolve('scp://root:root@127.0.0.1:7022')
25
+ expect(bucket).to be_instance_of(described_class)
26
+ end
27
+ end
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bfs-scp
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.1
5
+ platform: ruby
6
+ authors:
7
+ - Dimitrij Denissenko
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-09-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bfs
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 0.4.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 0.4.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: net-scp
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: https://github.com/bsm/bfs.rb
42
+ email: dimitrij@blacksquaremedia.com
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - bfs-scp.gemspec
48
+ - lib/bfs/bucket/scp.rb
49
+ - lib/bfs/scp.rb
50
+ - spec/bfs/bucket/scp_spec.rb
51
+ homepage: https://github.com/bsm/bfs.rb
52
+ licenses:
53
+ - Apache-2.0
54
+ metadata: {}
55
+ post_install_message:
56
+ rdoc_options: []
57
+ require_paths:
58
+ - lib
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: 2.4.0
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ requirements: []
70
+ rubygems_version: 3.0.3
71
+ signing_key:
72
+ specification_version: 4
73
+ summary: SCP/SSH adapter for bfs
74
+ test_files:
75
+ - spec/bfs/bucket/scp_spec.rb