Unverified Commit 3c9d9525 authored by azul's avatar azul
Browse files

namespace: asset for all the asset things

Also separate Asset::Storage::Path from Asset::Storage
parent 4955ee0a
......@@ -145,7 +145,7 @@ gem 'uglify_html', require: 'uglify_html',
# media upload post processing has it's own repo
# version is rather strict for now as api may still change.
gem 'crabgrass_media', '~> 0.0.5', require: 'media'
gem 'crabgrass_media', '~> 0.1.1', require: 'media', path: '../media'
##
## GEMS not required, but a really good idea
......
......@@ -11,6 +11,12 @@ GIT
rails-dev-boost (0.3.0)
railties (>= 3.0)
PATH
remote: ../media
specs:
crabgrass_media (0.1.1)
activesupport (>= 3.2)
PATH
remote: vendor/gems/riseuplabs-greencloth-0.1
specs:
......@@ -88,8 +94,6 @@ GEM
execjs
coffee-script-source (1.10.0)
columnize (0.9.0)
crabgrass_media (0.0.5)
activesupport (>= 3.2)
daemons (1.2.3)
debugger (1.6.8)
columnize (>= 0.3.1)
......@@ -252,7 +256,7 @@ DEPENDENCIES
cache_digests (~> 0.3)
capybara
coffee-rails (~> 3.2.2)
crabgrass_media (~> 0.0.5)
crabgrass_media (~> 0.1.1)!
daemons
debugger
delayed_job_active_record (~> 4.0)
......
......@@ -3,7 +3,7 @@
Assets use a lot of classes to manage a particular uploaded file:
Asset -- main asset class.
ImageAsset -- a subclass of Asset using STI, for example.
Asset::Image -- a subclass of Asset using STI, for example.
Asset::Version -- all the past and present versions of the main asset.
Thumbnail -- a processed representation of an Asset (usually a small image)
......@@ -11,9 +11,9 @@ Assets use a lot of classes to manage a particular uploaded file:
have many thumbnails.
Additionally, three modules are included by Asset:
AssetExtension::Upload -- handles uploading data
AssetExtension::Storage -- handles where/how data is stored
AssetExtension::Thumbnails -- handles the creation of the thumbnails
Asset::Upload -- handles uploading data
Asset::Storage -- handles where/how data is stored
Asset::Thumbnails -- handles the creation of the thumbnails
Asset::Versions have the latter two included as well.
......@@ -76,16 +76,12 @@ TODO:
class Asset < ActiveRecord::Base
include Crabgrass::Page::Data
# Polymorph does not seem to be working with subclasses of Asset. For parent_type,
# it always picks "Asset". So, we hardcode what the query should be:
POLYMORPH_AS_PARENT = lambda { |a| "SELECT * FROM thumbnails WHERE parent_id = #{self.id} AND parent_type = \"#{self.type_as_parent}\"" }
# fields in assets table not in asset_versions
NON_VERSIONED = %w(page_terms_id is_attachment is_image is_audio is_video is_document caption taken_at credit)
# This is included here because Asset may take new attachment file data, but
# Asset::Version and Thumbnail don't need to.
include AssetExtension::Upload
include Asset::Upload
validates_presence_of :filename, unless: 'new_record?'
......@@ -165,11 +161,11 @@ class Asset < ActiveRecord::Base
acts_as_versioned do
def self.included(base)
base.send :include, AssetExtension::Storage
base.send :include, AssetExtension::Thumbnails
base.send :include, Asset::Storage
base.send :include, Asset::Thumbnails
base.belongs_to :user
base.has_many :thumbnails, class_name: '::Thumbnail',
dependent: :destroy, finder_sql: POLYMORPH_AS_PARENT do
base.has_many :thumbnails, class_name: '::Thumbnail', as: :parent,
dependent: :destroy do
def preview_images
small, medium, large = nil
self.each do |tn|
......@@ -198,6 +194,10 @@ class Asset < ActiveRecord::Base
# file name without extension
def basename; File.basename(filename, ext); end
def url(name = filename)
path.url(name)
end
def big_icon
"mime_#{Media::MimeType.icon_for(content_type)}"
end
......@@ -221,30 +221,28 @@ class Asset < ActiveRecord::Base
##
# to be overridden in Asset::Version
def version_path; []; end
def path
@path ||= Storage::Path.new id: id, filename: filename
end
def is_version?; false; end
def type_as_parent; self.type; end
versioned_class.class_eval do
delegate :page, :public?, :has_access!, to: :asset
# all our paths will have version info inserted into them
def version_path
['versions',version.to_s]
end
# our path id will be the id of the main asset
def path_id
asset.path_id if asset
def path
@path ||= Storage::Path.new id: asset.id,
filename: filename,
version: version.to_s
end
# this object is a version, not the main asset
def is_version?; true; end
# delegate call to thumbdefs to our original Asset subclass.
# eg: Asset::Version#thumbdefs --> ImageAsset.thumbdefs
# eg: Asset::Version#thumbdefs --> Asset::Image.thumbdefs
def thumbdefs
versioned_type.constantize.class_thumbdefs if versioned_type
"Asset::#{versioned_type}".constantize.class_thumbdefs if versioned_type
end
def type_as_parent
......@@ -293,7 +291,7 @@ class Asset < ActiveRecord::Base
self.parent_page = AssetPage.create!(page_params)
end
# some asset subclasses (like AudioAsset) will display using flash
# some asset subclasses (like Asset::Audio) will display using flash
# they should override this method to say which partial will render this code
def embedding_partial
nil
......@@ -324,7 +322,7 @@ class Asset < ActiveRecord::Base
##
#
# creates an Asset of the appropriate subclass (ie ImageAsset).
# creates an Asset of the appropriate subclass (ie Asset::Image).
#
def self.create_from_params(attributes = nil, &block)
asset_class(attributes).create!(attributes, &block)
......@@ -349,7 +347,7 @@ class Asset < ActiveRecord::Base
private
#
# returns the appropriate asset class, ie ImageAsset, for the attributes passed in.
# returns the appropriate asset class, ie Asset::Image, for the attributes passed in.
#
def self.asset_class(attributes)
if attributes
......@@ -368,7 +366,7 @@ class Asset < ActiveRecord::Base
# MIME TYPES
#
# eg: 'image/jpg' --> ImageAsset
# eg: 'image/jpg' --> Asset::Image
def self.class_for_mime_type(mime)
if mime
Media::MimeType.asset_class_from_mime_type(mime).constantize
......
class AudioAsset < Asset
class Asset::Audio < Asset
def update_media_flags
self.is_audio = true
......
class CorruptedAsset < Asset
class Asset::Corrupted < Asset
def is_image
true
......
......@@ -2,14 +2,14 @@
A generic document asset: anything that we can create a pdf out of.
See TextdocAsset and SpreadsheetAsset for more specific asset types.
See Asset::Textdoc and Asset::Spreadsheet for more specific asset types.
What files become DocAssets? This is set by lib/media/mime_type.rb
What files become doc assets? This is set by lib/media/mime_type.rb
What doc files may generate thumbnails? This is set by lib/media/processors.rb
=end
class DocAsset < Asset
class Asset::Doc < Asset
def update_media_flags
self.is_document = true
......
......@@ -4,7 +4,7 @@
# we want thumbnails in a format that will preserve transparency.
#
class GifAsset < Asset
class Asset::Gif < Asset
def update_media_flags
self.is_image = true
......
class ImageAsset < Asset
class Asset::Image < Asset
def update_media_flags
self.is_image = true
......
......@@ -4,7 +4,7 @@
# we probably want the thumbnails to be PNGs.
#
class PngAsset < Asset
class Asset::Png < Asset
def update_media_flags
self.is_image = true
......
class SpreadsheetAsset < Asset
class Asset::Spreadsheet < Asset
def update_media_flags
self.is_document = true
......
#
# Asset::Storage
#
# Code to handle the backend file storage for asset models.
#
require 'fileutils'
module Asset::Storage
def self.included(base) #:nodoc:
base.before_update :rename_file
base.after_destroy :destroy_file
end
def self.make_required_dirs
Path.private_storage.mkpath unless Path.private_storage.exist?
Path.public_storage.mkpath unless Path.public_storage.exist?
end
##
## ASSET PATHS
##
delegate :private_filename, :public_filename, to: :path
# return a list of all the files that are associated with this asset
# including thumbnails, but not versions. This list is used to remove
# old files after a new version is uploaded.
def all_filenames
files = []
if filename
files << private_filename
thumbdefs.each do |name,thumbdef|
files << private_filename(thumbnail_filename(thumbdef))
end
end
files
end
##
## override attributes
##
# Sets a new filename.
def filename=(value)
@path = nil
write_attribute :filename, sanitize_filename(value)
end
# Sets a new base filename, without changing the extension
def base_filename=(value)
return unless value
if read_attribute(:filename) and !value.ends_with?(ext)
value += ext
end
self.filename = value
end
# create a hard link between the files for orig_model
# and the files for self (which are in a versioned directory)
def clone_files_from(orig_model)
if is_version? and filename
hard_link orig_model.private_filename, self.private_filename
thumbdefs.each do |name, thumbdef|
thumbnail_filename = thumbnail_filename(thumbdef)
hard_link orig_model.private_filename(thumbnail_filename),
self.private_filename(thumbnail_filename)
end
end
end
def hard_link(source, dest)
FileUtils.mkdir_p(File.dirname(dest))
if File.exist?(source) and !File.exist?(dest)
FileUtils.ln(source, dest)
end
end
protected
##
## file management
##
# Destroys the all files for this asset.
def destroy_file
if is_version?
# just remove version directory
FileUtils.rm_rf(File.dirname(private_filename)) if File.exist?(File.dirname(private_filename))
# elsif is_thumbnail?
# # just remove thumbnail
# FileUtils.rm(private_filename) if File.exists?(private_filename)
else
# remove everything
remove_symlink
FileUtils.rm_rf(File.dirname(private_filename)) if File.exist?(File.dirname(private_filename))
end
end
#
# renames the stored file if the self.filename attribute has changed.
# called as before_update callback.
#
def rename_file
if filename_changed? and !new_record? and !uploaded_data_changed?
Dir.chdir( File.dirname(private_filename) ) do
FileUtils.mv filename_was, filename
end
end
end
# Saves the file to the file system
def save_to_storage(temp_path)
if File.exist?(temp_path)
FileUtils.mkdir_p(File.dirname(private_filename))
FileUtils.cp(temp_path, private_filename)
File.chmod(0644, private_filename)
end
true
end
def current_data
File.file?(private_filename) ? File.read(private_filename) : nil
end
def symlink_missing?
self.public? and !has_symlink?
end
public :symlink_missing?
def has_symlink?
File.exist?(File.dirname(public_filename))
end
# creates a symlink from the private asset storage to a publicly accessible directory
def add_symlink
unless has_symlink?
real_private_path = Pathname.new(private_filename).realpath.dirname
real_public_path = Path.public_storage.realpath
public_to_private = real_private_path.relative_path_from(real_public_path)
real_public_path += "#{path.id}"
#puts "FileUtils.ln_s(#{public_to_private}, #{real_public_path})"
FileUtils.ln_s(public_to_private, real_public_path)
end
end
# removes symlink from public directory
def remove_symlink
if has_symlink?
FileUtils.rm(File.dirname(public_filename))
end
end
##
## Utility
##
def sanitize_filename(filename)
return unless filename
filename.strip.tap do |name|
# NOTE: File.basename doesn't work right with Windows paths on Unix
# get only the filename, not the whole path
name.gsub! /^.*(\\|\/)/, ''
# strip out ' and "
# name.gsub! /['"]/, ''
# keep:
# alphanumeric characters
# hypen
# space
# period
#name.gsub! /[^\w\.\ ]+/, '-'
# don't allow the thumbnail separator
name.gsub! /#{THUMBNAIL_SEPARATOR}/, ' '
# remove weird constructions:
# - trailing or leading hypen
# - hypen-space or hypen-period
# - duplicate spaces
name.gsub! /^\-|\-$|/, ''
name.gsub! /\-\.|\.\-/, '.'
name.gsub! /\-\ |\ \-/, ' '
name.gsub! /\ +/, ' '
end
end
# a utility function to remove a series of files.
def remove_files(*files)
files.each do |file|
File.unlink(file) if file and File.exist?(file)
end
end
end
#
# Asset::Storage::Path
#
# Handle the locations for the different asset files:
#
# (1) We want to keep two sets of file storage paths: one for private files and
# one for public files. When an asset becomes public, we create a symbolic
# link in the public directory to the file in the private directory.
#
# (2) Assets may be versioned. We keep the versions in a subfolder called 'versions'
#
# Lets suppose you have an asset called 'myfile.jpg' and have defined two thumbnails,
# one called :minithumb and one called :bigthumb.
#
# This is what the directory structure will look like:
#
# Rails.root/
# assets/
# 0000/
# 0055/
# myfile.jpg
# myfile_minithumb.jpg
# myfile_bigthumb.jpg
# versions/
# 1/
# myfile.jpg
# myfile_minithumb.jpg
# myfile_bigthumb.jpg
# public/
# assets/
# 55 --> ../../assets/0000/0055/
require 'pathname'
class Asset::Storage::Path
@@private_storage = ASSET_PRIVATE_STORAGE # \ set in environments/*.rb
@@public_storage = ASSET_PUBLIC_STORAGE # /
@@public_url_path = "/assets"
mattr_reader :public_url_path
def self.private_storage
Pathname.new(@@private_storage)
end
def self.public_storage
Pathname.new(@@public_storage)
end
attr_reader :id, :filename
def initialize(args)
@id = args[:id].to_i
@filename = args[:filename]
@version = args[:version]
end
def version_path
@version ? [:versions, @version] : []
end
# eg Rails.root/assets/0000/0055/myfile.jpg
# or Rails.root/assets/0000/0055/versions/1/myfile.jpg
# or Rails.root/assets/0000/0055/versions/1/myfile~small.jpg
def private_filename(name = filename)
path @@private_storage, partitioned_path, version_path, name
end
# eg Rails.root/public/assets/55/myfile.jpg
# or Rails.root/public/assets/55/versions/1/myfile.jpg
# or Rails.root/public/assets/55/versions/1/myfile~small.jpg
def public_filename(name = filename)
path @@public_storage, id, version_path, name
end
# eg /assets/55/myfile.jpg
# or /assets/55/versions/1/myfile.jpg
# or /assets/55/versions/1/myfile~small.jpg
def url(name = filename)
path public_url_path, id, version_path, CGI.escape(name)
end
protected
# with a id of 4, returns ['0000','0004']
def partitioned_path
("%08d" % id).scan(/..../)
end
# TODO: do we still need this?
# make a file or url path out of potentially missing or nested args
def path(*args)
args.flatten.compact.join('/')
end
end
class SvgAsset < Asset
class Asset::Svg < Asset
def update_media_flags
self.is_image = true
......
......@@ -2,7 +2,7 @@
# For MS Word and documents.
#
class TextAsset < Asset
class Asset::Text < Asset
def update_media_flags
self.is_document = true
......
#
# Thumbdef options:
#
# * :size -- specify in a format accepted by gm.
# ie: "64x64>" for resize but keep aspect ratio.
# * :ext -- the file extension of the thumbnail.
# * :mime_type -- usually automatic from :ext, but can be manually specified.
# * :depends -- specifies the name of a thumbnail that must be created first.
# if :depends is specified it is used as the source file for this
# thumbnail instead of the main asset.
# * :proxy -- suppose you need other thumbnails to depend on a thumbnail of
# of type odt, but the main asset might be an odt... setting
# proxy to true will make it so that we use the main asset
# file instead of generating a new one (but only if the mime
# types match).
# * :title -- some descriptive text for the kids.
# * :remote -- if true, try to process this thumbnail remotely, if possible.
# * :binary -- if true, this data must be treated as binary. if false,
# it may be transmitted over text-only channels. default is true.
class Asset::ThumbnailDefinition
attr_accessor :size, :name, :ext, :mime_type, :depends, :proxy, :title, :remote, :binary
def initialize(name, hsh)
self.name = name
self.binary = true
hsh.each do |key,value|
self.send("#{key}=",value)
end
self.mime_type ||= Media::MimeType.mime_type_from_extension(self.ext) if self.ext
end
end
module Asset::Thumbnails
extend ActiveSupport::Concern
included do
class_attribute :class_thumbdefs
end
module ClassMethods
def define_thumbnails(thumbnail_definitions={})
# inherit thumbdefs from super class but modify a dup
# so changes to not get pushed up.
# See http://martinciu.com/2011/07/difference-between-class_inheritable_attribute-and-class_attribute.html
self.class_thumbdefs = (class_thumbdefs || {}).dup
thumbnail_definitions.each do |name, data|
self.class_thumbdefs[name] = Asset::ThumbnailDefinition.new(name, data).freeze
end
self.class_thumbdefs.freeze
end
# true if any thumbnails are defined for the class
def thumbable?
self.class_thumbdefs.any?
end
end
#
# Allow for dynamic reassignment of thumbdefs for instances
# This is useful when we change Asset type depending on content type.
#
def thumbdefs
@thumbdefs || class_thumbdefs
end
def thumbdefs=(newdefs)
@thumbdefs = newdefs
end
# returnes true if the thumbnail file has been generated
def thumbnail_exists?(name)
fname = path.private_filename thumbnail_filename(name)
File.exist?(fname) and File.size(fname) > 0