Class: Kithe::Asset

Inherits:
Model
  • Object
show all
Defined in:
app/models/kithe/asset.rb

Defined Under Namespace

Classes: DerivativeCreator, DerivativeDefinition, DerivativeUpdater

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Model

#derivatives, #derivatives=, #friendlier_id, #set_leaf_representative, #to_param

Methods included from Indexable

auto_callbacks?, index_with, #update_index

Constructor Details

#initialize(*args) ⇒ Asset

Returns a new instance of Asset.



245
246
247
248
249
250
# File 'app/models/kithe/asset.rb', line 245

def initialize(*args)
  super
  if promotion_directives.present?
    file_attacher.set_promotion_directives(promotion_directives)
  end
end

Class Method Details

.define_derivative(key, storage_key: :kithe_derivatives, content_type: nil, default_create: true, &block) ⇒ Object

Establish a derivative definition that will be used to create a derivative when #create_derivatives is called, for instance automatically after promotion.

The most basic definition consists of a derivative key, and a ruby block that takes the original file, transforms it, and returns a ruby File or other (shrine-compatible) IO-like object. It will usually be done inside a custom Asset class definition.

class Asset < Kithe::Asset
  define_derivative :thumbnail do |original_file|
  end
end

The original_file passed in will be a ruby File object that is already open for reading. If you need a local file path for your transformation, just use ‘original_file.path`.

The return value can be any IO-like object. If it is a ruby File or Tempfile, that temporary file will be deleted for you after the derivative has been created. If you have to make any intermediate files, you are responsible for cleaning them up. Ruby stdlib Tempfile and Dir.mktmpdir may be useful.

If in order to do your transformation you need additional information about the original, just add a ‘record:` keyword argument to your block, and the Asset object will be passed in:

define_derivative :thumbnail do |original_file, record:|
   record.width, record.height, record.content_type # etc
end

Derivatives are normally uploaded to the Shrine storage labeled :kithe_derivatives, but a definition can specify an alternate Shrine storage id. (specified shrine storage key is applied on derivative creation; if you change it with existing derivatives, they should remain, and be accessible, where they were created; there is no built-in solution at present for moving them).

define_derivative :thumbnail, storage_key: :my_thumb_storage do |original| # ...

You can also set ‘default_create: false` if you want a particular definition not to be included in a no-arg `asset.create_derivatives` that is normally triggered on asset creation.

And you can set content_type to either a specific type like ‘image/jpeg` (or array of such) or a general type like `image`, if you want to define a derivative generation routine for only certain types. If multiple blocks for the same key are defined, with different content_type restrictions, the most specific one will be used. That is, for a JPG, `image/jpeg` beats `image` beats no restriction.



89
90
91
92
93
94
95
96
97
98
99
100
# File 'app/models/kithe/asset.rb', line 89

def self.define_derivative(key, storage_key: :kithe_derivatives, content_type: nil, default_create: true, &block)
  # Make sure we dup the array to handle sub-classes on class_attribute
  self.derivative_definitions = self.derivative_definitions.dup.push(
    DerivativeDefinition.new(
      key: key,
      storage_key: storage_key,
      content_type: content_type,
      default_create: default_create,
      proc: block
    )
  )
end

.defined_derivative_keysObject

Returns all derivative keys with a definition, as array of strings



103
104
105
# File 'app/models/kithe/asset.rb', line 103

def self.defined_derivative_keys
  self.derivative_definitions.collect(&:key).uniq.collect(&:to_s)
end

.remove_derivative_definition!(*keys) ⇒ Object

If you have a subclass that has inherited derivative definitions, you can remove them – only by key, will remove any definitions with that key regardless of content_type restrictions.

This could be considered rather bad OO design, you might want to consider a different class hieararchy where you don’t have to do this. But it’s here.



113
114
115
116
117
118
# File 'app/models/kithe/asset.rb', line 113

def self.remove_derivative_definition!(*keys)
  keys = keys.collect(&:to_sym)
  self.derivative_definitions = self.derivative_definitions.reject do |defn|
    keys.include?(defn.key.to_sym)
  end
end

Instance Method Details

#acquire_lock_on_shaObject

Take out a DB lock on the asset with unchanged sha512 saved in metadata. If a lock can’t be acquired – which would be expected to be because the asset has already changed and has a new sha for some reason – returns nil.

Useful for making a change to an asset making sure it applies to a certain original file.

Needs to be done in a transaction, and you should keep the transaction SMALL AS POSSIBLE. We can’t check to make sure you’re in a transaction reliably because of Rails transactional tests, you gotta do it!

This method is mostly intended for internal Kithe use, cause it’s a bit tricky.

Raises:

  • (ArgumentError)


223
224
225
226
227
# File 'app/models/kithe/asset.rb', line 223

def acquire_lock_on_sha
  raise ArgumentError.new("Can't acquire lock without sha512 in metadata") if self.sha512.blank?

  Kithe::Asset.where(id: self.id).where("file_data -> 'metadata' ->> 'sha512' = ?", self.sha512).lock.first
end

#create_derivatives(only: nil, except: nil, lazy: false, mark_created: nil) ⇒ Object

Create derivatives for every definition added with ‘define_derivative. Ordinarily will create a definition for every definition that has not been marked `default_create: false`.

But you can also pass ‘only` and/or `except` to customize the list of definitions to be created, possibly including some that are `default_create: false`.

create_derivatives should be idempotent. If it has failed having only created some derivatives, you can always just run it again.

Will normally re-create derivatives (per existing definitions) even if they already exist, but pass ‘lazy: false` to skip creating if a derivative with a given key already exists. This will use the asset `derivatives` association, so if you are doing this in bulk for several assets, you should eager-load the derivatives association for efficiency.



133
134
135
# File 'app/models/kithe/asset.rb', line 133

def create_derivatives(only: nil, except: nil, lazy: false, mark_created: nil)
  DerivativeCreator.new(derivative_definitions, self, only: only, except: except, lazy: lazy, mark_created: mark_created).call
end

#derivative_for(key) ⇒ Object

Just finds the Derivative object matching supplied key. if you’re going to be calling this on a list of Asset objects, make sure to preload :derivatives association.



179
180
181
# File 'app/models/kithe/asset.rb', line 179

def derivative_for(key)
  derivatives.find {|d| d.key == key.to_s }
end

#derivatives_created?Boolean

The derivative creator sets metadata when it’s created all derivatives defined as ‘default_create`. So we can tell you if it’s done or not.

Returns:

  • (Boolean)


206
207
208
209
210
# File 'app/models/kithe/asset.rb', line 206

def derivatives_created?
  if file
    !!file.["derivatives_created"]
  end
end

#promote(action: :store, context: {}) ⇒ Object

Runs the shrine promotion step, that we normally have in backgrounding, manually and in foreground. You might use this if a promotion failed and you need to re-run it, perhaps in bulk. It’s also useful in tests.

This will no-op unless the attached file is stored in cache – that is, it will no-op if the file has already been promoted. In this way it matches ordinary shrine promotion. (Do we need an option to force promotion anyway?)

Note that calling ‘file_attacher.promote` on it’s own won’t do quite the right thing, and won’t respect that the file is already cached.



193
194
195
196
197
198
199
200
201
202
# File 'app/models/kithe/asset.rb', line 193

def promote(action: :store, context: {})
  return unless file_attacher.cached?

  context = {
    action: action,
    record: self
  }.merge(context)

  file_attacher.promote(file_attacher.get, context)
end

#remove_derivative(key) ⇒ Object



165
166
167
168
169
170
171
172
173
174
175
# File 'app/models/kithe/asset.rb', line 165

def remove_derivative(key)
  if association(:derivatives).loaded?
    derivatives.find_all { |d| d.key == key.to_s }.each do |deriv|
      derivatives.delete(deriv)
    end
  else
    Kithe::Derivative.where(key: key.to_s, asset: self).each do |deriv|
      deriv.destroy!
    end
  end
end

#representativeObject Also known as: leaf_representative

An Asset is it’s own representative



236
237
238
# File 'app/models/kithe/asset.rb', line 236

def representative
  self
end

#representative_idObject Also known as: leaf_representative_id



240
241
242
# File 'app/models/kithe/asset.rb', line 240

def representative_id
  id
end

#saved_change_to_file_sha?Boolean

Returns:

  • (Boolean)


229
230
231
232
233
# File 'app/models/kithe/asset.rb', line 229

def saved_change_to_file_sha?
  saved_change_to_file_data? &&
    saved_change_to_file_data.first.try(:dig, "metadata", "sha512") !=
      saved_change_to_file_data.second.try(:dig, "metadata", "sha512")
end

#update_derivative(key, io, storage_key: :kithe_derivatives, metadata: {}) ⇒ Derivative

Adds an associated derivative with key and io bytestream specified. Normally you don’t use this with derivatives defined with ‘define_derivative`, this is used by higher-level API. But if you’d like to add a derivative not defined with ‘define_derivative`, or for any other reason would like to manually add a derivative, this is public API meant for that.

Ensures safe from race conditions under multi-thread/process concurrency, to make sure any existing derivative with same key is atomically replaced, and if the asset#file is changed to a different bytestream (compared to what’s in memory for this asset), we don’t end up saving a derivative based on old one.

Can specify any metadata values to be force set on the Derivative#file, and a specific Shrine storage key (defaults to :kithe_derivatives shrine storage)

Parameters:

  • key

    derivative-type identifying key

  • io

    An IO-like object (according to Shrine), bytestream for the derivative

  • storage_key (defaults to: :kithe_derivatives)

    what Shrine storage to store derivative file in, default :kithe_derivatives

  • metadata (defaults to: {})

    an optional hash of key/values to set as default metadata for the Derivative#file shrine object.

Returns:

  • (Derivative)

    the Derivative created, or nil if it was not created because no longer applicable (underlying Asset#file has changed in db)



159
160
161
162
163
# File 'app/models/kithe/asset.rb', line 159

def update_derivative(key, io, storage_key: :kithe_derivatives, metadata: {})
  DerivativeUpdater.new(self, key, io, storage_key: storage_key, metadata: ).update.tap do |result|
    self.derivatives.reset if result
  end
end