bfs-scp 0.4.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 +7 -0
- data/bfs-scp.gemspec +22 -0
- data/lib/bfs/bucket/scp.rb +190 -0
- data/lib/bfs/scp.rb +1 -0
- data/spec/bfs/bucket/scp_spec.rb +27 -0
- metadata +75 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 2249f61d6b64e9816ec5a54ca7141ceff09bc9f290ddb7bdebd040b695e5a339
|
4
|
+
data.tar.gz: 81c282a260b33d36c24f906c0fee161cc5594ddef47ac2c35fbd1c9786df864e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 78f6577a9b1a5e8617291f73f705539a3a3496b54cc9ed0c1c300dd83b140c7d566803f66f1ad145cc635fda1fe946678dc1c8622a1300e773459aa592dec888
|
7
|
+
data.tar.gz: cc8b355b35164f3ec1dbcc3c4abcd2dbf4a9922e6bd89b2588546d0c449656b9f84122d3ae9165728238d8b64697d9c7e990ffb6c7c517fd888fbc8f1a06a718
|
data/bfs-scp.gemspec
ADDED
@@ -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
|
data/lib/bfs/scp.rb
ADDED
@@ -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.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Dimitrij Denissenko
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-09-18 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.2
|
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.2
|
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
|