paperclip 3.0.3 → 3.5.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of paperclip might be problematic. Click here for more details.

Files changed (121) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +2 -1
  3. data/.travis.yml +3 -0
  4. data/Appraisals +8 -3
  5. data/Gemfile +1 -1
  6. data/LICENSE +1 -1
  7. data/NEWS +198 -35
  8. data/README.md +332 -113
  9. data/features/basic_integration.feature +24 -12
  10. data/features/migration.feature +94 -0
  11. data/features/rake_tasks.feature +2 -3
  12. data/features/step_definitions/attachment_steps.rb +28 -0
  13. data/features/step_definitions/rails_steps.rb +94 -8
  14. data/features/step_definitions/s3_steps.rb +1 -1
  15. data/features/step_definitions/web_steps.rb +3 -3
  16. data/features/support/fakeweb.rb +4 -1
  17. data/features/support/file_helpers.rb +10 -0
  18. data/features/support/rails.rb +18 -2
  19. data/gemfiles/3.0.gemfile +2 -2
  20. data/gemfiles/3.1.gemfile +2 -2
  21. data/gemfiles/3.2.gemfile +2 -2
  22. data/gemfiles/4.0.gemfile +11 -0
  23. data/lib/generators/paperclip/templates/paperclip_migration.rb.erb +4 -8
  24. data/lib/paperclip/attachment.rb +96 -43
  25. data/lib/paperclip/attachment_registry.rb +57 -0
  26. data/lib/paperclip/callbacks.rb +2 -2
  27. data/lib/paperclip/content_type_detector.rb +78 -0
  28. data/lib/paperclip/file_command_content_type_detector.rb +32 -0
  29. data/lib/paperclip/filename_cleaner.rb +16 -0
  30. data/lib/paperclip/geometry.rb +66 -30
  31. data/lib/paperclip/geometry_detector_factory.rb +41 -0
  32. data/lib/paperclip/geometry_parser_factory.rb +31 -0
  33. data/lib/paperclip/glue.rb +2 -8
  34. data/lib/paperclip/has_attached_file.rb +99 -0
  35. data/lib/paperclip/helpers.rb +12 -15
  36. data/lib/paperclip/interpolations/plural_cache.rb +17 -0
  37. data/lib/paperclip/interpolations.rb +15 -5
  38. data/lib/paperclip/io_adapters/abstract_adapter.rb +45 -0
  39. data/lib/paperclip/io_adapters/attachment_adapter.rb +14 -49
  40. data/lib/paperclip/io_adapters/data_uri_adapter.rb +27 -0
  41. data/lib/paperclip/io_adapters/empty_string_adapter.rb +18 -0
  42. data/lib/paperclip/io_adapters/file_adapter.rb +8 -69
  43. data/lib/paperclip/io_adapters/identity_adapter.rb +1 -1
  44. data/lib/paperclip/io_adapters/nil_adapter.rb +2 -2
  45. data/lib/paperclip/io_adapters/stringio_adapter.rb +16 -45
  46. data/lib/paperclip/io_adapters/uploaded_file_adapter.rb +17 -40
  47. data/lib/paperclip/io_adapters/uri_adapter.rb +44 -0
  48. data/lib/paperclip/matchers/have_attached_file_matcher.rb +1 -5
  49. data/lib/paperclip/matchers/validate_attachment_content_type_matcher.rb +36 -17
  50. data/lib/paperclip/matchers/validate_attachment_presence_matcher.rb +5 -1
  51. data/lib/paperclip/matchers.rb +3 -3
  52. data/lib/paperclip/missing_attachment_styles.rb +11 -16
  53. data/lib/paperclip/processor.rb +12 -0
  54. data/lib/paperclip/railtie.rb +5 -1
  55. data/lib/paperclip/schema.rb +59 -23
  56. data/lib/paperclip/storage/filesystem.rb +23 -5
  57. data/lib/paperclip/storage/fog.rb +64 -25
  58. data/lib/paperclip/storage/s3.rb +93 -52
  59. data/lib/paperclip/style.rb +2 -2
  60. data/lib/paperclip/tempfile_factory.rb +21 -0
  61. data/lib/paperclip/thumbnail.rb +18 -3
  62. data/lib/paperclip/validators/attachment_content_type_validator.rb +38 -10
  63. data/lib/paperclip/validators/attachment_presence_validator.rb +8 -8
  64. data/lib/paperclip/validators/attachment_size_validator.rb +12 -7
  65. data/lib/paperclip/validators.rb +21 -2
  66. data/lib/paperclip/version.rb +1 -1
  67. data/lib/paperclip.rb +15 -44
  68. data/lib/tasks/paperclip.rake +26 -7
  69. data/paperclip.gemspec +11 -7
  70. data/test/attachment_definitions_test.rb +12 -0
  71. data/test/attachment_processing_test.rb +83 -0
  72. data/test/attachment_registry_test.rb +77 -0
  73. data/test/attachment_test.rb +253 -44
  74. data/test/content_type_detector_test.rb +50 -0
  75. data/test/file_command_content_type_detector_test.rb +25 -0
  76. data/test/filename_cleaner_test.rb +14 -0
  77. data/test/fixtures/animated +0 -0
  78. data/test/fixtures/animated.unknown +0 -0
  79. data/test/fixtures/rotated.jpg +0 -0
  80. data/test/generator_test.rb +26 -24
  81. data/test/geometry_detector_test.rb +24 -0
  82. data/test/geometry_parser_test.rb +73 -0
  83. data/test/geometry_test.rb +55 -4
  84. data/test/has_attached_file_test.rb +125 -0
  85. data/test/helper.rb +38 -7
  86. data/test/integration_test.rb +105 -89
  87. data/test/interpolations_test.rb +12 -0
  88. data/test/io_adapters/abstract_adapter_test.rb +58 -0
  89. data/test/io_adapters/attachment_adapter_test.rb +120 -33
  90. data/test/io_adapters/data_uri_adapter_test.rb +60 -0
  91. data/test/io_adapters/empty_string_adapter_test.rb +17 -0
  92. data/test/io_adapters/file_adapter_test.rb +32 -1
  93. data/test/io_adapters/stringio_adapter_test.rb +29 -10
  94. data/test/io_adapters/uploaded_file_adapter_test.rb +53 -5
  95. data/test/io_adapters/uri_adapter_test.rb +102 -0
  96. data/test/matchers/validate_attachment_presence_matcher_test.rb +22 -0
  97. data/test/meta_class_test.rb +32 -0
  98. data/test/paperclip_missing_attachment_styles_test.rb +4 -8
  99. data/test/paperclip_test.rb +27 -51
  100. data/test/plural_cache_test.rb +36 -0
  101. data/test/processor_test.rb +16 -0
  102. data/test/rake_test.rb +103 -0
  103. data/test/schema_test.rb +179 -77
  104. data/test/storage/filesystem_test.rb +26 -3
  105. data/test/storage/fog_test.rb +181 -3
  106. data/test/storage/s3_test.rb +239 -4
  107. data/test/style_test.rb +18 -14
  108. data/test/tempfile_factory_test.rb +13 -0
  109. data/test/thumbnail_test.rb +96 -16
  110. data/test/validators/attachment_content_type_validator_test.rb +181 -55
  111. data/test/validators/attachment_size_validator_test.rb +10 -0
  112. data/test/validators_test.rb +8 -1
  113. metadata +126 -92
  114. data/Gemfile.lock +0 -157
  115. data/features/support/fixtures/.boot_config.rb.swo +0 -0
  116. data/images.rake +0 -21
  117. data/lib/.DS_Store +0 -0
  118. data/lib/paperclip/.DS_Store +0 -0
  119. data/lib/paperclip/attachment_options.rb +0 -9
  120. data/lib/paperclip/instance_methods.rb +0 -35
  121. data/test/attachment_options_test.rb +0 -27
@@ -1,7 +1,8 @@
1
1
  module Paperclip
2
- class UploadedFileAdapter
2
+ class UploadedFileAdapter < AbstractAdapter
3
3
  def initialize(target)
4
4
  @target = target
5
+ cache_current_values
5
6
 
6
7
  if @target.respond_to?(:tempfile)
7
8
  @tempfile = copy_to_tempfile(@target.tempfile)
@@ -10,52 +11,28 @@ module Paperclip
10
11
  end
11
12
  end
12
13
 
13
- def original_filename
14
- @target.original_filename
14
+ class << self
15
+ attr_accessor :content_type_detector
15
16
  end
16
17
 
17
- def content_type
18
- @target.content_type
19
- end
20
-
21
- def fingerprint
22
- @fingerprint ||= Digest::MD5.file(path).to_s
23
- end
24
-
25
- def size
26
- File.size(path)
27
- end
28
-
29
- def nil?
30
- false
31
- end
32
-
33
- def read(length = nil, buffer = nil)
34
- @tempfile.read(length, buffer)
35
- end
36
-
37
- # We don't use this directly, but aws/sdk does.
38
- def rewind
39
- @tempfile.rewind
40
- end
18
+ private
41
19
 
42
- def eof?
43
- @tempfile.eof?
20
+ def cache_current_values
21
+ self.original_filename = @target.original_filename
22
+ @content_type = determine_content_type
23
+ @size = File.size(@target.path)
44
24
  end
45
25
 
46
- def path
47
- @tempfile.path
26
+ def content_type_detector
27
+ self.class.content_type_detector
48
28
  end
49
29
 
50
- private
51
-
52
- def copy_to_tempfile(src)
53
- extension = File.extname(original_filename)
54
- basename = File.basename(original_filename, extension)
55
- dest = Tempfile.new([basename, extension])
56
- dest.binmode
57
- FileUtils.cp(src.path, dest.path)
58
- dest
30
+ def determine_content_type
31
+ content_type = @target.content_type.to_s.strip
32
+ if content_type_detector
33
+ content_type = content_type_detector.new(@target.path).detect
34
+ end
35
+ content_type
59
36
  end
60
37
  end
61
38
  end
@@ -0,0 +1,44 @@
1
+ require 'open-uri'
2
+
3
+ module Paperclip
4
+ class UriAdapter < AbstractAdapter
5
+ def initialize(target)
6
+ @target = target
7
+ @content = download_content
8
+ cache_current_values
9
+ @tempfile = copy_to_tempfile(@content)
10
+ end
11
+
12
+ attr_writer :content_type
13
+
14
+ private
15
+
16
+ def download_content
17
+ open(@target)
18
+ end
19
+
20
+ def cache_current_values
21
+ @original_filename = @target.path.split("/").last
22
+ @original_filename ||= "index.html"
23
+ self.original_filename = @original_filename.strip
24
+
25
+ @content_type = @content.content_type if @content.respond_to?(:content_type)
26
+ @content_type ||= "text/html"
27
+
28
+ @size = @content.size
29
+ end
30
+
31
+ def copy_to_tempfile(src)
32
+ while data = src.read(16*1024)
33
+ destination.write(data)
34
+ end
35
+ src.close
36
+ destination.rewind
37
+ destination
38
+ end
39
+ end
40
+ end
41
+
42
+ Paperclip.io_adapters.register Paperclip::UriAdapter do |target|
43
+ target.kind_of?(URI)
44
+ end
@@ -20,7 +20,7 @@ module Paperclip
20
20
  def matches? subject
21
21
  @subject = subject
22
22
  @subject = @subject.class unless Class === @subject
23
- responds? && has_column? && included?
23
+ responds? && has_column?
24
24
  end
25
25
 
26
26
  def failure_message
@@ -47,10 +47,6 @@ module Paperclip
47
47
  def has_column?
48
48
  @subject.column_names.include?("#{@attachment_name}_file_name")
49
49
  end
50
-
51
- def included?
52
- @subject.ancestors.include?(Paperclip::InstanceMethods)
53
- end
54
50
  end
55
51
  end
56
52
  end
@@ -39,18 +39,10 @@ module Paperclip
39
39
  end
40
40
 
41
41
  def failure_message
42
- "".tap do |str|
43
- str << "Content types #{@allowed_types.join(", ")} should be accepted" if @allowed_types.present?
44
- str << "\n" if @allowed_types.present? && @rejected_types.present?
45
- str << "Content types #{@rejected_types.join(", ")} should be rejected by #{@attachment_name}" if @rejected_types.present?
46
- end
47
- end
48
-
49
- def negative_failure_message
50
- "".tap do |str|
51
- str << "Content types #{@allowed_types.join(", ")} should be rejected" if @allowed_types.present?
52
- str << "\n" if @allowed_types.present? && @rejected_types.present?
53
- str << "Content types #{@rejected_types.join(", ")} should be accepted by #{@attachment_name}" if @rejected_types.present?
42
+ "#{expected_attachment}\n".tap do |message|
43
+ message << accepted_types_and_failures
44
+ message << "\n\n" if @allowed_types.present? && @rejected_types.present?
45
+ message << rejected_types_and_failures
54
46
  end
55
47
  end
56
48
 
@@ -60,20 +52,47 @@ module Paperclip
60
52
 
61
53
  protected
62
54
 
55
+ def accepted_types_and_failures
56
+ if @allowed_types.present?
57
+ "Accept content types: #{@allowed_types.join(", ")}\n".tap do |message|
58
+ if @missing_allowed_types.any?
59
+ message << " #{@missing_allowed_types.join(", ")} were rejected."
60
+ else
61
+ message << " All were accepted successfully."
62
+ end
63
+ end
64
+ end
65
+ end
66
+ def rejected_types_and_failures
67
+ if @rejected_types.present?
68
+ "Reject content types: #{@rejected_types.join(", ")}\n".tap do |message|
69
+ if @missing_rejected_types.any?
70
+ message << " #{@missing_rejected_types.join(", ")} were accepted."
71
+ else
72
+ message << " All were rejected successfully."
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ def expected_attachment
79
+ "Expected #{@attachment_name}:\n"
80
+ end
81
+
63
82
  def type_allowed?(type)
64
- file = Paperclip.io_adapters.for(StringIO.new("."))
65
- file.content_type = type
66
- @subject.attachment_for(@attachment_name).assign(file)
83
+ @subject.send("#{@attachment_name}_content_type=", type)
67
84
  @subject.valid?
68
85
  @subject.errors[:"#{@attachment_name}_content_type"].blank?
69
86
  end
70
87
 
71
88
  def allowed_types_allowed?
72
- @allowed_types.all? { |type| type_allowed?(type) }
89
+ @missing_allowed_types ||= @allowed_types.reject { |type| type_allowed?(type) }
90
+ @missing_allowed_types.none?
73
91
  end
74
92
 
75
93
  def rejected_types_rejected?
76
- !@rejected_types.any? { |type| type_allowed?(type) }
94
+ @missing_rejected_types ||= @rejected_types.select { |type| type_allowed?(type) }
95
+ @missing_rejected_types.none?
77
96
  end
78
97
  end
79
98
  end
@@ -46,7 +46,11 @@ module Paperclip
46
46
  @file = StringIO.new(".")
47
47
  @subject.send(@attachment_name).assign(@file)
48
48
  @subject.valid?
49
- @subject.errors[:"#{@attachment_name}"].blank?
49
+ expected_message = [
50
+ @attachment_name.to_s.titleize,
51
+ I18n.t(:blank, scope: [:errors, :messages])
52
+ ].join(' ')
53
+ @subject.errors.full_messages.exclude?(expected_message)
50
54
  end
51
55
  end
52
56
  end
@@ -15,7 +15,7 @@ module Paperclip
15
15
  #
16
16
  # And _include_ the module:
17
17
  #
18
- # Spec::Runner.configure do |config|
18
+ # RSpec.configure do |config|
19
19
  # config.include Paperclip::Shoulda::Matchers
20
20
  # end
21
21
  #
@@ -41,7 +41,7 @@ module Paperclip
41
41
  #
42
42
  # class ActiveSupport::TestCase
43
43
  # extend Paperclip::Shoulda::Matchers
44
- #
44
+ #
45
45
  # #...other initializers...#
46
46
  # end
47
47
  #
@@ -57,7 +57,7 @@ module Paperclip
57
57
  # should validate_attachment_size(:avatar).
58
58
  # less_than(2.megabytes)
59
59
  # end
60
- #
60
+ #
61
61
  module Matchers
62
62
  end
63
63
  end
@@ -1,16 +1,14 @@
1
-
1
+ require 'paperclip/attachment_registry'
2
2
  require 'set'
3
+
3
4
  module Paperclip
4
5
  class << self
5
- attr_accessor :classes_with_attachments
6
6
  attr_writer :registered_attachments_styles_path
7
7
  def registered_attachments_styles_path
8
8
  @registered_attachments_styles_path ||= Rails.root.join('public/system/paperclip_attachments.yml').to_s
9
9
  end
10
10
  end
11
11
 
12
- self.classes_with_attachments = Set.new
13
-
14
12
  # Get list of styles saved on previous deploy (running rake paperclip:refresh:missing_styles)
15
13
  def self.get_registered_attachments_styles
16
14
  YAML.load_file(Paperclip.registered_attachments_styles_path)
@@ -36,18 +34,15 @@ module Paperclip
36
34
  # }
37
35
  def self.current_attachments_styles
38
36
  Hash.new.tap do |current_styles|
39
- Paperclip.classes_with_attachments.each do |klass_name|
40
- klass = Paperclip.class_for(klass_name)
41
- klass.attachment_definitions.each do |attachment_name, attachment_attributes|
42
- # TODO: is it even possible to take into account Procs?
43
- next if attachment_attributes[:styles].kind_of?(Proc)
44
- attachment_attributes[:styles].try(:keys).try(:each) do |style_name|
45
- klass_sym = klass.to_s.to_sym
46
- current_styles[klass_sym] ||= Hash.new
47
- current_styles[klass_sym][attachment_name.to_sym] ||= Array.new
48
- current_styles[klass_sym][attachment_name.to_sym] << style_name.to_sym
49
- current_styles[klass_sym][attachment_name.to_sym].map!(&:to_s).sort!.map!(&:to_sym).uniq!
50
- end
37
+ Paperclip::AttachmentRegistry.each_definition do |klass, attachment_name, attachment_attributes|
38
+ # TODO: is it even possible to take into account Procs?
39
+ next if attachment_attributes[:styles].kind_of?(Proc)
40
+ attachment_attributes[:styles].try(:keys).try(:each) do |style_name|
41
+ klass_sym = klass.to_s.to_sym
42
+ current_styles[klass_sym] ||= Hash.new
43
+ current_styles[klass_sym][attachment_name.to_sym] ||= Array.new
44
+ current_styles[klass_sym][attachment_name.to_sym] << style_name.to_sym
45
+ current_styles[klass_sym][attachment_name.to_sym].map!(&:to_s).sort!.map!(&:to_sym).uniq!
51
46
  end
52
47
  end
53
48
  end
@@ -32,6 +32,18 @@ module Paperclip
32
32
  def self.make file, options = {}, attachment = nil
33
33
  new(file, options, attachment).make
34
34
  end
35
+
36
+ # The convert method runs the convert binary with the provided arguments.
37
+ # See Paperclip.run for the available options.
38
+ def convert(arguments = "", local_options = {})
39
+ Paperclip.run('convert', arguments, local_options)
40
+ end
41
+
42
+ # The identify method runs the identify binary with the provided arguments.
43
+ # See Paperclip.run for the available options.
44
+ def identify(arguments = "", local_options = {})
45
+ Paperclip.run('identify', arguments, local_options)
46
+ end
35
47
  end
36
48
 
37
49
  module ProcessorHelpers
@@ -5,10 +5,14 @@ module Paperclip
5
5
  require 'rails'
6
6
 
7
7
  class Railtie < Rails::Railtie
8
- initializer 'paperclip.insert_into_active_record' do
8
+ initializer 'paperclip.insert_into_active_record' do |app|
9
9
  ActiveSupport.on_load :active_record do
10
10
  Paperclip::Railtie.insert
11
11
  end
12
+
13
+ if app.config.respond_to?(:paperclip_defaults)
14
+ Paperclip::Attachment.default_options.merge!(app.config.paperclip_defaults)
15
+ end
12
16
  end
13
17
 
14
18
  rake_tasks { load "tasks/paperclip.rake" }
@@ -1,39 +1,75 @@
1
+ require 'active_support/deprecation'
2
+
1
3
  module Paperclip
2
- # Provides two helpers that can be used in migrations.
3
- #
4
- # In order to use this module, the target class should implement a
5
- # +column+ method that takes the column name and type, both as symbols,
6
- # as well as a +remove_column+ method that takes a table and column name,
7
- # also both symbols.
4
+ # Provides helper methods that can be used in migrations.
8
5
  module Schema
9
- @@columns = {:file_name => :string,
10
- :content_type => :string,
11
- :file_size => :integer,
12
- :updated_at => :datetime}
6
+ COLUMNS = {:file_name => :string,
7
+ :content_type => :string,
8
+ :file_size => :integer,
9
+ :updated_at => :datetime}
10
+
11
+ def self.included(base)
12
+ ActiveRecord::ConnectionAdapters::Table.send :include, TableDefinition
13
+ ActiveRecord::ConnectionAdapters::TableDefinition.send :include, TableDefinition
14
+ ActiveRecord::ConnectionAdapters::AbstractAdapter.send :include, Statements
13
15
 
14
- def has_attached_file(attachment_name)
15
- with_columns_for(attachment_name) do |column_name, column_type|
16
- column(column_name, column_type)
16
+ if defined?(ActiveRecord::Migration::CommandRecorder) # Rails 3.1+
17
+ ActiveRecord::Migration::CommandRecorder.send :include, CommandRecorder
17
18
  end
18
19
  end
19
20
 
20
- def drop_attached_file(table_name, attachment_name)
21
- with_columns_for(attachment_name) do |column_name, column_type|
22
- remove_column(table_name, column_name)
21
+ module Statements
22
+ def add_attachment(table_name, *attachment_names)
23
+ raise ArgumentError, "Please specify attachment name in your add_attachment call in your migration." if attachment_names.empty?
24
+
25
+ attachment_names.each do |attachment_name|
26
+ COLUMNS.each_pair do |column_name, column_type|
27
+ add_column(table_name, "#{attachment_name}_#{column_name}", column_type)
28
+ end
29
+ end
30
+ end
31
+
32
+ def remove_attachment(table_name, *attachment_names)
33
+ raise ArgumentError, "Please specify attachment name in your remove_attachment call in your migration." if attachment_names.empty?
34
+
35
+ attachment_names.each do |attachment_name|
36
+ COLUMNS.each_pair do |column_name, column_type|
37
+ remove_column(table_name, "#{attachment_name}_#{column_name}")
38
+ end
39
+ end
40
+ end
41
+
42
+ def drop_attached_file(*args)
43
+ ActiveSupport::Deprecation.warn "Method `drop_attached_file` in the migration has been deprecated and will be replaced by `remove_attachment`."
44
+ remove_attachment(*args)
23
45
  end
24
46
  end
25
47
 
26
- protected
48
+ module TableDefinition
49
+ def attachment(*attachment_names)
50
+ attachment_names.each do |attachment_name|
51
+ COLUMNS.each_pair do |column_name, column_type|
52
+ column("#{attachment_name}_#{column_name}", column_type)
53
+ end
54
+ end
55
+ end
27
56
 
28
- def with_columns_for(attachment_name)
29
- @@columns.each do |suffix, column_type|
30
- column_name = full_column_name(attachment_name, suffix)
31
- yield column_name, column_type
57
+ def has_attached_file(*attachment_names)
58
+ ActiveSupport::Deprecation.warn "Method `t.has_attached_file` in the migration has been deprecated and will be replaced by `t.attachment`."
59
+ attachment(*attachment_names)
32
60
  end
33
61
  end
34
62
 
35
- def full_column_name(attachment_name, column_name)
36
- "#{attachment_name}_#{column_name}".to_sym
63
+ module CommandRecorder
64
+ def add_attachment(*args)
65
+ record(:add_attachment, args)
66
+ end
67
+
68
+ private
69
+
70
+ def invert_add_attachment(args)
71
+ [:remove_attachment, args]
72
+ end
37
73
  end
38
74
  end
39
75
  end
@@ -3,7 +3,7 @@ module Paperclip
3
3
  # The default place to store attachments is in the filesystem. Files on the local
4
4
  # filesystem can be very easily served by Apache without requiring a hit to your app.
5
5
  # They also can be processed more easily after they've been saved, as they're just
6
- # normal files. There is one Filesystem-specific option for has_attached_file.
6
+ # normal files. There are two Filesystem-specific options for has_attached_file:
7
7
  # * +path+: The location of the repository of attachments on disk. This can (and, in
8
8
  # almost all cases, should) be coordinated with the value of the +url+ option to
9
9
  # allow files to be saved into a place where Apache can serve them without
@@ -15,6 +15,12 @@ module Paperclip
15
15
  # public directory.
16
16
  # See Paperclip::Attachment#interpolate for more information on variable interpolaton.
17
17
  # :path => "/var/app/attachments/:class/:id/:style/:basename.:extension"
18
+ # * +override_file_permissions+: This allows you to override the file permissions for files
19
+ # saved by paperclip. If you set this to an explicit octal value (0755, 0644, etc) then
20
+ # that value will be used to set the permissions for an uploaded file. The default is 0666.
21
+ # If you set :override_file_permissions to false, the chmod will be skipped. This allows
22
+ # you to use paperclip on filesystems that don't understand unix file permissions, and has the
23
+ # added benefit of using the storage directories default umask on those that do.
18
24
  module Filesystem
19
25
  def self.extended base
20
26
  end
@@ -30,12 +36,20 @@ module Paperclip
30
36
  def flush_writes #:nodoc:
31
37
  @queued_for_write.each do |style_name, file|
32
38
  FileUtils.mkdir_p(File.dirname(path(style_name)))
33
- File.open(path(style_name), "wb") do |new_file|
34
- while chunk = file.read(16 * 1024)
35
- new_file.write(chunk)
39
+ begin
40
+ FileUtils.mv(file.path, path(style_name))
41
+ rescue SystemCallError
42
+ File.open(path(style_name), "wb") do |new_file|
43
+ while chunk = file.read(16 * 1024)
44
+ new_file.write(chunk)
45
+ end
36
46
  end
37
47
  end
38
- FileUtils.chmod(0666&~File.umask, path(style_name))
48
+ unless @options[:override_file_permissions] == false
49
+ resolved_chmod = (@options[:override_file_permissions] &~ 0111) || (0666 &~ File.umask)
50
+ FileUtils.chmod( resolved_chmod, path(style_name) )
51
+ end
52
+ file.rewind
39
53
  end
40
54
 
41
55
  after_flush_writes # allows attachment to clean up temp files
@@ -66,6 +80,10 @@ module Paperclip
66
80
  end
67
81
  @queued_for_delete = []
68
82
  end
83
+
84
+ def copy_to_local_file(style, local_dest_path)
85
+ FileUtils.cp(path(style), local_dest_path)
86
+ end
69
87
  end
70
88
 
71
89
  end
@@ -18,6 +18,8 @@ module Paperclip
18
18
  # store your files. Remember that the bucket must be unique across
19
19
  # all of Amazon S3. If the bucket does not exist, Paperclip will
20
20
  # attempt to create it.
21
+ # * +fog_file*: This can be hash or lambda returning hash. The
22
+ # value is used as base properties for new uploaded file.
21
23
  # * +path+: This is the key under the bucket in which the file will
22
24
  # be stored. The URL will be constructed from the bucket and the
23
25
  # path. This is what you will want to interpolate. Keys should be
@@ -42,7 +44,7 @@ module Paperclip
42
44
 
43
45
  base.instance_eval do
44
46
  unless @options[:url].to_s.match(/^:fog.*url$/)
45
- @options[:path] = @options[:path].gsub(/:url/, @options[:url])
47
+ @options[:path] = @options[:path].gsub(/:url/, @options[:url]).gsub(/^:rails_root\/public\/system\//, '')
46
48
  @options[:url] = ':fog_public_url'
47
49
  end
48
50
  Paperclip.interpolates(:fog_public_url) do |attachment, style|
@@ -66,12 +68,28 @@ module Paperclip
66
68
  end
67
69
 
68
70
  def fog_file
69
- @fog_file ||= @options[:fog_file] || {}
71
+ @fog_file ||= begin
72
+ value = @options[:fog_file]
73
+ if !value
74
+ {}
75
+ elsif value.respond_to?(:call)
76
+ value.call(self)
77
+ else
78
+ value
79
+ end
80
+ end
70
81
  end
71
82
 
72
- def fog_public
73
- return @fog_public if defined?(@fog_public)
74
- @fog_public = defined?(@options[:fog_public]) ? @options[:fog_public] : true
83
+ def fog_public(style = default_style)
84
+ if @options.has_key?(:fog_public)
85
+ if @options[:fog_public].respond_to?(:has_key?) && @options[:fog_public].has_key?(style)
86
+ @options[:fog_public][style]
87
+ else
88
+ @options[:fog_public]
89
+ end
90
+ else
91
+ true
92
+ end
75
93
  end
76
94
 
77
95
  def flush_writes
@@ -82,7 +100,7 @@ module Paperclip
82
100
  directory.files.create(fog_file.merge(
83
101
  :body => file,
84
102
  :key => path(style),
85
- :public => fog_public,
103
+ :public => fog_public(style),
86
104
  :content_type => file.content_type
87
105
  ))
88
106
  rescue Excon::Errors::NotFound
@@ -90,6 +108,8 @@ module Paperclip
90
108
  retried = true
91
109
  directory.save
92
110
  retry
111
+ ensure
112
+ file.rewind
93
113
  end
94
114
  end
95
115
 
@@ -108,27 +128,30 @@ module Paperclip
108
128
 
109
129
  def public_url(style = default_style)
110
130
  if @options[:fog_host]
111
- host = if @options[:fog_host].respond_to?(:call)
112
- @options[:fog_host].call(self)
113
- else
114
- (@options[:fog_host] =~ /%d/) ? @options[:fog_host] % (path(style).hash % 4) : @options[:fog_host]
115
- end
116
-
117
- "#{host}/#{path(style)}"
131
+ "#{dynamic_fog_host_for_style(style)}/#{path(style)}"
118
132
  else
119
133
  if fog_credentials[:provider] == 'AWS'
120
- if @options[:fog_directory].to_s =~ Fog::AWS_BUCKET_SUBDOMAIN_RESTRICTON_REGEX
121
- "https://#{@options[:fog_directory]}.s3.amazonaws.com/#{path(style)}"
122
- else
123
- # directory is not a valid subdomain, so use path style for access
124
- "https://s3.amazonaws.com/#{@options[:fog_directory]}/#{path(style)}"
125
- end
134
+ "https://#{host_name_for_directory}/#{path(style)}"
126
135
  else
127
136
  directory.files.new(:key => path(style)).public_url
128
137
  end
129
138
  end
130
139
  end
131
140
 
141
+ def expiring_url(time = (Time.now + 3600), style = default_style)
142
+ if directory.files.respond_to?(:get_http_url)
143
+ expiring_url = directory.files.get_http_url(path(style), time)
144
+
145
+ if @options[:fog_host]
146
+ expiring_url.gsub!(/#{host_name_for_directory}/, dynamic_fog_host_for_style(style))
147
+ end
148
+ else
149
+ expiring_url = public_url
150
+ end
151
+
152
+ return expiring_url
153
+ end
154
+
132
155
  def parse_credentials(creds)
133
156
  creds = find_credentials(creds).stringify_keys
134
157
  env = Object.const_defined?(:Rails) ? Rails.env : nil
@@ -137,17 +160,33 @@ module Paperclip
137
160
 
138
161
  def copy_to_local_file(style, local_dest_path)
139
162
  log("copying #{path(style)} to local file #{local_dest_path}")
140
- local_file = ::File.open(local_dest_path, 'wb')
141
- file = directory.files.get(path(style))
142
- local_file.write(file.body)
143
- local_file.close
144
- rescue Fog::Errors::Error => e
163
+ ::File.open(local_dest_path, 'wb') do |local_file|
164
+ file = directory.files.get(path(style))
165
+ local_file.write(file.body)
166
+ end
167
+ rescue ::Fog::Errors::Error => e
145
168
  warn("#{e} - cannot copy #{path(style)} to local file #{local_dest_path}")
146
169
  false
147
170
  end
148
171
 
149
172
  private
150
173
 
174
+ def dynamic_fog_host_for_style(style)
175
+ if @options[:fog_host].respond_to?(:call)
176
+ @options[:fog_host].call(self)
177
+ else
178
+ (@options[:fog_host] =~ /%d/) ? @options[:fog_host] % (path(style).hash % 4) : @options[:fog_host]
179
+ end
180
+ end
181
+
182
+ def host_name_for_directory
183
+ if @options[:fog_directory].to_s =~ Fog::AWS_BUCKET_SUBDOMAIN_RESTRICTON_REGEX
184
+ "#{@options[:fog_directory]}.s3.amazonaws.com"
185
+ else
186
+ "s3.amazonaws.com/#{@options[:fog_directory]}"
187
+ end
188
+ end
189
+
151
190
  def find_credentials(creds)
152
191
  case creds
153
192
  when File
@@ -175,7 +214,7 @@ module Paperclip
175
214
  else
176
215
  @options[:fog_directory]
177
216
  end
178
-
217
+
179
218
  @directory ||= connection.directories.new(:key => dir)
180
219
  end
181
220
  end