jr-paperclip 7.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. checksums.yaml +7 -0
  2. data/.github/FUNDING.yml +3 -0
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
  4. data/.github/ISSUE_TEMPLATE/custom.md +10 -0
  5. data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  6. data/.github/workflows/reviewdog.yml +23 -0
  7. data/.github/workflows/test.yml +46 -0
  8. data/.gitignore +19 -0
  9. data/.qlty/.gitignore +7 -0
  10. data/.qlty/qlty.toml +89 -0
  11. data/.rubocop.yml +1060 -0
  12. data/Appraisals +29 -0
  13. data/CONTRIBUTING.md +85 -0
  14. data/Gemfile +17 -0
  15. data/LICENSE +25 -0
  16. data/NEWS +567 -0
  17. data/README.md +1083 -0
  18. data/RELEASING.md +17 -0
  19. data/Rakefile +52 -0
  20. data/bin/console +11 -0
  21. data/features/basic_integration.feature +85 -0
  22. data/features/migration.feature +29 -0
  23. data/features/rake_tasks.feature +62 -0
  24. data/features/step_definitions/attachment_steps.rb +121 -0
  25. data/features/step_definitions/html_steps.rb +15 -0
  26. data/features/step_definitions/rails_steps.rb +271 -0
  27. data/features/step_definitions/s3_steps.rb +16 -0
  28. data/features/step_definitions/web_steps.rb +106 -0
  29. data/features/support/env.rb +12 -0
  30. data/features/support/file_helpers.rb +34 -0
  31. data/features/support/fixtures/boot_config.txt +15 -0
  32. data/features/support/fixtures/gemfile.txt +5 -0
  33. data/features/support/fixtures/preinitializer.txt +20 -0
  34. data/features/support/paths.rb +28 -0
  35. data/features/support/rails.rb +39 -0
  36. data/features/support/selectors.rb +19 -0
  37. data/features/support/webmock_setup.rb +8 -0
  38. data/gemfiles/7.0.gemfile +20 -0
  39. data/gemfiles/7.1.gemfile +20 -0
  40. data/gemfiles/7.2.gemfile +20 -0
  41. data/gemfiles/8.0.gemfile +20 -0
  42. data/gemfiles/8.1.gemfile +20 -0
  43. data/lib/generators/paperclip/USAGE +8 -0
  44. data/lib/generators/paperclip/paperclip_generator.rb +36 -0
  45. data/lib/generators/paperclip/templates/paperclip_migration.rb.erb +15 -0
  46. data/lib/jr-paperclip.rb +1 -0
  47. data/lib/paperclip/attachment.rb +634 -0
  48. data/lib/paperclip/attachment_registry.rb +60 -0
  49. data/lib/paperclip/callbacks.rb +42 -0
  50. data/lib/paperclip/content_type_detector.rb +85 -0
  51. data/lib/paperclip/errors.rb +34 -0
  52. data/lib/paperclip/file_command_content_type_detector.rb +28 -0
  53. data/lib/paperclip/filename_cleaner.rb +15 -0
  54. data/lib/paperclip/geometry.rb +157 -0
  55. data/lib/paperclip/geometry_detector_factory.rb +45 -0
  56. data/lib/paperclip/geometry_parser_factory.rb +31 -0
  57. data/lib/paperclip/glue.rb +18 -0
  58. data/lib/paperclip/has_attached_file.rb +116 -0
  59. data/lib/paperclip/helpers.rb +60 -0
  60. data/lib/paperclip/interpolations/plural_cache.rb +18 -0
  61. data/lib/paperclip/interpolations.rb +205 -0
  62. data/lib/paperclip/io_adapters/abstract_adapter.rb +75 -0
  63. data/lib/paperclip/io_adapters/attachment_adapter.rb +56 -0
  64. data/lib/paperclip/io_adapters/data_uri_adapter.rb +22 -0
  65. data/lib/paperclip/io_adapters/empty_string_adapter.rb +19 -0
  66. data/lib/paperclip/io_adapters/file_adapter.rb +26 -0
  67. data/lib/paperclip/io_adapters/http_url_proxy_adapter.rb +16 -0
  68. data/lib/paperclip/io_adapters/identity_adapter.rb +17 -0
  69. data/lib/paperclip/io_adapters/nil_adapter.rb +37 -0
  70. data/lib/paperclip/io_adapters/registry.rb +36 -0
  71. data/lib/paperclip/io_adapters/stringio_adapter.rb +36 -0
  72. data/lib/paperclip/io_adapters/uploaded_file_adapter.rb +44 -0
  73. data/lib/paperclip/io_adapters/uri_adapter.rb +78 -0
  74. data/lib/paperclip/locales/en.yml +18 -0
  75. data/lib/paperclip/locales/fr.yml +18 -0
  76. data/lib/paperclip/locales/gd.yml +20 -0
  77. data/lib/paperclip/logger.rb +21 -0
  78. data/lib/paperclip/matchers/have_attached_file_matcher.rb +54 -0
  79. data/lib/paperclip/matchers/validate_attachment_content_type_matcher.rb +101 -0
  80. data/lib/paperclip/matchers/validate_attachment_presence_matcher.rb +59 -0
  81. data/lib/paperclip/matchers/validate_attachment_size_matcher.rb +97 -0
  82. data/lib/paperclip/matchers.rb +64 -0
  83. data/lib/paperclip/media_type_spoof_detector.rb +93 -0
  84. data/lib/paperclip/missing_attachment_styles.rb +84 -0
  85. data/lib/paperclip/processor.rb +56 -0
  86. data/lib/paperclip/processor_helpers.rb +52 -0
  87. data/lib/paperclip/rails_environment.rb +21 -0
  88. data/lib/paperclip/railtie.rb +31 -0
  89. data/lib/paperclip/schema.rb +104 -0
  90. data/lib/paperclip/storage/filesystem.rb +99 -0
  91. data/lib/paperclip/storage/fog.rb +262 -0
  92. data/lib/paperclip/storage/s3.rb +497 -0
  93. data/lib/paperclip/storage.rb +3 -0
  94. data/lib/paperclip/style.rb +106 -0
  95. data/lib/paperclip/tempfile.rb +42 -0
  96. data/lib/paperclip/tempfile_factory.rb +22 -0
  97. data/lib/paperclip/thumbnail.rb +131 -0
  98. data/lib/paperclip/url_generator.rb +83 -0
  99. data/lib/paperclip/validators/attachment_content_type_validator.rb +95 -0
  100. data/lib/paperclip/validators/attachment_file_name_validator.rb +82 -0
  101. data/lib/paperclip/validators/attachment_file_type_ignorance_validator.rb +28 -0
  102. data/lib/paperclip/validators/attachment_presence_validator.rb +28 -0
  103. data/lib/paperclip/validators/attachment_size_validator.rb +126 -0
  104. data/lib/paperclip/validators/media_type_spoof_detection_validator.rb +29 -0
  105. data/lib/paperclip/validators.rb +73 -0
  106. data/lib/paperclip/version.rb +3 -0
  107. data/lib/paperclip.rb +215 -0
  108. data/lib/tasks/paperclip.rake +140 -0
  109. data/paperclip.gemspec +51 -0
  110. data/shoulda_macros/paperclip.rb +134 -0
  111. data/spec/database.yml +4 -0
  112. data/spec/paperclip/attachment_definitions_spec.rb +13 -0
  113. data/spec/paperclip/attachment_processing_spec.rb +79 -0
  114. data/spec/paperclip/attachment_registry_spec.rb +158 -0
  115. data/spec/paperclip/attachment_spec.rb +1617 -0
  116. data/spec/paperclip/content_type_detector_spec.rb +58 -0
  117. data/spec/paperclip/file_command_content_type_detector_spec.rb +40 -0
  118. data/spec/paperclip/filename_cleaner_spec.rb +13 -0
  119. data/spec/paperclip/geometry_detector_spec.rb +47 -0
  120. data/spec/paperclip/geometry_parser_spec.rb +73 -0
  121. data/spec/paperclip/geometry_spec.rb +267 -0
  122. data/spec/paperclip/glue_spec.rb +63 -0
  123. data/spec/paperclip/has_attached_file_spec.rb +78 -0
  124. data/spec/paperclip/integration_spec.rb +702 -0
  125. data/spec/paperclip/interpolations_spec.rb +270 -0
  126. data/spec/paperclip/io_adapters/abstract_adapter_spec.rb +160 -0
  127. data/spec/paperclip/io_adapters/attachment_adapter_spec.rb +167 -0
  128. data/spec/paperclip/io_adapters/data_uri_adapter_spec.rb +88 -0
  129. data/spec/paperclip/io_adapters/empty_string_adapter_spec.rb +17 -0
  130. data/spec/paperclip/io_adapters/file_adapter_spec.rb +134 -0
  131. data/spec/paperclip/io_adapters/http_url_proxy_adapter_spec.rb +142 -0
  132. data/spec/paperclip/io_adapters/identity_adapter_spec.rb +8 -0
  133. data/spec/paperclip/io_adapters/nil_adapter_spec.rb +25 -0
  134. data/spec/paperclip/io_adapters/registry_spec.rb +35 -0
  135. data/spec/paperclip/io_adapters/stringio_adapter_spec.rb +64 -0
  136. data/spec/paperclip/io_adapters/uploaded_file_adapter_spec.rb +146 -0
  137. data/spec/paperclip/io_adapters/uri_adapter_spec.rb +231 -0
  138. data/spec/paperclip/matchers/have_attached_file_matcher_spec.rb +19 -0
  139. data/spec/paperclip/matchers/validate_attachment_content_type_matcher_spec.rb +108 -0
  140. data/spec/paperclip/matchers/validate_attachment_presence_matcher_spec.rb +69 -0
  141. data/spec/paperclip/matchers/validate_attachment_size_matcher_spec.rb +88 -0
  142. data/spec/paperclip/media_type_spoof_detector_spec.rb +126 -0
  143. data/spec/paperclip/meta_class_spec.rb +30 -0
  144. data/spec/paperclip/paperclip_missing_attachment_styles_spec.rb +88 -0
  145. data/spec/paperclip/paperclip_spec.rb +196 -0
  146. data/spec/paperclip/plural_cache_spec.rb +37 -0
  147. data/spec/paperclip/processor_helpers_spec.rb +57 -0
  148. data/spec/paperclip/processor_spec.rb +26 -0
  149. data/spec/paperclip/rails_environment_spec.rb +30 -0
  150. data/spec/paperclip/rake_spec.rb +103 -0
  151. data/spec/paperclip/schema_spec.rb +298 -0
  152. data/spec/paperclip/storage/filesystem_spec.rb +102 -0
  153. data/spec/paperclip/storage/fog_spec.rb +606 -0
  154. data/spec/paperclip/storage/s3_live_spec.rb +188 -0
  155. data/spec/paperclip/storage/s3_spec.rb +1974 -0
  156. data/spec/paperclip/style_spec.rb +251 -0
  157. data/spec/paperclip/tempfile_factory_spec.rb +33 -0
  158. data/spec/paperclip/tempfile_spec.rb +35 -0
  159. data/spec/paperclip/thumbnail_spec.rb +504 -0
  160. data/spec/paperclip/url_generator_spec.rb +231 -0
  161. data/spec/paperclip/validators/attachment_content_type_validator_spec.rb +410 -0
  162. data/spec/paperclip/validators/attachment_file_name_validator_spec.rb +249 -0
  163. data/spec/paperclip/validators/attachment_presence_validator_spec.rb +85 -0
  164. data/spec/paperclip/validators/attachment_size_validator_spec.rb +325 -0
  165. data/spec/paperclip/validators/media_type_spoof_detection_validator_spec.rb +48 -0
  166. data/spec/paperclip/validators_spec.rb +179 -0
  167. data/spec/spec_helper.rb +52 -0
  168. data/spec/support/assertions.rb +84 -0
  169. data/spec/support/fake_model.rb +24 -0
  170. data/spec/support/fake_rails.rb +12 -0
  171. data/spec/support/fixtures/12k.png +0 -0
  172. data/spec/support/fixtures/50x50.png +0 -0
  173. data/spec/support/fixtures/5k.png +0 -0
  174. data/spec/support/fixtures/animated +0 -0
  175. data/spec/support/fixtures/animated.gif +0 -0
  176. data/spec/support/fixtures/animated.unknown +0 -0
  177. data/spec/support/fixtures/aws_s3.yml +13 -0
  178. data/spec/support/fixtures/bad.png +1 -0
  179. data/spec/support/fixtures/empty.html +1 -0
  180. data/spec/support/fixtures/empty.xlsx +0 -0
  181. data/spec/support/fixtures/fog.yml +8 -0
  182. data/spec/support/fixtures/rotated.jpg +0 -0
  183. data/spec/support/fixtures/s3.yml +8 -0
  184. data/spec/support/fixtures/sample.xlsm +0 -0
  185. data/spec/support/fixtures/spaced file.jpg +0 -0
  186. data/spec/support/fixtures/spaced file.png +0 -0
  187. data/spec/support/fixtures/text.txt +1 -0
  188. data/spec/support/fixtures/twopage.pdf +0 -0
  189. data/spec/support/fixtures/uppercase.PNG +0 -0
  190. data/spec/support/matchers/accept.rb +5 -0
  191. data/spec/support/matchers/exist.rb +5 -0
  192. data/spec/support/matchers/have_column.rb +23 -0
  193. data/spec/support/mock_attachment.rb +24 -0
  194. data/spec/support/mock_interpolator.rb +24 -0
  195. data/spec/support/mock_url_generator_builder.rb +26 -0
  196. data/spec/support/model_reconstruction.rb +72 -0
  197. data/spec/support/reporting.rb +11 -0
  198. data/spec/support/test_data.rb +13 -0
  199. data/spec/support/version_helper.rb +9 -0
  200. metadata +702 -0
@@ -0,0 +1,52 @@
1
+ module Paperclip
2
+ module ProcessorHelpers
3
+ class NoSuchProcessor < StandardError; end
4
+
5
+ def processor(name) #:nodoc:
6
+ @known_processors ||= {}
7
+ if @known_processors[name.to_s]
8
+ @known_processors[name.to_s]
9
+ else
10
+ name = name.to_s.camelize
11
+ load_processor(name) unless Paperclip.const_defined?(name)
12
+ processor = Paperclip.const_get(name)
13
+ @known_processors[name.to_s] = processor
14
+ end
15
+ end
16
+
17
+ def load_processor(name)
18
+ if defined?(Rails.root) && Rails.root
19
+ filename = "#{name.to_s.underscore}.rb"
20
+ directories = %w(lib/paperclip lib/paperclip_processors)
21
+
22
+ required = directories.map do |directory|
23
+ pathname = File.expand_path(Rails.root.join(directory, filename))
24
+ file_exists = File.exist?(pathname)
25
+ require pathname if file_exists
26
+ file_exists
27
+ end
28
+
29
+ unless required.any?
30
+ raise LoadError, "Could not find the '#{name}' processor in any of these paths: #{directories.join(', ')}"
31
+ end
32
+ end
33
+ end
34
+
35
+ def clear_processors!
36
+ @known_processors.try(:clear)
37
+ end
38
+
39
+ # You can add your own processor via the Paperclip configuration. Normally
40
+ # Paperclip will load all processors from the
41
+ # Rails.root/lib/paperclip_processors directory, but here you can add any
42
+ # existing class using this mechanism.
43
+ #
44
+ # Paperclip.configure do |c|
45
+ # c.register_processor :watermarker, WatermarkingProcessor.new
46
+ # end
47
+ def register_processor(name, processor)
48
+ @known_processors ||= {}
49
+ @known_processors[name.to_s] = processor
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,21 @@
1
+ module Paperclip
2
+ class RailsEnvironment
3
+ def self.get
4
+ new.get
5
+ end
6
+
7
+ def get
8
+ Rails.env if rails_exists? && rails_environment_exists?
9
+ end
10
+
11
+ private
12
+
13
+ def rails_exists?
14
+ Object.const_defined?(:Rails)
15
+ end
16
+
17
+ def rails_environment_exists?
18
+ Rails.respond_to?(:env)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,31 @@
1
+ require "paperclip"
2
+ require "paperclip/schema"
3
+
4
+ module Paperclip
5
+ require "rails"
6
+
7
+ class Railtie < Rails::Railtie
8
+ initializer "paperclip.insert_into_active_record" do |app|
9
+ ActiveSupport.on_load :active_record do
10
+ Paperclip::Railtie.insert
11
+ end
12
+
13
+ if app.config.respond_to?(:paperclip_defaults)
14
+ Paperclip::Attachment.default_options.merge!(app.config.paperclip_defaults)
15
+ end
16
+ end
17
+
18
+ rake_tasks { load "tasks/paperclip.rake" }
19
+ end
20
+
21
+ class Railtie
22
+ def self.insert
23
+ Paperclip.options[:logger] = Rails.logger
24
+
25
+ if defined?(ActiveRecord)
26
+ Paperclip.options[:logger] = ActiveRecord::Base.logger
27
+ ActiveRecord::Base.include Paperclip::Glue
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,104 @@
1
+ require "active_support/deprecation"
2
+
3
+ module Paperclip
4
+ # Provides helper methods that can be used in migrations.
5
+ module Schema
6
+ COLUMNS = { file_name: :string,
7
+ content_type: :string,
8
+ file_size: :bigint,
9
+ updated_at: :datetime }
10
+
11
+ def self.included(_base)
12
+ ActiveRecord::ConnectionAdapters::Table.include TableDefinition
13
+ ActiveRecord::ConnectionAdapters::TableDefinition.include TableDefinition
14
+ ActiveRecord::Migration.include Statements
15
+ ActiveRecord::Migration::CommandRecorder.include CommandRecorder
16
+ end
17
+
18
+ # Extract column-specific options and merge with general options
19
+ def self.column_options(options, column_name)
20
+ column_specific = options[column_name.to_sym] || {}
21
+ general_options = options.except(*COLUMNS.keys)
22
+ general_options.merge(column_specific)
23
+ end
24
+
25
+ module DeprecationHelper
26
+ private
27
+
28
+ def deprecation_warn(message)
29
+ if defined?(ActiveSupport.deprecator)
30
+ ActiveSupport.deprecator.warn(message)
31
+ elsif ActiveSupport::Deprecation.respond_to?(:warn)
32
+ ActiveSupport::Deprecation.warn(message)
33
+ end
34
+ end
35
+ end
36
+
37
+ module Statements
38
+ include DeprecationHelper
39
+
40
+ def add_attachment(table_name, *attachment_names)
41
+ if attachment_names.empty?
42
+ raise ArgumentError, "Please specify attachment name in your add_attachment call in your migration."
43
+ end
44
+
45
+ options = attachment_names.extract_options!
46
+
47
+ attachment_names.each do |attachment_name|
48
+ COLUMNS.each_pair do |column_name, column_type|
49
+ column_options = Schema.column_options(options, column_name)
50
+ add_column(table_name, "#{attachment_name}_#{column_name}", column_type, **column_options)
51
+ end
52
+ end
53
+ end
54
+
55
+ def remove_attachment(table_name, *attachment_names)
56
+ if attachment_names.empty?
57
+ raise ArgumentError, "Please specify attachment name in your remove_attachment call in your migration."
58
+ end
59
+
60
+ attachment_names.each do |attachment_name|
61
+ COLUMNS.keys.each do |column_name|
62
+ remove_column(table_name, "#{attachment_name}_#{column_name}")
63
+ end
64
+ end
65
+ end
66
+
67
+ def drop_attached_file(*args)
68
+ deprecation_warn "Method `drop_attached_file` in the migration has been deprecated and will be replaced by `remove_attachment`."
69
+ remove_attachment(*args)
70
+ end
71
+ end
72
+
73
+ module TableDefinition
74
+ include DeprecationHelper
75
+
76
+ def attachment(*attachment_names)
77
+ options = attachment_names.extract_options!
78
+ attachment_names.each do |attachment_name|
79
+ COLUMNS.each_pair do |column_name, column_type|
80
+ column_options = Schema.column_options(options, column_name)
81
+ column("#{attachment_name}_#{column_name}", column_type, **column_options)
82
+ end
83
+ end
84
+ end
85
+
86
+ def has_attached_file(*attachment_names)
87
+ deprecation_warn "Method `t.has_attached_file` in the migration has been deprecated and will be replaced by `t.attachment`."
88
+ attachment(*attachment_names)
89
+ end
90
+ end
91
+
92
+ module CommandRecorder
93
+ def add_attachment(*args)
94
+ record(:add_attachment, args)
95
+ end
96
+
97
+ private
98
+
99
+ def invert_add_attachment(args)
100
+ [:remove_attachment, args]
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,99 @@
1
+ module Paperclip
2
+ module Storage
3
+ # The default place to store attachments is in the filesystem. Files on the local
4
+ # filesystem can be very easily served by Apache without requiring a hit to your app.
5
+ # They also can be processed more easily after they've been saved, as they're just
6
+ # normal files. There are two Filesystem-specific options for has_attached_file:
7
+ # * +path+: The location of the repository of attachments on disk. This can (and, in
8
+ # almost all cases, should) be coordinated with the value of the +url+ option to
9
+ # allow files to be saved into a place where Apache can serve them without
10
+ # hitting your app. Defaults to
11
+ # ":rails_root/public/:attachment/:id/:style/:basename.:extension"
12
+ # By default this places the files in the app's public directory which can be served
13
+ # directly. If you are using capistrano for deployment, a good idea would be to
14
+ # make a symlink to the capistrano-created system directory from inside your app's
15
+ # public directory.
16
+ # See Paperclip::Attachment#interpolate for more information on variable interpolaton.
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.
24
+ module Filesystem
25
+ def self.extended(base); end
26
+
27
+ def exists?(style_name = default_style)
28
+ if original_filename
29
+ File.exist?(path(style_name))
30
+ else
31
+ false
32
+ end
33
+ end
34
+
35
+ def flush_writes #:nodoc:
36
+ @queued_for_write.each do |style_name, file|
37
+ FileUtils.mkdir_p(File.dirname(path(style_name)))
38
+ begin
39
+ move_file(file.path, path(style_name))
40
+ rescue SystemCallError
41
+ File.open(path(style_name), "wb") do |new_file|
42
+ while chunk = file.read(16 * 1024)
43
+ new_file.write(chunk)
44
+ end
45
+ end
46
+ end
47
+ unless @options[:override_file_permissions] == false
48
+ resolved_chmod = (@options[:override_file_permissions] & ~0o111) || (0o666 & ~File.umask)
49
+ FileUtils.chmod(resolved_chmod, path(style_name))
50
+ end
51
+ file.rewind
52
+ end
53
+
54
+ after_flush_writes # allows attachment to clean up temp files
55
+
56
+ @queued_for_write = {}
57
+ end
58
+
59
+ def flush_deletes #:nodoc:
60
+ @queued_for_delete.uniq.each do |path|
61
+ begin
62
+ log("deleting #{path}")
63
+ FileUtils.rm(path) if File.exist?(path)
64
+ rescue Errno::ENOENT => e
65
+ # ignore file-not-found, let everything else pass
66
+ end
67
+ begin
68
+ loop do
69
+ path = File.dirname(path)
70
+ FileUtils.rmdir(path)
71
+ break if File.exist?(path) # Ruby 1.9.2 does not raise if the removal failed.
72
+ end
73
+ rescue Errno::EEXIST, Errno::ENOTEMPTY, Errno::ENOENT, Errno::EINVAL, Errno::ENOTDIR, Errno::EACCES
74
+ # Stop trying to remove parent directories
75
+ rescue SystemCallError => e
76
+ log("There was an unexpected error while deleting directories: #{e.class}")
77
+ # Ignore it
78
+ end
79
+ end
80
+ @queued_for_delete = []
81
+ end
82
+
83
+ def copy_to_local_file(style, local_dest_path)
84
+ FileUtils.cp(path(style), local_dest_path)
85
+ end
86
+
87
+ private
88
+
89
+ def move_file(src, dest)
90
+ # Support hardlinked files
91
+ if File.identical?(src, dest)
92
+ File.unlink(src)
93
+ else
94
+ FileUtils.mv(src, dest)
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,262 @@
1
+ module Paperclip
2
+ module Storage
3
+ # fog is a modern and versatile cloud computing library for Ruby.
4
+ # Among others, it supports Amazon S3 to store your files. In
5
+ # contrast to the outdated AWS-S3 gem it is actively maintained and
6
+ # supports multiple locations.
7
+ # Amazon's S3 file hosting service is a scalable, easy place to
8
+ # store files for distribution. You can find out more about it at
9
+ # http://aws.amazon.com/s3 There are a few fog-specific options for
10
+ # has_attached_file, which will be explained using S3 as an example:
11
+ # * +fog_credentials+: Takes a Hash with your credentials. For S3,
12
+ # you can use the following format:
13
+ # aws_access_key_id: '<your aws_access_key_id>'
14
+ # aws_secret_access_key: '<your aws_secret_access_key>'
15
+ # provider: 'AWS'
16
+ # region: 'eu-west-1'
17
+ # scheme: 'https'
18
+ # * +fog_directory+: This is the name of the S3 bucket that will
19
+ # store your files. Remember that the bucket must be unique across
20
+ # all of Amazon S3. If the bucket does not exist, Paperclip will
21
+ # attempt to create it.
22
+ # * +fog_file+: This can be hash or lambda returning hash. The
23
+ # value is used as base properties for new uploaded file.
24
+ # * +path+: This is the key under the bucket in which the file will
25
+ # be stored. The URL will be constructed from the bucket and the
26
+ # path. This is what you will want to interpolate. Keys should be
27
+ # unique, like filenames, and despite the fact that S3 (strictly
28
+ # speaking) does not support directories, you can still use a / to
29
+ # separate parts of your file name.
30
+ # * +fog_public+: (optional, defaults to true) Should the uploaded
31
+ # files be public or not? (true/false)
32
+ # * +fog_host+: (optional) The fully-qualified domain name (FQDN)
33
+ # that is the alias to the S3 domain of your bucket, e.g.
34
+ # 'http://images.example.com'. This can also be used in
35
+ # conjunction with Cloudfront (http://aws.amazon.com/cloudfront)
36
+ # * +fog_options+: (optional) A hash of options that are passed
37
+ # to fog when the file is created. For example, you could set
38
+ # the multipart-chunk size to 100MB with a hash:
39
+ # { :multipart_chunk_size => 104857600 }
40
+
41
+ module Fog
42
+ def self.extended(base)
43
+ unless defined?(Fog)
44
+ begin
45
+ require "fog"
46
+ rescue LoadError => e
47
+ e.message << " (You may need to install the fog gem)"
48
+ raise e
49
+ end
50
+ end
51
+
52
+ base.instance_eval do
53
+ unless @options[:url].to_s.match(/\A:fog.*url\z/)
54
+ @options[:path] = @options[:path].gsub(/:url/, @options[:url]).gsub(/\A:rails_root\/public\/system\//, "")
55
+ @options[:url] = ":fog_public_url"
56
+ end
57
+ unless Paperclip::Interpolations.respond_to? :fog_public_url
58
+ Paperclip.interpolates(:fog_public_url) do |attachment, style|
59
+ attachment.public_url(style)
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ AWS_BUCKET_SUBDOMAIN_RESTRICTON_REGEX = /\A(?:[a-z]|\d(?!\d{0,2}(?:\.\d{1,3}){3}\z))(?:[a-z0-9]|\.(?![\.\-])|\-(?![\.])){1,61}[a-z0-9]\z/.freeze
66
+
67
+ def exists?(style = default_style)
68
+ if original_filename
69
+ !!directory.files.head(path(style))
70
+ else
71
+ false
72
+ end
73
+ end
74
+
75
+ def fog_credentials
76
+ @fog_credentials ||= parse_credentials(@options[:fog_credentials])
77
+ end
78
+
79
+ def fog_file
80
+ @fog_file ||= begin
81
+ value = @options[:fog_file]
82
+ if !value
83
+ {}
84
+ elsif value.respond_to?(:call)
85
+ value.call(self)
86
+ else
87
+ value
88
+ end
89
+ end
90
+ end
91
+
92
+ def fog_public(style = default_style)
93
+ if @options.key?(:fog_public)
94
+ value = @options[:fog_public]
95
+ if value.respond_to?(:key?) && value.key?(style)
96
+ value[style]
97
+ elsif value.respond_to?(:call)
98
+ value.call(self)
99
+ else
100
+ value
101
+ end
102
+ else
103
+ true
104
+ end
105
+ end
106
+
107
+ def flush_writes
108
+ @queued_for_write.each do |style, file|
109
+ log("saving #{path(style)}")
110
+ retried = false
111
+ begin
112
+ attributes = fog_file.merge(
113
+ body: file,
114
+ key: path(style),
115
+ public: fog_public(style),
116
+ content_type: file.content_type
117
+ )
118
+ attributes.merge!(@options[:fog_options]) if @options[:fog_options]
119
+ directory.files.create(attributes)
120
+ rescue Excon::Errors::NotFound
121
+ raise if retried
122
+
123
+ retried = true
124
+ directory.save
125
+ file.rewind
126
+ retry
127
+ ensure
128
+ file.rewind
129
+ end
130
+ end
131
+
132
+ after_flush_writes # allows attachment to clean up temp files
133
+
134
+ @queued_for_write = {}
135
+ end
136
+
137
+ def flush_deletes
138
+ @queued_for_delete.uniq.each do |path|
139
+ log("deleting #{path}")
140
+ directory.files.new(key: path).destroy
141
+ end
142
+ @queued_for_delete = []
143
+ end
144
+
145
+ def public_url(style = default_style)
146
+ if @options[:fog_host]
147
+ "#{dynamic_fog_host_for_style(style)}/#{path(style)}"
148
+ else
149
+ if fog_credentials[:provider] == "AWS"
150
+ "#{scheme}://#{host_name_for_directory}/#{path(style)}"
151
+ else
152
+ directory.files.new(key: path(style)).public_url
153
+ end
154
+ end
155
+ end
156
+
157
+ def expiring_url(time = (Time.now + 3600), style_name = default_style)
158
+ time = convert_time(time)
159
+ http_url_method = "get_#{scheme}_url"
160
+ if path(style_name) && directory.files.respond_to?(http_url_method)
161
+ expiring_url = directory.files.public_send(http_url_method, path(style_name), time)
162
+
163
+ if @options[:fog_host]
164
+ expiring_url.gsub!(/#{host_name_for_directory}/, dynamic_fog_host_for_style(style_name))
165
+ end
166
+ else
167
+ expiring_url = url(style_name)
168
+ end
169
+
170
+ expiring_url
171
+ end
172
+
173
+ def parse_credentials(creds)
174
+ creds = find_credentials(creds).stringify_keys
175
+ (creds[RailsEnvironment.get] || creds).symbolize_keys
176
+ end
177
+
178
+ def copy_to_local_file(style, local_dest_path)
179
+ log("copying #{path(style)} to local file #{local_dest_path}")
180
+ ::File.open(local_dest_path, "wb") do |local_file|
181
+ file = directory.files.get(path(style))
182
+ return false unless file
183
+
184
+ local_file.write(file.body)
185
+ end
186
+ rescue ::Fog::Errors::Error => e
187
+ warn("#{e} - cannot copy #{path(style)} to local file #{local_dest_path}")
188
+ false
189
+ end
190
+
191
+ private
192
+
193
+ def convert_time(time)
194
+ time = Time.now + time if time.is_a?(Integer)
195
+ time
196
+ end
197
+
198
+ def dynamic_fog_host_for_style(style)
199
+ if @options[:fog_host].respond_to?(:call)
200
+ @options[:fog_host].call(self)
201
+ else
202
+ @options[:fog_host] =~ /%d/ ? @options[:fog_host] % (path(style).hash % 4) : @options[:fog_host]
203
+ end
204
+ end
205
+
206
+ def host_name_for_directory
207
+ if directory_name.to_s =~ Fog::AWS_BUCKET_SUBDOMAIN_RESTRICTON_REGEX
208
+ "#{directory_name}.s3.amazonaws.com"
209
+ else
210
+ "s3.amazonaws.com/#{directory_name}"
211
+ end
212
+ end
213
+
214
+ def find_credentials(creds)
215
+ case creds
216
+ when File
217
+ load_credentials_from_file(creds.path)
218
+ when Pathname
219
+ load_credentials_from_file(creds.to_s)
220
+ when String
221
+ load_credentials_from_file(creds)
222
+ when Hash
223
+ creds
224
+ else
225
+ if creds.respond_to?(:call)
226
+ creds.call(self)
227
+ else
228
+ raise ArgumentError, "Credentials are not a path, file, hash or proc."
229
+ end
230
+ end
231
+ end
232
+
233
+ def load_credentials_from_file(path)
234
+ if Gem::Version.new(Psych::VERSION) >= Gem::Version.new("3.1.0")
235
+ YAML::safe_load(ERB.new(File.read(path)).result, aliases: true)
236
+ else
237
+ YAML::safe_load(ERB.new(File.read(path)).result, [], [], true)
238
+ end
239
+ end
240
+
241
+ def connection
242
+ @connection ||= ::Fog::Storage.new(fog_credentials)
243
+ end
244
+
245
+ def directory
246
+ @directory ||= connection.directories.new(key: directory_name)
247
+ end
248
+
249
+ def directory_name
250
+ if @options[:fog_directory].respond_to?(:call)
251
+ @options[:fog_directory].call(self)
252
+ else
253
+ @options[:fog_directory]
254
+ end
255
+ end
256
+
257
+ def scheme
258
+ @scheme ||= fog_credentials[:scheme] || "https"
259
+ end
260
+ end
261
+ end
262
+ end