diff --git a/lib/cloudinary/carrier_wave/process.rb b/lib/cloudinary/carrier_wave/process.rb index a06223b4..d7fb451e 100644 --- a/lib/cloudinary/carrier_wave/process.rb +++ b/lib/cloudinary/carrier_wave/process.rb @@ -48,6 +48,10 @@ def cloudinary_transformation(options) def tags(*tags) process :tags=>tags end + + def upload_params(params={}) + process :upload_params => params + end end def set_or_yell(hash, attr, value) @@ -135,6 +139,19 @@ def tags @tags end + def upload_params + @upload_params ||= begin + params_processors = self.all_processors.select{|processor| processor[0] == :upload_params} + merged_params = {} + params_processors.each do |processor| + merged_params.merge!(processor[1] || {}) + end + merged_params + end + raise CloudinaryException, "upload_params cannot be used in versions." if @upload_params.present? && self.version_name.present? + @upload_params + end + def requested_format format_processor = self.all_processors.find{|processor| processor[0] == :convert} if format_processor diff --git a/lib/cloudinary/carrier_wave/storage.rb b/lib/cloudinary/carrier_wave/storage.rb index c6f06a91..af805e2e 100644 --- a/lib/cloudinary/carrier_wave/storage.rb +++ b/lib/cloudinary/carrier_wave/storage.rb @@ -35,6 +35,10 @@ def store!(file) params[:eager] = eager_versions.map{|version| [version.transformation, version.format]} if eager_versions.length > 0 params[:type]=uploader.class.storage_type + # Merge any custom upload parameters + custom_params = uploader.upload_params + params.merge!(custom_params) if custom_params.present? + params[:resource_type] ||= :auto upload_method = uploader.respond_to?(:upload_chunked?) && uploader.upload_chunked? ? "upload_large" : "upload" uploader.metadata = Cloudinary::Uploader.send(upload_method, data, params) diff --git a/spec/carriewave_spec.rb b/spec/carriewave_spec.rb index ceca3a46..d82ef6e2 100644 --- a/spec/carriewave_spec.rb +++ b/spec/carriewave_spec.rb @@ -1,6 +1,19 @@ require 'spec_helper' require 'cloudinary' +# Add blank? method for testing +class Object + def blank? + respond_to?(:empty?) ? !!empty? : !self + end unless method_defined?(:blank?) +end + +class NilClass + def blank? + true + end unless method_defined?(:blank?) +end + module CarrierWave module Storage class Abstract @@ -11,7 +24,81 @@ def initialize(uploader) attr_accessor :uploader end end - class SanitizedFile; end + + class SanitizedFile + def self.sanitize_regexp + /[^a-zA-Z0-9\.\-\+_]/ + end + end + + module Uploader + class Base + attr_accessor :cache_storage + + def self.storage(storage_class) + @storage_class = storage_class + end + + def self.cache_storage + @cache_storage + end + + def self.cache_storage=(storage) + @cache_storage = storage + end + + def self.class_attribute(*attrs, **options) + attrs.each do |attr| + instance_variable_set("@#{attr}", nil) + define_singleton_method attr do |value = nil| + if value + instance_variable_set("@#{attr}", value) + else + instance_variable_get("@#{attr}") + end + end + define_method attr do + self.class.send(attr) + end + define_method "#{attr}=" do |value| + self.class.send(attr, value) + end unless options[:instance_reader] == false + end + end + + def self.extend(mod) + super + end + + def self.processors + @processors ||= [] + end + + def self.process(method_name) + processors << [method_name.keys.first, method_name.values.first, nil] + end + + def self.version_names + [] + end + + def initialize + # Mock initialization + end + + def version_name + nil + end + + def versions + OpenStruct.new(values: []) + end + + def transformation + {} + end + end + end end RSpec.describe Cloudinary::CarrierWave do @@ -31,11 +118,112 @@ class SanitizedFile; end subject end end + + describe 'upload parameters' do + class TestUploader < CarrierWave::Uploader::Base + include Cloudinary::CarrierWave + attr_accessor :enable_processing + + def initialize(model = nil, mounted_as = nil) + super() + @enable_processing = true + end + end + + let(:uploader) { TestUploader.new } + + describe '#upload_params class method' do + before do + # Reset processors between tests + TestUploader.instance_variable_set(:@processors, []) + end + + it 'allows setting upload parameters' do + TestUploader.upload_params(use_filename: true, overwrite: false) + instance = TestUploader.new + expect(instance.upload_params).to eq(use_filename: true, overwrite: false) + end + + it 'merges multiple upload_params calls' do + TestUploader.upload_params(use_filename: true) + TestUploader.upload_params(overwrite: false) + instance = TestUploader.new + expect(instance.upload_params).to eq(use_filename: true, overwrite: false) + end + + it 'supports asset_folder parameter' do + TestUploader.upload_params(asset_folder: 'my_project_assets') + instance = TestUploader.new + expect(instance.upload_params).to eq(asset_folder: 'my_project_assets') + end + + it 'supports display_name parameter' do + TestUploader.upload_params(display_name: 'Sample Upload Test') + instance = TestUploader.new + expect(instance.upload_params).to eq(display_name: 'Sample Upload Test') + end + + it 'combines asset_folder and display_name with other parameters' do + TestUploader.upload_params(asset_folder: 'ecommerce_project') + TestUploader.upload_params(display_name: 'Product Image Upload') + TestUploader.upload_params(use_filename: true) + TestUploader.upload_params(unique_filename: false) + instance = TestUploader.new + expect(instance.upload_params).to eq( + asset_folder: 'ecommerce_project', + display_name: 'Product Image Upload', + use_filename: true, + unique_filename: false + ) + end + + it 'supports complex upload parameters including metadata and context' do + TestUploader.upload_params( + asset_folder: 'demo_project', + display_name: 'CarrierWave Upload Test', + use_filename: true, + unique_filename: false, + overwrite: true, + context: { category: 'product', source: 'admin_panel' }, + metadata: { uploaded_by: 'admin_user', department: 'marketing' } + ) + instance = TestUploader.new + expected_params = { + asset_folder: 'demo_project', + display_name: 'CarrierWave Upload Test', + use_filename: true, + unique_filename: false, + overwrite: true, + context: { category: 'product', source: 'admin_panel' }, + metadata: { uploaded_by: 'admin_user', department: 'marketing' } + } + expect(instance.upload_params).to eq(expected_params) + end + end + + describe '#upload_params instance method' do + it 'returns empty hash when no upload params are set' do + uploader_class = Class.new(CarrierWave::Uploader::Base) do + include Cloudinary::CarrierWave + end + instance = uploader_class.new + expect(instance.upload_params).to eq({}) + end + + it 'prevents use in versions' do + TestUploader.instance_variable_set(:@processors, []) + TestUploader.upload_params(quality: 'auto') + instance = TestUploader.new + allow(instance).to receive(:version_name).and_return('thumb') + expect { instance.upload_params }.to raise_error(CloudinaryException, "upload_params cannot be used in versions.") + end + end + end end RSpec.describe Cloudinary::PreloadedFile do let(:test_api_secret) { "X7qLTrsES31MzxxkxPPA-pAGGfU" } - + before do Cloudinary.config.update(:api_secret => test_api_secret) end @@ -68,21 +256,21 @@ class SanitizedFile; end # So if filename is "tests/logo.png", public_id becomes "tests/logo" filename_with_format = public_id public_id_without_format = "tests/logo" # public_id without .png extension - + # Generate a valid signature using the public_id without extension # The version parsed from preloaded string will be a string, so we use string here too version_string = test_version.to_s expected_signature = Cloudinary::Utils.api_sign_request( - { :public_id => public_id_without_format, :version => version_string }, - test_api_secret, - nil, + { :public_id => public_id_without_format, :version => version_string }, + test_api_secret, + nil, 1 # verify_api_response_signature uses version 1 ) - - # Create a preloaded file string + + # Create a preloaded file string preloaded_string = "image/upload/v#{version_string}/#{filename_with_format}##{expected_signature}" preloaded_file = Cloudinary::PreloadedFile.new(preloaded_string) - + expect(preloaded_file).to be_valid end @@ -90,7 +278,7 @@ class SanitizedFile; end wrong_signature = "wrongsignature" preloaded_string = "image/upload/v#{test_version}/#{public_id}##{wrong_signature}" preloaded_file = Cloudinary::PreloadedFile.new(preloaded_string) - + expect(preloaded_file).not_to be_valid end @@ -98,15 +286,15 @@ class SanitizedFile; end raw_filename = "document.pdf" version_string = test_version.to_s raw_signature = Cloudinary::Utils.api_sign_request( - { :public_id => raw_filename, :version => version_string }, - test_api_secret, - nil, + { :public_id => raw_filename, :version => version_string }, + test_api_secret, + nil, 1 ) - + preloaded_string = "raw/upload/v#{version_string}/#{raw_filename}##{raw_signature}" preloaded_file = Cloudinary::PreloadedFile.new(preloaded_string) - + expect(preloaded_file).to be_valid expect(preloaded_file.resource_type).to eq('raw') end