lib/fiona7/scrivito_patches/binary.rb in infopark_fiona7-0.30.0.2 vs lib/fiona7/scrivito_patches/binary.rb in infopark_fiona7-0.70.0.1

- old
+ new

@@ -1,55 +1,474 @@ require 'scrivito/binary' -module Scrivito - # This class had been reworked to support uploads directly - # into CMS and not into S3 - class Binary - # new implementation - def content_type - if shadow_obj - shadow_obj.mime_type +require 'mini_magick' + + +module Fiona7 + module BinaryHandling + class ParamEncoder + def initialize + self.verifier = ActiveSupport::MessageVerifier.new( + Rails.application.secrets.secret_key_base, serializer: ::JSON) end - end - def content_length - if shadow_obj - shadow_obj.body_length - else - 0 + def encode(params) + self.verifier.generate(params) end - end - def filename - if shadow_obj - shadow_obj.name + '.' + shadow_obj.file_extension + def decode(string) + self.verifier.verify(string) + rescue ActiveSupport::MessageVerifier::InvalidSignature => e + {} end + + protected + attr_accessor :verifier end - # New API call. Allows to read the extension of the stored file. - def file_extension - if shadow_obj - shadow_obj.file_extension + class MetaBinary + class ActualBinary + def initialize(binary_id, transformation=false) + self.binary_id = binary_id.to_i + self.transformation = transformation + self.obj = self.load_obj + end + protected + attr_accessor :binary_id, :transformation, :obj + + def load_obj + if Fiona7.mode == :legacy + Fiona7::WriteObj.find(self.binary_id) + else + Fiona7::InternalReleasedObj.find(self.binary_id) + end + rescue ActiveRecord::RecordNotFound + nil + end end + + class UnmodifiedBinary < ActualBinary + def valid? + self.binary_id >= 0 && self.obj.try(:binary?) + end + + def present? + !self.obj.nil? + end + + def filename + self.obj.filename + end + + def filepath + self.obj.body_data_path + end + + def length + self.obj.body_length + end + + def width + if self.image + self.image[:width] + else + 0 + end + end + + def height + if self.image + self.image[:height] + else + 0 + end + end + + def mime_type + self.obj.mime_type + end + + def last_changed + self.obj.last_changed.utc + end + + protected + def image + return @image if defined?(@image) + if self.filepath + @image = MiniMagick::Image.new(self.filepath) + else + @image = nil + end + end + end + + class TransformedBinary < UnmodifiedBinary + def valid? + super && valid_transformation? + end + + def filename + ::File.basename(self.transformed_filepath) + end + + alias_method :original_filepath, :filepath + def filepath + self.transformed_filepath + end + + def legnth + ::File.size(self.transformed_filepath) + end + + def last_changed + ::File.mtime(self.transformed_filepath).utc + end + + protected + def valid_transformation? + true && + (self.mime_type =~ /image\//) && + (self.width.present? || self.height.present?) && + (!self.width.present? || (1..4096).include?(self.width.to_i)) && + (!self.height.present? || (1..4096).include?(self.height.to_i)) && + ((1..75).include?(self.quality.to_i)) && + (self.fit == 'clip' || self.fit == 'crop') && + (self.width.to_i + self.height.to_i < 4096) && + (self.fit == 'clip' || (self.width.present? && self.height.present?)) + end + + def transformed_filepath + return @transformed_filepath if @transformed_filepath + return nil if self.original_filepath.nil? + + output_filepath = original_filepath + self.transformed_filename + + if ::File.exists?(output_filepath) + Rails.logger.debug("Transformed image #{output_filepath} already generated, serving") + return @transformed_filepath = output_filepath + else + Rails.logger.debug("Transforming image") + end + + image = MiniMagick::Image.open(original_filepath) + + if self.fit == 'clip' + image.combine_options do |b| + b.resize "#{self.width}x#{self.height}>" + end + elsif self.fit == 'crop' + image.combine_options do |b| + b.resize "#{self.width}x#{self.height}>" + b.gravity "center" + b.extent "#{self.width}x#{self.height}>" + end + else + raise 'invalid fit' + end + + image.write(output_filepath) + @transformed_filepath = output_filepath + end + + def transformed_filename + ext = ::File.extname(self.original_filepath) + "#{self.fit}_#{self.width}_#{self.height}_#{self.quality}#{ext}" + end + + def transformation_with_fallback + @transformation_with_fallback ||= (self.transformation || {}).with_indifferent_access + end + + def width + self.transformation_with_fallback[:width].to_s + end + + def height + self.transformation_with_fallback[:height].to_s + end + + def quality + self.transformation_with_fallback[:quality] || '75' + end + + def fit + self.transformation_with_fallback[:fit] || 'clip' + end + end + + delegate :valid?, :present?, + :filename, :filepath, + :mime_type, :length, :last_changed, + :width, :height, + :to => :implementation + + def initialize(binary_id, transformation=false) + if transformation + self.implementation = TransformedBinary.new(binary_id, transformation) + else + self.implementation = UnmodifiedBinary.new(binary_id, transformation) + end + end + + protected + attr_accessor :implementation + end - # New API call. Allows to access the contents of the blob. - def content - if shadow_obj - shadow_obj.body + module DeliveryMixin + # GET + def show + binary_id = binary_id_from_params + transformation = transformation_from_params + meta_binary = MetaBinary.new(binary_id, transformation) + + if !meta_binary.present? + not_found + elsif !meta_binary.valid? + bad_request + elsif stale?(:last_modified => meta_binary.last_changed) && true + filename = meta_binary.filename + filepath = meta_binary.filepath + mime_type = meta_binary.mime_type + + send_file(File.expand_path(filepath), { + :type => mime_type, + :filename => filename, + :disposition => 'inline' + }) + end end + + # HEAD + def query + binary_id = binary_id_from_params + transformation = transformation_from_params + meta_binary = BinaryHandling::MetaBinary.new(binary_id, transformation) + + if !meta_binary.valid? + bad_request + elsif !meta_binary.present? + not_found + else + set_header('Content-Type', meta_binary.mime_type) + set_header('Content-Length', meta_binary.length) + set_header('Cache-Control', 'no-transform,public,max-age=300,s-maxage=900') + head_ok + end + end + + protected + def bad_request + head 400 + end + + def not_found + head 404 + end + + def head_ok + head 200 + end end - def url - ActiveSupport::Deprecation.warn("This method should never be called. Use scrivito_path or scrvito_url whenever possible") - if shadow_obj - Rails.application.routes.url_helpers.fiona7_blob_path(id: @id, name: self.filename) + + class EmbeddedServer + class << self + attr_accessor :enable end + + self.enable = false + + HOST = 'localhost' + PORT = 7104 + extend MonitorMixin + + require 'webrick' + + class BinaryServerlet < ::WEBrick::HTTPServlet::AbstractServlet + class RequestHandler + + include Fiona7::BinaryHandling::DeliveryMixin + + def initialize(request, response) + self.request = request + self.response = response + end + + protected + attr_accessor :request, :response + def binary_id_from_params + # TODO: handle malformed input + match = /\/_b\/([0-9]+)(\/(.*)(\.[a-z0-9A-Z]+)?)?/.match(self.request.request_uri.to_s) + if match + match[1] + else + 0 + end + end + + def transformation_from_params + if self.request.query_string + ParamEncoder.new.decode( + ::CGI.parse(self.request.query_string)["t"] + ) + end + end + + def set_header(name, value) + self.response[name] = value + end + + # minimal stubs for Rails API below + + def stale?(*args) + true + end + + def head(status) + self.response.status = status + end + + def send_file(filepath, options={}) + self.response['Content-Type'] = options[:type] + self.response['Content-Length'] = ::File.size(filepath) + self.response['Content-Disposition'] = "#{options[:disposition]}; filename=\"#{options[:filename]}\"" + self.response.body = File.read(filepath) + self.response.status = 200 + end + end + + def do_GET(request, response) + RequestHandler.new(request, response).show + end + + def do_HEAD(request, response) + RequestHandler.new(request, response).query + end + end + + def self.running_instance + if !self.enable + return HOST, PORT + end + + self.synchronize do + @server_thread ||= self.wait_for_server + + Kernel.at_exit do + Process.kill("INT", @server_thread) + Process.kill("KILL", @server_thread) + end + + return HOST, PORT + end + end + + def self.wait_for_server + pid = Process.fork do + self.run_server + end + # wait for webrick! + sleep 1 + pid + end + + def self.run_server + + server = ::WEBrick::HTTPServer.new({ + :Port => PORT, :DocumentRoot => '/dev/null', + #Logger: WEBrick::Log.new(Logger.new(nil)), AccessLog: [] + }) + + server.mount '/', BinaryServerlet + + Signal.trap('INT') do + server.shutdown + exit! + end + + server.start + end end - private - def shadow_obj - @shadow_obj ||= Fiona7::InternalReleasedObj.find(@id.to_i) if @id.to_i != 0 + class UrlGenerator + attr_reader :blob_id, :access_type, :verb, :transformation + def initialize(blob_id, access_type, verb, transformation) + self.blob_id = blob_id + self.access_type = access_type + self.verb = verb + self.transformation = transformation + end + + def generate + if server_detected? + hosted_server_url + else + embedded_server_url + end + end + + protected + attr_writer :blob_id, :access_type, :verb, :transformation + def blob + if Fiona7.mode == :legacy + @blob ||= Fiona7::WriteObj.find(blob_id.to_i) + else + @blob ||= Fiona7::InternalReleasedObj.find(blob_id.to_i) + end + end + + def filename + self.blob.filename + rescue ActiveRecord::RecordNotFound + "null.null" + end + + def server_detected? + Fiona7::Middleware::ServerDetectionMiddleware.server_detected? + end + + def hosted_server_url + # TODO: use middleware for storing url + host = Fiona7::Middleware::ServerDetectionMiddleware.server_name + port = Fiona7::Middleware::ServerDetectionMiddleware.server_port + + generate_url(host, port) + end + + def embedded_server_url + host, port = EmbeddedServer.running_instance + generate_url(host, port) + end + + def generate_url(host, port=80) + options = {} + options[:host] = host + options[:port] = port + options[:path] = "/_b/#{self.blob_id}/#{self.filename}" + if self.transformation.present? + self.validate_transformation! + options[:query] = self.encode_query_string + end + URI::HTTP.build(options).to_s + end + + def encode_query_string + token = ParamEncoder.new.encode(self.transformation) + return "t=#{token}" + end + + def validate_transformation! + MetaBinary.new(self.blob_id, self.transformation).valid? || raise(Scrivito::TransformationDefinitionError.new("Invalid transformation", "binary.unprocessable.image.transform.invalid_config")) + end end + end +end + +module Scrivito + # This class had been reworked to support uploads directly + # into CMS and not into S3 + class Binary end end