lib/refile.rb in refile-0.5.5 vs lib/refile.rb in refile-0.6.0

- old
+ new

@@ -11,10 +11,16 @@ # @api private class InvalidID < Invalid; end # @api private + class InvalidMaxSize < Invalid; end + + # @api private + class InvalidFile < Invalid; end + + # @api private class Confirm < StandardError def message "are you sure? this will remove all files in the backend, call as \ `clear!(:confirm)` if you're sure you want to do this" end @@ -26,27 +32,70 @@ # value. # # @return [Refile::App, nil] attr_accessor :app + # The host name of a CDN distribution that the Rack application can be + # reached at. If not set, Refile will use an absolute URL without hostname. + # It is strongly recommended to run Refile behind a CDN and to set this to + # the hostname of the CDN distribution. + # + # The `cdn_host` setting is used when retrieving files, but not when + # uploading new files, since uploads should normally not go through the + # CDN. + # + # A protocol relative URL is recommended for this value. + # + # @return [String, nil] + attr_accessor :cdn_host + # The host name that the Rack application can be reached at. If not set, - # Refile will use an absolute URL without hostname. It is strongly - # recommended to run Refile behind a CDN and to set this to the hostname of - # the CDN distribution. A protocol relative URL is recommended for this - # value. + # Refile will use an absolute URL without hostname. You should only change + # this setting if you are running the Refile app on a different domain + # than your main application. # + # If you are simply running the Refile app behind a CDN you'll want to + # change {Refile.cdn_host} instead. + # + # The difference between {Refile.app_host} and {Refile.cdn_host} is that the + # latter only affects URLs generated by {Refile.file_url} and the + # {Refile::AttachmentHelper#attachment_url} and + # {Refile::AttachmentHelper#attachment_image_tag} helpers, whereas the + # former also affects {Refile.upload_url}, {Refile.presign_url} and the + # {Refile::AttachmentHelper#attachment_field} helper. + # # @return [String, nil] - attr_accessor :host + attr_accessor :app_host + # @deprecated use {Refile.cdn_host} instead + def host + warn "Refile.host is deprecated, please use Refile.cdn_host instead" + cdn_host + end + + # @deprecated use {Refile.cdn_host} instead + def host=(host) + warn "Refile.host is deprecated, please use Refile.cdn_host instead" + self.cdn_host = host + end + # A list of names which identify backends in the global backend registry. # The Rack application allows POST requests to only the backends specified # in this config option. This defaults to `["cache"]`, only allowing direct # uploads to the cache backend. # - # @return [Array[String]] - attr_accessor :direct_upload + # @return [Array[String], :all] + attr_accessor :allow_uploads_to + # A list of names which identify backends in the global backend registry. + # The Rack application allows GET requests to only the backends specified + # in this config option. This defaults to `:all`, allowing files from all + # backends to be downloaded. + # + # @return [Array[String], :all] + attr_accessor :allow_downloads_from + # Logger that should be used by rack application # # @return [Logger] attr_accessor :logger @@ -73,10 +122,15 @@ # The default is true. # # @return [Boolean] attr_accessor :automount + # Value for generating signed attachment urls to protect from DoS + # + # @return [String] + attr_accessor :secret_key + # A global registry of backends. # # @return [Hash{String => Backend}] def backends @backends ||= {} @@ -105,11 +159,12 @@ # this method which also receives and returns an IO-like object. # # An IO-like object is recommended to be an instance of the `IO` class or # one of its subclasses, like `File` or a `StringIO`, or a `Refile::File`. # It can also be any other object which responds to `size`, `read`, `eof`? - # and `close` and mimics the behaviour of IO objects for these methods. + # `rewind` and `close` and mimics the behaviour of IO objects for these + # methods. # # @example With processor class # class Reverse # def call(file) # StringIO.new(file.read.reverse) @@ -199,78 +254,248 @@ content_type.to_s if content_type end end end - # Generate a URL to an attachment. This method receives an instance of a - # class which has used the {Refile::Attachment#attachment} macro to - # generate an attachment column, and the name of this column, and based on - # this generates a URL to a {Refile::App}. + # Generates a URL to the Refile application. # + # The host defaults to {Refile.app_host}. You can also override the host via + # the `host` option. Normally the Refile app will not be mounted at the + # root but rather at some other path, the `prefix` option allows you to + # override this setting, and if not set it will fall back to + # {Refile.mount_point}. + # + # @example + # Refilee.app_url + # + # @example With host and prefix + # Refilee.app_url(host: "http://some.domain", prefix: "/refile") + # + # @param [String, nil] host Override the host + # @param [String, nil] prefix Adds a prefix to the URL if the application is not mounted at root + # @return [String] The generated URL + def app_url(host: nil, prefix: nil) + host ||= Refile.app_host + prefix ||= Refile.mount_point + + uri = URI(host.to_s) + uri.path = prefix || "/" + uri.to_s + end + + # Receives a {Refile::File} and generates a URL to it. + # # Optionally the name of a processor and arguments to it can be appended. # + # The `filename` option must be given. + # + # The host defaults to {Refile.cdn_host}, which is useful for serving all + # attachments from a CDN. You can also override the host via the `host` + # option. + # + # Returns `nil` if the supplied file is `nil`. + # + # @example + # Refile.file_url(Refile.store.get(id)) + # + # @example With processor + # Refile.file_url(Refile.store.get(id), :image, :fill, 300, 300, format: "jpg") + # + # @param [Refile::File] file The file to generate a URL for + # @param [String] filename The filename to be appended to the URL + # @param [String, nil] format A file extension to be appended to the URL + # @param [String, nil] host Override the host + # @param [String, nil] prefix Adds a prefix to the URL if the application is not mounted at root + # @return [String, nil] The generated URL + def file_url(file, *args, host: nil, prefix: nil, filename:, format: nil) + return unless file + + host ||= Refile.cdn_host + backend_name = Refile.backends.key(file.backend) + + filename = Rack::Utils.escape(filename) + filename << "." << format.to_s if format + + base_path = ::File.join("", backend_name, *args.map(&:to_s), file.id.to_s, filename) + + ::File.join(app_url(prefix: prefix, host: host), token(base_path), base_path) + end + + # Receives a Refile backend and returns a URL to the Refile application + # where files can be uploaded. + # + # @example + # Refile.upload_url(Refile.store) + # + # @param [Refile::Backend] backend The backend to generate a URL for + # @param [String, nil] host Override the host + # @param [String, nil] prefix Adds a prefix to the URL if the application is not mounted at root + # @return [String] The generated URL + def upload_url(backend, host: nil, prefix: nil) + backend_name = Refile.backends.key(backend) + + ::File.join(app_url(host: host, prefix: prefix), backend_name) + end + + # Receives a Refile backend and returns a URL to the Refile application + # where a presign object for the backend can be retrieved. + # + # @example + # Refile.upload_url(Refile.store) + # + # @param [Refile::Backend] backend The backend to generate a URL for + # @param [String, nil] host Override the host + # @param [String, nil] prefix Adds a prefix to the URL if the application is not mounted at root + # @return [String] The generated URL + def presign_url(backend, host: nil, prefix: nil) + ::File.join(upload_url(backend, host: host, prefix: prefix), "presign") + end + + # Generate a URL to an attachment. Receives an instance of a class which + # has used the {Refile::Attachment#attachment} macro to generate an + # attachment column, and the name of this column, and based on this + # generates a URL to a {Refile::App}. + # + # Optionally the name of a processor and arguments to it can be appended. + # # If the filename option is not given, the filename is taken from the # metadata stored in the attachment, or eventually falls back to the # `name`. # - # The host defaults to {Refile.host}, which is useful for serving all + # The host defaults to {Refile.cdn_host}, which is useful for serving all # attachments from a CDN. You can also override the host via the `host` # option. # # Returns `nil` if there is no file attached. # # @example - # attachment_url(@post, :document) + # Refile.attachment_url(@post, :document) # # @example With processor - # attachment_url(@post, :image, :fill, 300, 300, format: "jpg") + # Refile.attachment_url(@post, :image, :fill, 300, 300, format: "jpg") # # @param [Refile::Attachment] object Instance of a class which has an attached file # @param [Symbol] name The name of the attachment column # @param [String, nil] filename The filename to be appended to the URL # @param [String, nil] format A file extension to be appended to the URL # @param [String, nil] host Override the host # @param [String, nil] prefix Adds a prefix to the URL if the application is not mounted at root # @return [String, nil] The generated URL - def attachment_url(object, name, *args, prefix: nil, filename: nil, format: nil, host: nil) + def attachment_url(object, name, *args, host: nil, prefix: nil, filename: nil, format: nil) attacher = object.send(:"#{name}_attacher") file = attacher.get return unless file - host ||= Refile.host - prefix ||= Refile.mount_point filename ||= attacher.basename || name.to_s format ||= attacher.extension - backend_name = Refile.backends.key(file.backend) + file_url(file, *args, host: host, prefix: prefix, filename: filename, format: format) + end - filename = Rack::Utils.escape(filename) - filename << "." << format.to_s if format + # Receives an instance of a class which has used the + # {Refile::Attachment#attachment} macro to generate an attachment column, + # and the name of this column, and based on this generates a URL to a + # {Refile::App} where files can be uploaded. + # + # @example + # Refile.attachment_upload_url(@post, :document) + # + # @param [Refile::Attachment] object Instance of a class which has an attached file + # @param [Symbol] name The name of the attachment column + # @param [String, nil] host Override the host + # @param [String, nil] prefix Adds a prefix to the URL if the application is not mounted at root + # @return [String] The generated URL + def attachment_upload_url(object, name, host: nil, prefix: nil) + backend = object.send(:"#{name}_attachment_definition").cache - uri = URI(host.to_s) - uri.path = ::File.join("", *prefix, backend_name, *args.map(&:to_s), file.id.to_s, filename) - uri.to_s + upload_url(backend, host: host, prefix: prefix) end + + # Receives an instance of a class which has used the + # {Refile::Attachment#attachment} macro to generate an attachment column, + # and the name of this column, and based on this generates a URL to a + # {Refile::App} where a presign object for the backend can be retrieved. + # + # @example + # Refile.attachment_presign_url(@post, :document) + # + # @param [Refile::Attachment] object Instance of a class which has an attached file + # @param [Symbol] name The name of the attachment column + # @param [String, nil] host Override the host + # @param [String, nil] prefix Adds a prefix to the URL if the application is not mounted at root + # @return [String] The generated URL + def attachment_presign_url(object, name, host: nil, prefix: nil) + backend = object.send(:"#{name}_attachment_definition").cache + + presign_url(backend, host: host, prefix: prefix) + end + + # Generate a signature for a given path concatenated with the configured secret token. + # + # Raises an error if no secret token is configured. + # + # @example + # Refile.token('/store/f5f2e4/document.pdf') + # + # @param [String] path The path to generate a token for + # @raise [RuntimeError] If {Refile.secret_key} is not set + # @return [String, nil] The generated token + def token(path) + if secret_key.nil? + error = "Refile.secret_key was not set.\n\n" + error << "Please add the following to your Refile configuration and restart your application:\n\n" + error << "```\nRefile.secret_key = '#{SecureRandom.hex(64)}'\n```\n\n" + + raise error + end + + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha1"), secret_key, path) + end + + # Check if the given token is a valid token for the given path. + # + # @example + # Refile.valid_token?('/store/f5f2e4/document.pdf', 'abcd1234') + # + # @param [String] path The path to check validity for + # @param [String] token The token to check + # @raise [RuntimeError] If {Refile.secret_key} is not set + # @return [Boolean] Whether the token is valid + def valid_token?(path, token) + expected = Digest::SHA1.hexdigest(token(path)) + actual = Digest::SHA1.hexdigest(token) + + expected == actual + end + + # @api private + def parse_json(data, *args) + JSON.parse(data.to_s, *args) + rescue JSON::ParserError + nil + end end require "refile/version" require "refile/signature" require "refile/type" require "refile/backend_macros" + require "refile/attachment_definition" require "refile/attacher" require "refile/attachment" require "refile/random_hasher" require "refile/file" require "refile/custom_logger" require "refile/app" require "refile/backend/file_system" end Refile.configure do |config| - config.direct_upload = ["cache"] + config.allow_uploads_to = ["cache"] + config.allow_downloads_from = :all config.allow_origin = "*" - config.logger = Logger.new(STDOUT) - config.mount_point = "attachments" + config.logger = Logger.new(STDOUT) unless ENV["RACK_ENV"] == "test" + config.mount_point = "/attachments" config.automount = true config.content_max_age = 60 * 60 * 24 * 365 config.types[:image] = Refile::Type.new(:image, content_type: %w[image/jpeg image/gif image/png]) end