class ActiveStorage::Service::SFTPService

Wraps a remote path as an Active Storage service. See ActiveStorage::Service for the generic API documentation that applies to all services.

Constants

MAX_CHUNK_SIZE

Attributes

host[R]
public_host[R]
public_root[R]
root[R]
user[R]

Public Class Methods

new(host:, user:, public_host: nil, root: './', public_root: './', password: nil, simple_public_urls: false, verify_via_http_get: false) click to toggle source
# File lib/active_storage/service/sftp_service.rb, line 14
def initialize(host:, user:, public_host: nil, root: './', public_root: './', password: nil, simple_public_urls: false, verify_via_http_get: false)
  @host = host
  @user = user
  @root = root
  @public_host = public_host
  @public_root = public_root
  @password = password
  @simple_public_urls = simple_public_urls
  @verify_via_http_get = verify_via_http_get
end

Public Instance Methods

classic_url(key, expires_in:, filename:, disposition:, content_type:) click to toggle source
# File lib/active_storage/service/sftp_service.rb, line 159
def classic_url(key, expires_in:, filename:, disposition:, content_type:)
  instrument :url, key: key do |payload|
    raise NotConfigured, "public_host not defined." unless public_host
    content_disposition = content_disposition_with(type: disposition, filename: filename)
    verified_key_with_expiration = ActiveStorage.verifier.generate(
      {
        key: key,
        disposition: content_disposition,
        content_type: content_type
      },
      {
        expires_in: expires_in,
        purpose: :blob_key
      }
    )

    generated_url = url_helpers.rails_disk_service_url(verified_key_with_expiration,
                                                       host: public_host,
                                                       disposition: content_disposition,
                                                       content_type: content_type,
                                                       filename: filename
    )
    payload[:url] = generated_url
    generated_url
  end
end
delete(key) click to toggle source
# File lib/active_storage/service/sftp_service.rb, line 95
def delete(key)
  instrument :delete, key: key do
    through_sftp do |sftp|
      sftp.remove!(path_for(key))
    end
  rescue Net::SFTP::StatusException
    # Ignore files already deleted
  end
end
delete_prefixed(prefix) click to toggle source
# File lib/active_storage/service/sftp_service.rb, line 105
def delete_prefixed(prefix)
  instrument :delete_prefixed, prefix: prefix do
    through_sftp do |sftp|
      sftp.dir.glob(root, "#{prefix}*") do |entry|
        begin
          sftp.remove!(entry.path)
        rescue Net::SFTP::StatusException
          # Ignore files already deleted
        end
      end
    end
  end
end
download(key, chunk_size: MAX_CHUNK_SIZE, &block) click to toggle source
# File lib/active_storage/service/sftp_service.rb, line 35
def download(key, chunk_size: MAX_CHUNK_SIZE, &block)
  if chunk_size > MAX_CHUNK_SIZE
    raise ChunkSizeError, "Maximum chunk size: #{MAX_CHUNK_SIZE}"
  end
  if block_given?
    instrument :streaming_download, key: key do
      through_sftp do |sftp|
        file = sftp.open!(path_for(key))
        buf = StringIO.new
        pos = 0
        eof = false
        until eof do
          request = sftp.read(file, pos, chunk_size) do |response|
            if response.eof?
              eof = true
            elsif !response.ok?
              raise SFTPResponseError, response.code
            else
              chunk = response[:data]
              block.call(chunk)
              buf << chunk
              pos += chunk.size
            end
          end
          request.wait
        end
        sftp.close(file)
        buf.string
      end
    end
  else
    instrument :download, key: key do
      io = StringIO.new
      through_sftp do |sftp|
        sftp.download!(path_for(key), io)
      end
      io.string
    rescue Errno::ENOENT
      raise ActiveStorage::FileNotFoundError
    end
  end
end
download_chunk(key, range) click to toggle source
# File lib/active_storage/service/sftp_service.rb, line 78
def download_chunk(key, range)
  instrument :download_chunk, key: key, range: range do
    if range.size > MAX_CHUNK_SIZE
      raise ChunkSizeError, "Maximum chunk size: #{MAX_CHUNK_SIZE}"
    end
    chunk = StringIo.new
    through_sftp do |sftp|
      sftp.open(path_for(key)) do |file|
        chunk << sftp.read(file, range.begin, ranage.size).response[:data]
      end
    end
    chunk.string
  rescue Errno::ENOENT
    raise ActiveStorage::FileNotFoundError
  end
end
exist?(key) click to toggle source
# File lib/active_storage/service/sftp_service.rb, line 119
def exist?(key)
  instrument :exist, key: key do |payload|
    answer = false

    if @verify_via_http_get
      uri = URI([public_host, relative_folder_for(key), key].join('/'))
      request = Net::HTTP.new uri.host
      response = request.request_head uri.path

      answer = (response.code.to_i == 200)
    else
      through_sftp do |sftp|
        # TODO Probably adviseable to let some more exceptions go through
        begin
          sftp.stat!(path_for(key)) do |response|
            answer = response.ok?
          end
        rescue Net::SFTP::StatusException => e
          answer = false
        end
      end
    end

    payload[:exist] = answer
    answer
  end
end
headers_for_direct_upload(key, content_type:, **) click to toggle source
# File lib/active_storage/service/sftp_service.rb, line 218
def headers_for_direct_upload(key, content_type:, **)
  { "Content-Type" => content_type }
end
path_for(key) click to toggle source
# File lib/active_storage/service/sftp_service.rb, line 222
def path_for(key)
  File.join folder_for(key), key
end
public_url(key) click to toggle source
# File lib/active_storage/service/sftp_service.rb, line 186
def public_url(key)
  instrument :url, key: key do |payload|
    raise NotConfigured, "public_host not defined." unless public_host
    generated_url = File.join(public_host, public_root, path_for(key), key)
    payload[:url] = generated_url
    generated_url
  end
end
upload(key, io, checksum: nil, **) click to toggle source
# File lib/active_storage/service/sftp_service.rb, line 25
def upload(key, io, checksum: nil, **)
  instrument :upload, key: key, checksum: checksum do
    ensure_integrity_of(io, checksum) if checksum
    mkdir_for(key)
    through_sftp do |sftp|
      sftp.upload!(io.path, path_for(key))
    end
  end
end
url(key, expires_in:, filename:, disposition:, content_type:) click to toggle source
# File lib/active_storage/service/sftp_service.rb, line 147
def url(key, expires_in:, filename:, disposition:, content_type:)
  if @simple_public_urls
    public_url(key)
  else
    classic_url(key,
                expires_in: expires_in,
                filename: filename,
                disposition: disposition,
                content_type: content_type)
  end
end
url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:) click to toggle source
# File lib/active_storage/service/sftp_service.rb, line 195
def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
  instrument :url, key: key do |payload|
    verified_token_with_expiration = ActiveStorage.verifier.generate(
        {
            key: key,
            content_type: content_type,
            content_length: content_length,
            checksum: checksum
        },
        { expires_in: expires_in,
          purpose: :blob_token }
    )

    generated_url = url_helpers.update_rails_disk_service_url(verified_token_with_expiration,
                                                              host: public_host
    )

    payload[:url] = generated_url

    generated_url
  end
end

Protected Instance Methods

ensure_integrity_of(io, checksum) click to toggle source
# File lib/active_storage/service/sftp_service.rb, line 261
def ensure_integrity_of(io, checksum)
  unless Digest::MD5.new.update(io.read).base64digest == checksum
    delete key
    raise ActiveStorage::IntegrityError
  end
end
folder_for(key) click to toggle source
# File lib/active_storage/service/sftp_service.rb, line 234
def folder_for(key)
  File.join root, relative_folder_for(key)
end
mkdir_for(key) click to toggle source
# File lib/active_storage/service/sftp_service.rb, line 242
def mkdir_for(key)
  mkdir_p_for(path_for key)
end
mkdir_p_for(abs_path) click to toggle source
# File lib/active_storage/service/sftp_service.rb, line 246
def mkdir_p_for(abs_path)
  through_sftp do |sftp|
    base_path = ''
    abs_path.split('/')[0...-1].each do |path|
      sub_folder = File.join(base_path, path)
      begin
        sftp.opendir!(sub_folder)
      rescue => e
        sftp.mkdir!(sub_folder)
      end
      base_path = sub_folder
    end
  end
end
relative_folder_for(key) click to toggle source
# File lib/active_storage/service/sftp_service.rb, line 238
def relative_folder_for(key)
  [ key[0..1], key[2..3] ].join("/")
end
through_sftp(&block) click to toggle source
# File lib/active_storage/service/sftp_service.rb, line 227
def through_sftp(&block)
  opts = @password.present? ? {password: @password} : {}
  Net::SFTP.start(@host, @user, opts) do |sftp|
    block.call(sftp)
  end
end
url_helpers() click to toggle source
# File lib/active_storage/service/sftp_service.rb, line 268
def url_helpers
  @url_helpers ||= Rails.application.routes.url_helpers
end