module CarrierWave # Takes care of the file upload for cards with attached files. # Most of the upload behaviour depends on the card itself. # (e.g. card type and storage option chosen for the card). So in contrary # to CarrierWave's default uploader we depend very much on the model # (= card object) to get the correct paths for retrieving and storing # the file. # # Cards that support attachments (by default those are cards of type "file" # and "image") accept a file handle as a card attribute. # # @example Attaching a file to a file card # Card.create name: "file card", type: :file, # file: File.new(path_to_file) # # @example Attaching a image to a image card # Card.create name: "file card", type: :image, # image: File.new(path_to_image) # # It's possible to upload files using a url. The card attribute for that is # remote__url # # @example Create a file card using a remote url # Card.create name: "file_card", type: :file, # remote_file_url: "http://a.file.in/the.web" # # @example Updating a image card using a remote url # card.update remote_image_url: "http://a.image/somewhere.png" # # ## Storage types # You can choose between four different storage options # - coded: These files are in the codebase, like the default logo. # Every view is a decko request. # - local: Uploaded files which are stored in a local upload directory # (upload path is configurable via config.paths["files"]). # If read permissions are set such that "Anyone" can read, then there is # a symlink from the public directory. Otherwise every view is a decko # request. # - cloud: You can configure buckets that refer to an external storage # service. Link is rendered as absolute url # - web: A fixed url (to external source). No upload or other file # processing. Link is just the saved url. # # Currently, there is no web interface that let's a user or administrator # choose a storage option for a specific card or set of cards. # There is only a global config option to set the storage type for all new # uploads (config.storage_type). On the *admin card it's possible to # update all existing file cards according to the current global config. # # Storage types for single cards can be changed by developers using # the card attributes "storage_type", "bucket", and "mod". # # @example Creating a hard-coded file # Card.create name: "file card", type_id: Card::FileID, # file: File.new(path), # storage_type: :coded, mod: "account" # # @example Moving a file to a cloud service # # my_deck/config/application.rb: # config.file_buckets = { # aws_bucket: { # provider: "fog/aws", # directory: "bucket-name", # subdirectory: "files", # credentials: { # provider: 'AWS' # required # aws_access_key_id: 'key' # required # aws_secret_access_key: 'secret-key' # required # public: true, # } # } # # # decko console or rake task: # card.update storage_type: :cloud, bucket: :aws_bucket # # @example Creating a file card with fixed external link # Card.create name: "file card", type_id: Card::FileID, # content: "http://animals.org/cat.png" # storage_type: :web # # Card.create name: "file card", type_id: Card::FileID, # file: "http://animals.org/cat.png" # storage_type: :web # # Depending on the storage type the uploader uses the following paths # and identifiers. # ### Identifier (stored in the database as db_content) # - coded: :codename/mod_name.ext # - local: ~card_id/action_id.ext # - cloud: (bucket)/card_id/action_id.ext # - web: http://url # # ### Storage path # - coded: # mod_dir/file/codename/type_code(-variant).ext (no colon on codename!) # - local: # files_dir/card_id/action_id(-variant).ext (no tilde on id!) # - cloud: # bucket/bucket_subdir/id/action_id(-variant).ext # - web: no storage # # Variants are only used for images. Possible options are # icon|small|medium|large|original. # files_dir, bucket, and bucket_subdir can be changed via config options. # # ### Supported url patterns # mark.ext # mark/revision.ext # mark/revision-variant.ext # /files/mark/revision-variant.ext # <- public symlink if readable by # # "Anyone" # # can be one of the following options # - # - ~ # - : # # is the mod name if the file is coded or and action_id in any # case # # Examples: # *logo.png # ~22/33-medium.png # local # :yeti_skin/standard-large.png # coded # class FileCardUploader < Uploader::Base attr_accessor :mod include Card::Env::Location include Path STORAGE_TYPES = %i[cloud web coded local].freeze CONFIG_OPTIONS = %i[provider attributes directory public credentials authenticated_url_expiration use_ssl_for_aws].freeze CONFIG_CREDENTIAL_OPTIONS = %i[ provider aws_access_key_id aws_secret_access_key region host endpoint google_access_key_id google_secret_access_key ].freeze delegate :store_dir, :retrieve_dir, :file_dir, :mod, :bucket, to: :model def valid? extension.present? end def filename if model.coded? "#{model.attachment_name}#{extension}" else "#{action_id}#{extension}" end end def extension case when file&.extension.present? then ".#{file.extension}" when card_content = model.content then File.extname(card_content) when orig = original_filename then File.extname(orig) else "" end.downcase end def db_content return model.content if model.web? return "" unless file.present? "%s/%s" % [file_dir, url_filename] end def url_filename if model.coded? "#{model.mod}#{extension}" else "#{action_id}#{extension}" end end # @option opts [Symbol] :absolute - return absolute url def url opts={} if model.cloud? file&.url elsif model.web? model.content else local_url opts end end def create_versions? new_file model.create_versions? new_file end def original_filename @original_filename ||= model.selected_action&.comment end def action_id model.selected_content_action_id || model.last_content_action_id || action_id_stand_in end # delegate carrierwave's fog config methods to bucket configuration CONFIG_OPTIONS.each do |name| define_method("fog_#{name}") { bucket_config name } end def bucket_config option @model.bucket_config[option] end def asset_host bucket_config(:asset_host) || super end private # used as action_id in the filename # if card is not #actionable? def action_id_stand_in @action_id_stand_in ||= Time.now.to_i end def storage case @model.storage_type when :cloud Storage::Fog.new self else Storage::File.new self end end end class SanitizedFile def content_type # the original content_type method doesn't seem to be very reliable # It uses mime_magic_content_type - which returns invalid/invalid for css files # that start with a comment - as the second option. (we switch the order and # use it as the third option) @content_type ||= existing_content_type || mini_mime_content_type || mime_magic_content_type end end module Uploader # Implements a different name pattern for versions than CarrierWave's # default: we expect the version name at the end of the filename separated # by a dash module Versions private # put version at the end of the filename def full_filename for_file name = super(for_file) parts = name.split "." basename = [parts.shift, version_name].compact.join("-") "#{basename}.#{parts.join('.')}" end end end end