activestorage-sftp 0.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 0c1cad83fd43e3324294dd2c0081f8b61a76c003
4
+ data.tar.gz: 99d47be472b87031a8d506d0630b35e45b5d1061
5
+ SHA512:
6
+ metadata.gz: 4e73d5a932d849c9a901a4065950ec1600fe1a2cb1446bd80f6f99239a3c59f4c53e81054eeb57f86f43e1f81897e60830d8cccd74abfa14380eaaa7b31781f8
7
+ data.tar.gz: 20732912bfbf3332da9a2264d9bd7179cc94dee9533be29f3c71402bc82afa4f25b69d67c948f34bb33cd1cc070f31653eac80ecb5026827375eb1c6b86a85a6
@@ -0,0 +1,8 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in activestorage-sftp.gemspec
6
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2019 treenewbee
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,62 @@
1
+ Remote DiskService through SFTP, for ActiveStorage.
2
+
3
+ ## Installation
4
+
5
+ Add this line to your application's Gemfile:
6
+
7
+ ```ruby
8
+ gem 'activestorage-sftp'
9
+ ```
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install activestorage-sftp
18
+
19
+ ## Usage
20
+
21
+
22
+ Each application server saves blobs to file server through SFTP:
23
+ ```yml
24
+ # config/storage.yml
25
+ sftp:
26
+ service: SFTP
27
+ user: user
28
+ root: /var/www/proj/shared/storage
29
+ host: file.intranet
30
+ public_host: https://file.internet
31
+ ```
32
+ File server serves blobs using DiskService:
33
+ ```yml
34
+ # config/storage.yml
35
+ local:
36
+ service: Disk
37
+ root: <%= Rails.root.join("storage") %>
38
+ ```
39
+
40
+ Or use it as backup for your primary service:
41
+ ```yml
42
+ # config/storage.yml
43
+ mirrored:
44
+ service: Mirror
45
+ primary: local #/S3/AzureStorage/GCS
46
+ mirrors:
47
+ - sftp
48
+ sftp:
49
+ service: SFTP
50
+ user: user
51
+ root: /etc/backup/proj
52
+ host: secure.backup
53
+ ```
54
+
55
+
56
+ ## Contributing
57
+
58
+ Bug reports and pull requests are welcome on GitHub at https://github.com/treenewbee/activestorage-sftp.
59
+
60
+ ## License
61
+
62
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
@@ -0,0 +1,31 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "active_storage/sftp/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "activestorage-sftp"
8
+ spec.version = ActiveStorage::SFTP::VERSION
9
+ spec.authors = ["treenewbee"]
10
+ spec.email = ["yangguchen@gmail.com"]
11
+
12
+ spec.summary = %q{SFTP Service for ActiveStorage}
13
+ spec.description = %q{SFTP Service for ActiveStorage}
14
+ spec.homepage = "https://github.com/treenewbee/activestorage-sftp"
15
+ spec.license = "MIT"
16
+
17
+ # Specify which files should be added to the gem when it is released.
18
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
19
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
20
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
21
+ end
22
+ spec.bindir = "exe"
23
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
+ spec.require_paths = ["lib"]
25
+
26
+ spec.add_dependency "rails", "> 5.2.0"
27
+ spec.add_dependency "net-sftp", "~> 2.1.2"
28
+
29
+ spec.add_development_dependency "bundler", "~> 1.17"
30
+ spec.add_development_dependency "rake", "~> 10.0"
31
+ end
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "activestorage/sftp"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,229 @@
1
+ require "net/sftp"
2
+ require "digest/md5"
3
+ require "active_support/core_ext/numeric/bytes"
4
+
5
+ module ActiveStorage
6
+ # Wraps a remote path as an Active Storage service. See ActiveStorage::Service for the generic API
7
+ # documentation that applies to all services.
8
+ class Service::SFTPService < Service
9
+
10
+ MAX_CHUNK_SIZE = 64.kilobytes.freeze
11
+
12
+ attr_reader :host, :user, :root, :public_host
13
+
14
+ def initialize(host:, user:, public_host: nil, root: './')
15
+ @host = host
16
+ @user = user
17
+ @root = root
18
+ @public_host = public_host
19
+ end
20
+
21
+ def upload(key, io, checksum: nil, **)
22
+ instrument :upload, key: key, checksum: checksum do
23
+ ensure_integrity_of(io, checksum) if checksum
24
+ mkdir_for(key)
25
+ through_sftp do |sftp|
26
+ sftp.upload!(io.path, path_for(key))
27
+ end
28
+ end
29
+ end
30
+
31
+ def download(key, chunk_size: MAX_CHUNK_SIZE, &block)
32
+ if chunk_size > MAX_CHUNK_SIZE
33
+ # TODO Error
34
+ raise "Maximum chunk size: #{MAX_CHUNK_SIZE}"
35
+ end
36
+ if block_given?
37
+ instrument :streaming_download, key: key do
38
+ through_sftp do |sftp|
39
+ file = sftp.open!(path_for(key))
40
+ buf = StringIO.new
41
+ pos = 0
42
+ eof = false
43
+ until eof do
44
+ request = sftp.read(file, pos, chunk_size) do |response|
45
+ if response.eof?
46
+ eof = true
47
+ elsif !response.ok?
48
+ # TODO Error
49
+ raise "SFTP Response Error"
50
+ else
51
+ chunk = response[:data]
52
+ block.call(chunk)
53
+ buf << chunk
54
+ pos += chunk.size
55
+ end
56
+ end
57
+ request.wait
58
+ end
59
+ sftp.close(file)
60
+ buf.string
61
+ end
62
+ end
63
+ else
64
+ instrument :download, key: key do
65
+ io = StringIO.new
66
+ through_sftp do |sftp|
67
+ sftp.download!(path_for(key), io)
68
+ end
69
+ io.string
70
+ rescue Errno::ENOENT
71
+ raise ActiveStorage::FileNotFoundError
72
+ end
73
+ end
74
+ end
75
+
76
+ def download_chunk(key, range)
77
+ instrument :download_chunk, key: key, range: range do
78
+ if range.size > MAX_CHUNK_SIZE
79
+ # TODO Error
80
+ raise "Maximun chunk size: #{MAX_CHUNK_SIZE}"
81
+ end
82
+ chunk = StringIo.new
83
+ through_sftp do |sftp|
84
+ sftp.open(path_for(key)) do |file|
85
+ chunk << sftp.read(file, range.begin, ranage.size).response[:data]
86
+ end
87
+ end
88
+ chunk.string
89
+ rescue Errno::ENOENT
90
+ raise ActiveStorage::FileNotFoundError
91
+ end
92
+ end
93
+
94
+ def delete(key)
95
+ instrument :delete, key: key do
96
+ through_sftp do |sftp|
97
+ sftp.remove!(path_for(key))
98
+ end
99
+ rescue Net::SFTP::StatusException
100
+ # Ignore files already deleted
101
+ end
102
+ end
103
+
104
+ def delete_prefixed(prefix)
105
+ instrument :delete_prefixed, prefix: prefix do
106
+ through_sftp do |sftp|
107
+ sftp.dir.glob(root, "#{prefix}*") do |entry|
108
+ begin
109
+ sftp.remove!(entry.path)
110
+ rescue Net::SFTP::StatusException
111
+ # Ignore files already deleted
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+
118
+ def exist?(key)
119
+ instrument :exist, key: key do |payload|
120
+ answer = false
121
+ through_sftp do |sftp|
122
+ answer = sftp.stat(path_for(key)).present?
123
+ end
124
+ payload[:exist] = answer
125
+ answer
126
+ end
127
+ end
128
+
129
+ def url(key, expires_in:, filename:, disposition:, content_type:)
130
+ instrument :url, key: key do |payload|
131
+ raise "public_host not defined." unless public_host
132
+ content_disposition = content_disposition_with(type: disposition, filename: filename)
133
+ verified_key_with_expiration = ActiveStorage.verifier.generate(
134
+ {
135
+ key: key,
136
+ disposition: content_disposition,
137
+ content_type: content_type
138
+ },
139
+ { expires_in: expires_in,
140
+ purpose: :blob_key }
141
+ )
142
+
143
+ generated_url = url_helpers.rails_disk_service_url(verified_key_with_expiration,
144
+ host: public_host,
145
+ disposition: content_disposition,
146
+ content_type: content_type,
147
+ filename: filename
148
+ )
149
+ payload[:url] = generated_url
150
+
151
+ generated_url
152
+ end
153
+ end
154
+
155
+ def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
156
+ instrument :url, key: key do |payload|
157
+ verified_token_with_expiration = ActiveStorage.verifier.generate(
158
+ {
159
+ key: key,
160
+ content_type: content_type,
161
+ content_length: content_length,
162
+ checksum: checksum
163
+ },
164
+ { expires_in: expires_in,
165
+ purpose: :blob_token }
166
+ )
167
+
168
+ generated_url = url_helpers.update_rails_disk_service_url(verified_token_with_expiration,
169
+ host: public_host
170
+ )
171
+
172
+ payload[:url] = generated_url
173
+
174
+ generated_url
175
+ end
176
+ end
177
+
178
+ def headers_for_direct_upload(key, content_type:, **)
179
+ { "Content-Type" => content_type }
180
+ end
181
+
182
+ protected
183
+ def through_sftp(&block)
184
+ Net::SFTP.start(@host, @user) do |sftp|
185
+ block.call(sftp)
186
+ end
187
+ end
188
+
189
+ def path_for(key)
190
+ File.join folder_for(key), key
191
+ end
192
+
193
+ def folder_for(key)
194
+ File.join root, relative_folder_for(key)
195
+ end
196
+
197
+ def relative_folder_for(key)
198
+ [ key[0..1], key[2..3] ].join("/")
199
+ end
200
+
201
+ def mkdir_for(key)
202
+ through_sftp do |sftp|
203
+ sub_folder = File.join root, key[0..1]
204
+ begin
205
+ sftp.opendir!(sub_folder)
206
+ rescue => e
207
+ sftp.mkdir!(sub_folder)
208
+ end
209
+ sub_folder = File.join(sub_folder, key[2..3])
210
+ begin
211
+ sftp.opendir!(sub_folder)
212
+ rescue => e
213
+ sftp.mkdir!(sub_folder)
214
+ end
215
+ end
216
+ end
217
+
218
+ def ensure_integrity_of(io, checksum)
219
+ unless Digest::MD5.new.update(io.read).base64digest == checksum
220
+ delete key
221
+ raise ActiveStorage::IntegrityError
222
+ end
223
+ end
224
+
225
+ def url_helpers
226
+ @url_helpers ||= Rails.application.routes.url_helpers
227
+ end
228
+ end
229
+ end
@@ -0,0 +1,9 @@
1
+ require "active_storage/sftp/version"
2
+ require "active_storage/service/sftp_service"
3
+
4
+ module ActiveStorage
5
+ module SFTP
6
+ class Error < StandardError; end
7
+ # Your code goes here...
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ module ActiveStorage
2
+ module SFTP
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
metadata ADDED
@@ -0,0 +1,111 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: activestorage-sftp
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - treenewbee
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2019-08-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">"
18
+ - !ruby/object:Gem::Version
19
+ version: 5.2.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">"
25
+ - !ruby/object:Gem::Version
26
+ version: 5.2.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: net-sftp
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 2.1.2
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 2.1.2
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.17'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.17'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ description: SFTP Service for ActiveStorage
70
+ email:
71
+ - yangguchen@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".gitignore"
77
+ - Gemfile
78
+ - LICENSE.txt
79
+ - README.md
80
+ - Rakefile
81
+ - activestorage-sftp.gemspec
82
+ - bin/console
83
+ - bin/setup
84
+ - lib/active_storage/service/sftp_service.rb
85
+ - lib/active_storage/sftp.rb
86
+ - lib/active_storage/sftp/version.rb
87
+ homepage: https://github.com/treenewbee/activestorage-sftp
88
+ licenses:
89
+ - MIT
90
+ metadata: {}
91
+ post_install_message:
92
+ rdoc_options: []
93
+ require_paths:
94
+ - lib
95
+ required_ruby_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ required_rubygems_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ requirements: []
106
+ rubyforge_project:
107
+ rubygems_version: 2.6.14.4
108
+ signing_key:
109
+ specification_version: 4
110
+ summary: SFTP Service for ActiveStorage
111
+ test_files: []