leifcr-refile 0.6.3
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/app/assets/javascripts/refile.js +125 -0
- data/config/locales/en.yml +10 -0
- data/config/routes.rb +5 -0
- data/lib/refile.rb +510 -0
- data/lib/refile/app.rb +186 -0
- data/lib/refile/attacher.rb +190 -0
- data/lib/refile/attachment.rb +108 -0
- data/lib/refile/attachment/active_record.rb +133 -0
- data/lib/refile/attachment_definition.rb +83 -0
- data/lib/refile/backend/file_system.rb +120 -0
- data/lib/refile/backend/s3.rb +1 -0
- data/lib/refile/backend_macros.rb +45 -0
- data/lib/refile/custom_logger.rb +48 -0
- data/lib/refile/file.rb +102 -0
- data/lib/refile/file_double.rb +13 -0
- data/lib/refile/image_processing.rb +1 -0
- data/lib/refile/rails.rb +54 -0
- data/lib/refile/rails/attachment_helper.rb +121 -0
- data/lib/refile/random_hasher.rb +11 -0
- data/lib/refile/signature.rb +36 -0
- data/lib/refile/simple_form.rb +17 -0
- data/lib/refile/type.rb +28 -0
- data/lib/refile/version.rb +3 -0
- data/spec/refile/active_record_helper.rb +35 -0
- data/spec/refile/app_spec.rb +424 -0
- data/spec/refile/attachment/active_record_spec.rb +568 -0
- data/spec/refile/attachment_helper_spec.rb +78 -0
- data/spec/refile/attachment_spec.rb +589 -0
- data/spec/refile/backend/file_system_spec.rb +5 -0
- data/spec/refile/backend_examples.rb +228 -0
- data/spec/refile/backend_macros_spec.rb +83 -0
- data/spec/refile/custom_logger_spec.rb +21 -0
- data/spec/refile/features/direct_upload_spec.rb +63 -0
- data/spec/refile/features/multiple_upload_spec.rb +122 -0
- data/spec/refile/features/normal_upload_spec.rb +144 -0
- data/spec/refile/features/presigned_upload_spec.rb +31 -0
- data/spec/refile/features/simple_form_spec.rb +8 -0
- data/spec/refile/fixtures/hello.txt +1 -0
- data/spec/refile/fixtures/image.jpg +0 -0
- data/spec/refile/fixtures/large.txt +44 -0
- data/spec/refile/fixtures/monkey.txt +1 -0
- data/spec/refile/fixtures/world.txt +1 -0
- data/spec/refile/spec_helper.rb +72 -0
- data/spec/refile_spec.rb +355 -0
- metadata +143 -0
@@ -0,0 +1,133 @@
|
|
1
|
+
module Refile
|
2
|
+
module ActiveRecord
|
3
|
+
module Attachment
|
4
|
+
include Refile::Attachment
|
5
|
+
|
6
|
+
# Attachment method which hooks into ActiveRecord models
|
7
|
+
#
|
8
|
+
# @param [true, false] destroy Whether to remove the stored file if its model is destroyed
|
9
|
+
# @return [void]
|
10
|
+
# @see Refile::Attachment#attachment
|
11
|
+
def attachment(name, raise_errors: false, destroy: true, **options)
|
12
|
+
super(name, raise_errors: raise_errors, **options)
|
13
|
+
|
14
|
+
attacher = "#{name}_attacher"
|
15
|
+
|
16
|
+
validate do
|
17
|
+
if send(attacher).present?
|
18
|
+
send(attacher).valid?
|
19
|
+
errors = send(attacher).errors
|
20
|
+
errors.each do |error|
|
21
|
+
self.errors.add(name, *error)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
define_method "#{name}=" do |value|
|
27
|
+
send("#{name}_id_will_change!") if respond_to?("#{name}_id_will_change!")
|
28
|
+
super(value)
|
29
|
+
end
|
30
|
+
|
31
|
+
define_method "remove_#{name}=" do |value|
|
32
|
+
send("#{name}_id_will_change!")
|
33
|
+
super(value)
|
34
|
+
end
|
35
|
+
|
36
|
+
define_method "remote_#{name}_url=" do |value|
|
37
|
+
send("#{name}_id_will_change!")
|
38
|
+
super(value)
|
39
|
+
end
|
40
|
+
|
41
|
+
before_save do
|
42
|
+
send(attacher).store!
|
43
|
+
end
|
44
|
+
|
45
|
+
after_destroy do
|
46
|
+
send(attacher).delete! if destroy
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Macro which generates accessors for assigning multiple attachments at
|
51
|
+
# once. This is primarily useful together with multiple file uploads.
|
52
|
+
#
|
53
|
+
# The name of the generated accessors will be the name of the association
|
54
|
+
# and the name of the attachment in the associated model. So if a `Post`
|
55
|
+
# accepts attachments for `images`, and the attachment in the `Image`
|
56
|
+
# model is named `file`, then the accessors will be named `images_files`.
|
57
|
+
#
|
58
|
+
# @example in model
|
59
|
+
# class Post
|
60
|
+
# has_many :images, dependent: :destroy
|
61
|
+
# accepts_attachments_for :images
|
62
|
+
# end
|
63
|
+
#
|
64
|
+
# @example in associated model
|
65
|
+
# class Image
|
66
|
+
# attachment :image
|
67
|
+
# end
|
68
|
+
#
|
69
|
+
# @example in form
|
70
|
+
# <%= form_for @post do |form| %>
|
71
|
+
# <%= form.attachment_field :images_files, multiple: true %>
|
72
|
+
# <% end %>
|
73
|
+
#
|
74
|
+
# @param [Symbol] association_name Name of the association
|
75
|
+
# @param [Symbol] attachment Name of the attachment in the associated model
|
76
|
+
# @param [Symbol] append If true, new files are appended instead of replacing the entire list of associated models.
|
77
|
+
# @return [void]
|
78
|
+
def accepts_attachments_for(association_name, attachment: :file, append: false)
|
79
|
+
association = reflect_on_association(association_name)
|
80
|
+
attachment_pluralized = attachment.to_s.pluralize
|
81
|
+
name = "#{association_name}_#{attachment_pluralized}"
|
82
|
+
|
83
|
+
mod = Module.new do
|
84
|
+
define_method :"#{name}_attachment_definition" do
|
85
|
+
association.klass.send("#{attachment}_attachment_definition")
|
86
|
+
end
|
87
|
+
|
88
|
+
define_method(:method_missing) do |method|
|
89
|
+
if method == attachment_pluralized.to_sym
|
90
|
+
raise NoMethodError, "wrong association name #{method}, use like this #{name}"
|
91
|
+
else
|
92
|
+
super(method)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
define_method :"#{name}_data" do
|
97
|
+
if send(association_name).all? { |record| record.send("#{attachment}_attacher").valid? }
|
98
|
+
send(association_name).map(&:"#{attachment}_data").select(&:present?)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
define_method :"#{name}" do
|
103
|
+
send(association_name).map(&attachment)
|
104
|
+
end
|
105
|
+
|
106
|
+
define_method :"#{name}=" do |files|
|
107
|
+
cache, files = files.partition { |file| file.is_a?(String) }
|
108
|
+
|
109
|
+
cache = Refile.parse_json(cache.first)
|
110
|
+
|
111
|
+
if not append and (files.present? or cache.present?)
|
112
|
+
send("#{association_name}=", [])
|
113
|
+
end
|
114
|
+
|
115
|
+
if files.empty? and cache.present?
|
116
|
+
cache.select(&:present?).each do |file|
|
117
|
+
send(association_name).build(attachment => file.to_json)
|
118
|
+
end
|
119
|
+
else
|
120
|
+
files.select(&:present?).each do |file|
|
121
|
+
send(association_name).build(attachment => file)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
include mod
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
::ActiveRecord::Base.extend(Refile::ActiveRecord::Attachment)
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module Refile
|
2
|
+
# @api private
|
3
|
+
class AttachmentDefinition
|
4
|
+
attr_reader :record, :name, :cache, :store, :options, :type, :valid_content_types
|
5
|
+
attr_accessor :remove
|
6
|
+
|
7
|
+
def initialize(name, cache:, store:, raise_errors: true, type: nil, extension: nil, content_type: nil)
|
8
|
+
@name = name
|
9
|
+
@raise_errors = raise_errors
|
10
|
+
@cache_name = cache
|
11
|
+
@store_name = store
|
12
|
+
@type = type
|
13
|
+
@extension = extension
|
14
|
+
@valid_content_types = [content_type].flatten if content_type
|
15
|
+
@valid_content_types ||= Refile.types.fetch(type).content_type if type
|
16
|
+
end
|
17
|
+
|
18
|
+
def cache
|
19
|
+
Refile.backends.fetch(@cache_name.to_s)
|
20
|
+
end
|
21
|
+
|
22
|
+
def store
|
23
|
+
Refile.backends.fetch(@store_name.to_s)
|
24
|
+
end
|
25
|
+
|
26
|
+
def accept
|
27
|
+
if valid_content_types
|
28
|
+
valid_content_types.join(",")
|
29
|
+
elsif valid_extensions
|
30
|
+
valid_extensions.map { |e| ".#{e}" }.join(",")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def raise_errors?
|
35
|
+
@raise_errors
|
36
|
+
end
|
37
|
+
|
38
|
+
def valid_extensions
|
39
|
+
return unless @extension
|
40
|
+
if @extension.is_a?(Proc)
|
41
|
+
Array(@extension.call)
|
42
|
+
else
|
43
|
+
Array(@extension)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def validate(attacher)
|
48
|
+
extension = attacher.extension.to_s.downcase
|
49
|
+
content_type = attacher.content_type.to_s.downcase
|
50
|
+
content_type = content_type.split(";").first unless content_type.empty?
|
51
|
+
|
52
|
+
errors = []
|
53
|
+
errors << extension_error_params(extension) if invalid_extension?(extension)
|
54
|
+
errors << content_type_error_params(content_type) if invalid_content_type?(content_type)
|
55
|
+
errors << :too_large if cache.max_size and attacher.size and attacher.size >= cache.max_size
|
56
|
+
errors << :zero_byte_detected if attacher.size.to_i.zero?
|
57
|
+
errors
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def extension_error_params(extension)
|
63
|
+
[:invalid_extension, extension: format_param(extension), permitted: valid_extensions.to_sentence]
|
64
|
+
end
|
65
|
+
|
66
|
+
def content_type_error_params(content_type)
|
67
|
+
[:invalid_content_type, content: format_param(content_type), permitted: valid_content_types.to_sentence]
|
68
|
+
end
|
69
|
+
|
70
|
+
def invalid_extension?(extension)
|
71
|
+
extension_included = valid_extensions && valid_extensions.map(&:downcase).include?(extension)
|
72
|
+
valid_extensions and not extension_included
|
73
|
+
end
|
74
|
+
|
75
|
+
def invalid_content_type?(content_type)
|
76
|
+
valid_content_types and not valid_content_types.include?(content_type)
|
77
|
+
end
|
78
|
+
|
79
|
+
def format_param(param)
|
80
|
+
param.empty? ? I18n.t("refile.empty_param") : param
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module Refile
|
2
|
+
module Backend
|
3
|
+
# A backend which stores uploaded files in the local filesystem
|
4
|
+
#
|
5
|
+
# @example
|
6
|
+
# backend = Refile::Backend::FileSystem.new("some/path")
|
7
|
+
# file = backend.upload(StringIO.new("hello"))
|
8
|
+
# backend.read(file.id) # => "hello"
|
9
|
+
class FileSystem
|
10
|
+
extend Refile::BackendMacros
|
11
|
+
|
12
|
+
# @return [String] the directory where files are stored
|
13
|
+
attr_reader :directory
|
14
|
+
|
15
|
+
# @return [String] the maximum size of files stored in this backend
|
16
|
+
attr_reader :max_size
|
17
|
+
|
18
|
+
# Creates the given directory if it doesn't exist.
|
19
|
+
#
|
20
|
+
# @param [String] directory The path to a directory where files should be stored
|
21
|
+
# @param [Integer, nil] max_size The maximum size of an uploaded file
|
22
|
+
# @param [#hash] hasher A hasher which is used to generate ids from files
|
23
|
+
def initialize(directory, max_size: nil, hasher: Refile::RandomHasher.new)
|
24
|
+
@hasher = hasher
|
25
|
+
@directory = directory
|
26
|
+
@max_size = max_size
|
27
|
+
|
28
|
+
FileUtils.mkdir_p(@directory)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Upload a file into this backend
|
32
|
+
#
|
33
|
+
# @param [IO] uploadable An uploadable IO-like object.
|
34
|
+
# @return [Refile::File] The uploaded file
|
35
|
+
verify_uploadable def upload(uploadable)
|
36
|
+
id = @hasher.hash(uploadable)
|
37
|
+
IO.copy_stream(uploadable, path(id))
|
38
|
+
|
39
|
+
Refile::File.new(self, id)
|
40
|
+
ensure
|
41
|
+
uploadable.close
|
42
|
+
end
|
43
|
+
|
44
|
+
# Get a file from this backend.
|
45
|
+
#
|
46
|
+
# Note that this method will always return a {Refile::File} object, even
|
47
|
+
# if a file with the given id does not exist in this backend. Use
|
48
|
+
# {FileSystem#exists?} to check if the file actually exists.
|
49
|
+
#
|
50
|
+
# @param [String] id The id of the file
|
51
|
+
# @return [Refile::File] The retrieved file
|
52
|
+
verify_id def get(id)
|
53
|
+
Refile::File.new(self, id)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Delete a file from this backend
|
57
|
+
#
|
58
|
+
# @param [String] id The id of the file
|
59
|
+
# @return [void]
|
60
|
+
verify_id def delete(id)
|
61
|
+
FileUtils.rm(path(id)) if exists?(id)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Return an IO object for the uploaded file which can be used to read its
|
65
|
+
# content.
|
66
|
+
#
|
67
|
+
# @param [String] id The id of the file
|
68
|
+
# @return [IO] An IO object containing the file contents
|
69
|
+
verify_id def open(id)
|
70
|
+
::File.open(path(id), "rb")
|
71
|
+
end
|
72
|
+
|
73
|
+
# Return the entire contents of the uploaded file as a String.
|
74
|
+
#
|
75
|
+
# @param [String] id The id of the file
|
76
|
+
# @return [String] The file's contents
|
77
|
+
verify_id def read(id)
|
78
|
+
::File.read(path(id)) if exists?(id)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Return the size in bytes of the uploaded file.
|
82
|
+
#
|
83
|
+
# @param [String] id The id of the file
|
84
|
+
# @return [Integer] The file's size
|
85
|
+
verify_id def size(id)
|
86
|
+
::File.size(path(id)) if exists?(id)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Return whether the file with the given id exists in this backend.
|
90
|
+
#
|
91
|
+
# @param [String] id The id of the file
|
92
|
+
# @return [Boolean]
|
93
|
+
verify_id def exists?(id)
|
94
|
+
::File.exist?(path(id))
|
95
|
+
end
|
96
|
+
|
97
|
+
# Remove all files in this backend. You must confirm the deletion by
|
98
|
+
# passing the symbol `:confirm` as an argument to this method.
|
99
|
+
#
|
100
|
+
# @example
|
101
|
+
# backend.clear!(:confirm)
|
102
|
+
# @raise [Refile::Confirm] Unless the `:confirm` symbol has been passed.
|
103
|
+
# @param [:confirm] confirm Pass the symbol `:confirm` to confirm deletion.
|
104
|
+
# @return [void]
|
105
|
+
def clear!(confirm = nil)
|
106
|
+
raise Refile::Confirm unless confirm == :confirm
|
107
|
+
FileUtils.rm_rf(@directory)
|
108
|
+
FileUtils.mkdir_p(@directory)
|
109
|
+
end
|
110
|
+
|
111
|
+
# Return the full path of the uploaded file with the given id.
|
112
|
+
#
|
113
|
+
# @param [String] id The id of the file
|
114
|
+
# @return [String]
|
115
|
+
verify_id def path(id)
|
116
|
+
::File.join(@directory, id)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
raise "[Refile] the S3 backend has been extracted into a separate gem, see https://github.com/refile/refile-s3"
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Refile
|
2
|
+
# Macros which make it easier to write secure backends.
|
3
|
+
#
|
4
|
+
# @api private
|
5
|
+
module BackendMacros
|
6
|
+
def verify_id(method)
|
7
|
+
mod = Module.new do
|
8
|
+
define_method(method) do |id|
|
9
|
+
id = self.class.decode_id(id)
|
10
|
+
if self.class.valid_id?(id)
|
11
|
+
super(id)
|
12
|
+
else
|
13
|
+
raise Refile::InvalidID
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
prepend mod
|
18
|
+
end
|
19
|
+
|
20
|
+
def verify_uploadable(method)
|
21
|
+
mod = Module.new do
|
22
|
+
define_method(method) do |uploadable|
|
23
|
+
[:size, :read, :eof?, :rewind, :close].each do |m|
|
24
|
+
unless uploadable.respond_to?(m)
|
25
|
+
raise Refile::InvalidFile, "does not respond to `#{m}`."
|
26
|
+
end
|
27
|
+
end
|
28
|
+
if max_size and uploadable.size > max_size
|
29
|
+
raise Refile::InvalidMaxSize, "#{uploadable.inspect} is too large"
|
30
|
+
end
|
31
|
+
super(uploadable)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
prepend mod
|
35
|
+
end
|
36
|
+
|
37
|
+
def valid_id?(id)
|
38
|
+
id =~ /\A[a-z0-9]+\z/i
|
39
|
+
end
|
40
|
+
|
41
|
+
def decode_id(id)
|
42
|
+
id.to_s
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require "rack/body_proxy"
|
2
|
+
|
3
|
+
module Refile
|
4
|
+
# @api private
|
5
|
+
class CustomLogger
|
6
|
+
LOG_FORMAT = %(%s: [%s] %s "%s%s" %d %0.1fms\n)
|
7
|
+
|
8
|
+
def initialize(app, prefix, logger_proc)
|
9
|
+
@app = app
|
10
|
+
@prefix = prefix
|
11
|
+
@logger_proc = logger_proc
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(env)
|
15
|
+
began_at = Time.now
|
16
|
+
status, header, body = @app.call(env)
|
17
|
+
body = Rack::BodyProxy.new(body) { log(env, status, began_at) }
|
18
|
+
[status, header, body]
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def log(env, status, began_at)
|
24
|
+
now = Time.now
|
25
|
+
logger.info do
|
26
|
+
format(
|
27
|
+
LOG_FORMAT,
|
28
|
+
@prefix,
|
29
|
+
now.strftime("%F %T %z"),
|
30
|
+
env["REQUEST_METHOD"],
|
31
|
+
env["PATH_INFO"],
|
32
|
+
env["QUERY_STRING"].empty? ? "" : "?" + env["QUERY_STRING"],
|
33
|
+
status.to_s[0..3],
|
34
|
+
(now - began_at) * 1000
|
35
|
+
)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def logger
|
40
|
+
@logger ||= @logger_proc.call
|
41
|
+
@logger || fallback_logger
|
42
|
+
end
|
43
|
+
|
44
|
+
def fallback_logger
|
45
|
+
@fallback_logger ||= Logger.new(nil)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/refile/file.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
module Refile
|
2
|
+
class File
|
3
|
+
# @return [Backend] the backend the file is stored in
|
4
|
+
attr_reader :backend
|
5
|
+
|
6
|
+
# @return [String] the id of the file
|
7
|
+
attr_reader :id
|
8
|
+
|
9
|
+
# @api private
|
10
|
+
def initialize(backend, id)
|
11
|
+
@backend = backend
|
12
|
+
@id = id
|
13
|
+
end
|
14
|
+
|
15
|
+
# Reads from the file.
|
16
|
+
#
|
17
|
+
# @see http://www.ruby-doc.org/core-2.2.0/IO.html#method-i-read
|
18
|
+
#
|
19
|
+
# @return [String] The contents of the read chunk
|
20
|
+
def read(*args)
|
21
|
+
io.read(*args)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns whether there is more data to read. Returns true if the end of
|
25
|
+
# the data has been reached.
|
26
|
+
#
|
27
|
+
# @return [Boolean]
|
28
|
+
def eof?
|
29
|
+
io.eof?
|
30
|
+
end
|
31
|
+
|
32
|
+
# Close the file object and release its file descriptor.
|
33
|
+
#
|
34
|
+
# @return [void]
|
35
|
+
def close
|
36
|
+
io.close
|
37
|
+
end
|
38
|
+
|
39
|
+
# @return [Integer] the size of the file in bytes
|
40
|
+
def size
|
41
|
+
backend.size(id)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Remove the file from the backend.
|
45
|
+
#
|
46
|
+
# @return [void]
|
47
|
+
def delete
|
48
|
+
backend.delete(id)
|
49
|
+
end
|
50
|
+
|
51
|
+
# @return [Boolean] whether the file exists in the backend
|
52
|
+
def exists?
|
53
|
+
backend.exists?(id)
|
54
|
+
end
|
55
|
+
|
56
|
+
# @return [IO] an IO object which contains the contents of the file
|
57
|
+
def to_io
|
58
|
+
io
|
59
|
+
end
|
60
|
+
|
61
|
+
# Downloads the file to a Tempfile on disk and returns this tempfile.
|
62
|
+
#
|
63
|
+
# @example
|
64
|
+
# file = backend.upload(StringIO.new("hello"))
|
65
|
+
# tempfile = file.download
|
66
|
+
# File.read(tempfile.path) # => "hello"
|
67
|
+
#
|
68
|
+
# @return [Tempfile] a tempfile with the file's content
|
69
|
+
def download
|
70
|
+
return io if io.is_a?(Tempfile)
|
71
|
+
|
72
|
+
Tempfile.new(id, binmode: true).tap do |tempfile|
|
73
|
+
IO.copy_stream(io, tempfile)
|
74
|
+
tempfile.rewind
|
75
|
+
tempfile.fsync
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Rewind to beginning of file.
|
80
|
+
#
|
81
|
+
# @return [nil]
|
82
|
+
def rewind
|
83
|
+
@io = nil
|
84
|
+
end
|
85
|
+
|
86
|
+
# Prevent from exposing secure information unexpectedly
|
87
|
+
#
|
88
|
+
# @return [Hash]
|
89
|
+
def as_json
|
90
|
+
{
|
91
|
+
id: id,
|
92
|
+
backend: backend.to_s
|
93
|
+
}
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def io
|
99
|
+
@io ||= backend.open(id)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|