card-mod-carrierwave 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,59 @@
1
+ require "mini_magick"
2
+
3
+ module CarrierWave
4
+ # Adds image specific version handling to {FileCardUploader}.
5
+ # The ImageCardUploader creates five versions of different sizes when it
6
+ # uploads an imagae file:
7
+ # icon (16x16), small (75x75), medium (200X200), large (500x500) and
8
+ # the original size.
9
+ class ImageCardUploader < FileCardUploader
10
+ include CarrierWave::MiniMagick
11
+
12
+ def path version=nil
13
+ (version && version != :original) ? versions[version].path : super()
14
+ end
15
+
16
+ version :icon, if: :create_versions?, from_version: :small do
17
+ process resize_and_pad: [16, 16]
18
+ end
19
+ version :small, if: :create_versions?, from_version: :medium do
20
+ process resize_to_fit: [75, 75]
21
+ end
22
+ version :medium, if: :create_versions? do
23
+ process resize_to_limit: [200, 200]
24
+ end
25
+ version :large, if: :create_versions? do
26
+ process resize_to_limit: [500, 500]
27
+ end
28
+
29
+ # version :small_square, if: :create_versions?,
30
+ # from_version: :medium_square do
31
+ # process resize_to_fill: [75, 75]
32
+ # end
33
+ # version :medium_square, if: :create_versions? do
34
+ # process resize_to_fill: [200, 200]
35
+ # end
36
+ #
37
+ # In case we decide to support the squared versions
38
+ # we have to update all existing images with the following snippet:
39
+ # Card.search(type_id: Card::ImageID) do |card|
40
+ # card.image.cache_stored_file!
41
+ # card.image.recreate_versions!
42
+ # end
43
+
44
+ def identifier
45
+ full_filename(super())
46
+ end
47
+
48
+ # add 'original' if no version is given
49
+ def full_filename for_file
50
+ name = super(for_file)
51
+ if version_name
52
+ name
53
+ else
54
+ parts = name.split "."
55
+ "#{parts.shift}-original.#{parts.join('.')}"
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,112 @@
1
+ attr_writer :empty_ok
2
+
3
+ def self.included host_class
4
+ host_class.extend CarrierWave::CardMount
5
+ end
6
+
7
+ event :select_file_revision, after: :select_action do
8
+ attachment.retrieve_from_store!(attachment.identifier)
9
+ end
10
+
11
+ # we need a card id for the path so we have to update db_content when we have
12
+ # an id
13
+ event :correct_identifier, :finalize, on: :create, when: proc { |c| !c.web? } do
14
+ update_column(:db_content, attachment.db_content)
15
+ expire
16
+ end
17
+
18
+ event :save_original_filename, :prepare_to_store, on: :save, when: :file_ready_to_save? do
19
+ return unless @current_action
20
+ @current_action.update! comment: original_filename
21
+ end
22
+
23
+ event :validate_file_exist, :validate, on: :create do
24
+ return if empty_ok?
25
+ if will_be_stored_as == :web
26
+ errors.add "url is missing" if content.blank?
27
+ elsif !attachment.file.present?
28
+ errors.add attachment_name, "is missing"
29
+ end
30
+ end
31
+
32
+ event :write_identifier, after: :save_original_filename, when: proc { |c| !c.web? } do
33
+ self.content = attachment.db_content
34
+ end
35
+
36
+ def file_ready_to_save?
37
+ attachment.file.present? &&
38
+ !preliminary_upload? &&
39
+ !save_preliminary_upload? &&
40
+ attachment_is_changing?
41
+ end
42
+
43
+ def item_names _args={} # needed for flexmail attachments. hacky.
44
+ [name]
45
+ end
46
+
47
+ def original_filename
48
+ return content.split("/").last if web?
49
+ attachment.original_filename
50
+ end
51
+
52
+ def unfilled?
53
+ !attachment.present? && !save_preliminary_upload? && !subcards? && blank_content?
54
+ end
55
+
56
+ def attachment_changed?
57
+ send "#{attachment_name}_changed?"
58
+ end
59
+
60
+ def attachment_is_changing?
61
+ send "#{attachment_name}_is_changing?"
62
+ end
63
+
64
+ def attachment_before_act
65
+ send "#{attachment_name}_before_act"
66
+ end
67
+
68
+ def create_versions? _new_file
69
+ true
70
+ end
71
+
72
+ def empty_ok?
73
+ @empty_ok
74
+ end
75
+
76
+ def assign_set_specific_attributes
77
+ # reset content if we really have something to upload
78
+ self.content = nil if set_specific[attachment_name.to_s].present?
79
+ super
80
+ end
81
+
82
+ def delete_files_for_action action
83
+ with_selected_action_id(action.id) do
84
+ attachment.file.delete
85
+ attachment.versions.each_value do |version|
86
+ version.file.delete
87
+ end
88
+ end
89
+ end
90
+
91
+ def revision action, before_action=false
92
+ return unless (result = super)
93
+ result[:empty_ok] = true
94
+ result
95
+ end
96
+
97
+ def attachment_format ext
98
+ if ext.present? && attachment && (original_ext = attachment.extension.sub(/^\./, ""))
99
+ if ["file", original_ext].member? ext
100
+ original_ext
101
+ elsif (exts = Mime::Types[attachment.content_type])
102
+ if exts.find { |mt| mt.extensions.member? ext }
103
+ ext
104
+ else
105
+ exts[0].extensions[0]
106
+ end
107
+ end
108
+ end
109
+ rescue => e
110
+ Rails.logger.info "attachment_format issue: #{e.message}"
111
+ nil
112
+ end
@@ -0,0 +1,133 @@
1
+ event :change_bucket_if_read_only, :initialize,
2
+ on: :update, when: :change_bucket_if_read_only? do
3
+ @new_storage_type = storage_type_from_config
4
+ end
5
+
6
+ event :validate_storage_type_update, :validate, on: :update, when: :cloud? do
7
+ # FIXME: make it possible to retrieve the file from cloud storage
8
+ # to store it somewhere else. Currently, it only works to change the
9
+ # storage type if a new file is provided
10
+ # i.e. `update storage_type: :local` fails but
11
+ # `update storage_type: :local, file: [file handle]` is ok
12
+ return unless storage_type_changed? && !attachment_is_changing?
13
+
14
+ errors.add :storage_type, tr(:moving_files_is_not_supported)
15
+ end
16
+
17
+ def bucket
18
+ @bucket ||= cloud? && (new_card_bucket || bucket_from_content || bucket_from_config)
19
+ end
20
+
21
+ def new_card_bucket
22
+ return unless new_card?
23
+ # If the file is assigned before the bucket option we have to
24
+ # check if there is a bucket options in set_specific.
25
+ # That happens for exmaple when the file appears before the bucket in the
26
+ # options hash:
27
+ # Card.create file: file_handle, bucket: "my_bucket"
28
+ set_specific[:bucket] || set_specific["bucket"] || bucket_from_config
29
+ end
30
+
31
+ def bucket_config
32
+ @bucket_config ||= load_bucket_config
33
+ end
34
+
35
+ def load_bucket_config
36
+ return {} unless bucket
37
+ bucket_config = Cardio.config.file_buckets&.dig(bucket.to_sym) || {}
38
+ bucket_config.symbolize_keys!
39
+ bucket_config[:credentials]&.symbolize_keys!
40
+ # we don't want :attributes hash symbolized, so we can't use
41
+ # deep_symbolize_keys
42
+ ensure_bucket_config do
43
+ load_bucket_config_from_env bucket_config
44
+ end
45
+ end
46
+
47
+ def ensure_bucket_config
48
+ yield.tap do |config|
49
+ require_configuration! config
50
+ require_credentials! config
51
+ end
52
+ end
53
+
54
+ def require_configuration! config
55
+ cant_find_in_bucket! "configuration" unless config.present?
56
+ end
57
+
58
+ def require_credentials! config
59
+ cant_find_in_bucket! "credentials" unless config[:credentials]
60
+ end
61
+
62
+ def cant_find_in_bucket! need
63
+ raise Card::Error, "couldn't find #{need} for bucket #{bucket}"
64
+ end
65
+
66
+ def load_bucket_config_from_env config
67
+ config ||= {}
68
+ each_config_option_from_env do |key|
69
+ replace_with_env_variable config, key
70
+ end
71
+ credential_config config do |cred_hash|
72
+ load_bucket_credentials_from_env cred_hash
73
+ end
74
+ end
75
+
76
+ def credential_config config
77
+ config[:credentials] ||= {}
78
+ yield config[:credentials]
79
+ config.delete :credentials if config[:credentials].blank?
80
+ config
81
+ end
82
+
83
+ def each_config_option_from_env
84
+ CarrierWave::FileCardUploader::CONFIG_OPTIONS.each do |key|
85
+ yield key unless key.in? %i[attributes credentials]
86
+ end
87
+ end
88
+
89
+ def load_bucket_credentials_from_env cred_config
90
+ each_credential_from_env do |option|
91
+ replace_with_env_variable cred_config, option, "credentials"
92
+ end
93
+ end
94
+
95
+ def each_credential_from_env
96
+ regexp = credential_from_env_regexp
97
+ ENV.each_key do |env_key|
98
+ next unless (m = regexp.match env_key)
99
+ yield m[:option].downcase.to_sym
100
+ end
101
+ end
102
+
103
+ def credential_from_env_regexp
104
+ Regexp.new "^(?:#{bucket.to_s.upcase}_)?CREDENTIALS_(?<option>.+)$"
105
+ end
106
+
107
+ def replace_with_env_variable config, option, prefix=nil
108
+ env_key = [prefix, option].compact.join("_").upcase
109
+ new_value = ENV["#{bucket.to_s.upcase}_#{env_key}"] || ENV[env_key]
110
+ config[option] = new_value if new_value
111
+ end
112
+
113
+ def bucket_from_content
114
+ return unless content
115
+ content.match(/^\((?<bucket>[^)]+)\)/) { |m| m[:bucket] }
116
+ end
117
+
118
+ def bucket_from_config
119
+ cnf = Cardio.config
120
+ cnf.file_default_bucket || cnf.file_buckets&.keys&.first
121
+ end
122
+
123
+ def change_bucket_if_read_only?
124
+ cloud? && bucket_config[:read_only] && attachment_is_changing?
125
+ end
126
+
127
+ def bucket= value
128
+ if @action == :update
129
+ @new_bucket = value
130
+ else
131
+ @bucket = value
132
+ end
133
+ end
@@ -0,0 +1,22 @@
1
+ event :lose_coded_status_on_update, :initialize, on: :update, when: :coded? do
2
+ # unless explicit
3
+ return if @new_mod
4
+ @new_storage_type ||= storage_type_from_config
5
+ end
6
+
7
+ event :validate_coded_storage_type, :validate, on: :save, when: :will_become_coded? do
8
+ errors.add :storage_type, tr(:mod_argument_needed_to_save) unless mod || @new_mod
9
+ errors.add :storage_type, tr(:codename_needed_for_storage) if codename.blank?
10
+ end
11
+
12
+ def will_become_coded?
13
+ will_be_stored_as == :coded
14
+ end
15
+
16
+ def mod= value
17
+ if @action == :update && mod != value
18
+ @new_mod = value.to_s
19
+ else
20
+ @mod = value.to_s
21
+ end
22
+ end
@@ -0,0 +1,40 @@
1
+
2
+ event :update_public_link_on_create, :integrate, on: :create, when: :local? do
3
+ update_public_link
4
+ end
5
+
6
+ event :remove_public_link_on_delete, :integrate, on: :delete, when: :local? do
7
+ remove_public_links
8
+ end
9
+
10
+ event :update_public_link, after: :update_read_rule, when: :local? do
11
+ return if content.blank?
12
+ if who_can(:read).include? Card::AnyoneID
13
+ create_public_links
14
+ else
15
+ remove_public_links
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def create_public_links
22
+ path = attachment.public_path
23
+ return if File.exist? path
24
+ FileUtils.mkdir_p File.dirname(path)
25
+ File.symlink attachment.path, path unless File.symlink? path
26
+ create_versions_public_links
27
+ end
28
+
29
+ def create_versions_public_links
30
+ attachment.versions.each_value do |version|
31
+ next if File.symlink? version.public_path
32
+ File.symlink version.path, version.public_path
33
+ end
34
+ end
35
+
36
+ def remove_public_links
37
+ symlink_dir = File.dirname attachment.public_path
38
+ return unless Dir.exist? symlink_dir
39
+ FileUtils.rm_rf symlink_dir
40
+ end
@@ -0,0 +1,58 @@
1
+ MOD_FILE_DIR = "file".freeze
2
+
3
+ def store_dir
4
+ will_become_coded? ? coded_dir(@new_mod) : upload_dir
5
+ end
6
+
7
+ def retrieve_dir
8
+ coded? ? coded_dir : upload_dir
9
+ end
10
+
11
+ # place for files of regular file cards
12
+ def upload_dir
13
+ id ? "#{files_base_dir}/#{id}" : tmp_upload_dir
14
+ end
15
+
16
+ # place for files of mod file cards
17
+ def coded_dir new_mod=nil
18
+ dir = File.join mod_dir(new_mod), MOD_FILE_DIR, codename.to_s
19
+ FileUtils.mkdir_p(dir) unless File.directory?(dir)
20
+ dir
21
+ end
22
+
23
+ def mod_dir new_mod=nil
24
+ mod_name = new_mod || mod
25
+ dir = Cardio::Mod.dirs.path(mod_name) || (mod_name.to_sym == :test && "test")
26
+
27
+ raise Error, "can't find mod \"#{mod_name}\"" unless dir
28
+ dir
29
+ end
30
+
31
+ def files_base_dir
32
+ dir = bucket ? bucket_config[:subdirectory] : Card.paths["files"].existent.first
33
+ dir || files_base_dir_configuration_error
34
+ end
35
+
36
+ def files_base_dir_configuration_error
37
+ raise StandardError,
38
+ "missing directory for file cache (default is `files` in deck root)"
39
+ end
40
+
41
+ # used in the indentifier
42
+ def file_dir
43
+ if coded?
44
+ ":#{codename}"
45
+ elsif cloud?
46
+ "(#{bucket})/#{file_id}"
47
+ else
48
+ "~#{file_id}"
49
+ end
50
+ end
51
+
52
+ def public?
53
+ who_can(:read).include? Card::AnyoneID
54
+ end
55
+
56
+ def file_id
57
+ id? ? id : upload_cache_card.id
58
+ end
@@ -0,0 +1,170 @@
1
+ attr_writer :bucket, :storage_type
2
+
3
+ event :storage_type_change, :store, on: :update, when: :storage_type_changed? do
4
+ # carrierwave stores file if @cache_id is not nil
5
+ attachment.cache_stored_file!
6
+ # attachment.retrieve_from_cache!(attachment.cache_name)
7
+ update_storage_attributes
8
+ # next line might be necessary to move files to cloud
9
+
10
+ # make sure that we get the new identifier
11
+ # otherwise action_id will return wrong id for new identifier
12
+ db_content_will_change!
13
+ write_identifier
14
+ end
15
+
16
+ event :validate_storage_type, :validate, on: :save do
17
+ unless known_storage_type? will_be_stored_as
18
+ errors.add :storage_type, tr(
19
+ :unknown_storage_type,
20
+ new_storage_type: @new_storage_type
21
+ )
22
+ end
23
+ end
24
+
25
+ def will_be_stored_as
26
+ @new_storage_type || storage_type
27
+ end
28
+
29
+ def read_only?
30
+ web? || (cloud? && bucket_config[:read_only])
31
+ end
32
+
33
+ def cloud?
34
+ storage_type == :cloud
35
+ end
36
+
37
+ def web?
38
+ storage_type == :web
39
+ end
40
+
41
+ def local?
42
+ storage_type == :local
43
+ end
44
+
45
+ def coded?
46
+ storage_type == :coded
47
+ end
48
+
49
+ def remote_storage?
50
+ cloud? || web?
51
+ end
52
+
53
+ def storage_type
54
+ @storage_type ||=
55
+ new_card? ? storage_type_from_config : storage_type_from_content
56
+ end
57
+
58
+ def deprecated_mod_file?
59
+ content && (lines = content.split("\n")) && lines.size == 4
60
+ end
61
+
62
+ def mod
63
+ @mod ||= coded? && mod_from_content
64
+ end
65
+
66
+ def mod_from_content
67
+ if content =~ %r{^:[^/]+/([^.]+)}
68
+ Regexp.last_match(1) # current mod_file format
69
+ else
70
+ mod_from_deprecated_content
71
+ end
72
+ end
73
+
74
+ # old format is still used in card_changes
75
+ def mod_from_deprecated_content
76
+ return if content =~ /^\~/
77
+ return unless (lines = content.split("\n")) && lines.size == 4
78
+ lines.last
79
+ end
80
+
81
+ def storage_type_from_config
82
+ valid_storage_type ENV["FILE_STORAGE"] || Cardio.config.file_storage
83
+ end
84
+
85
+ def valid_storage_type storage_type
86
+ storage_type.to_sym.tap do |type|
87
+ invalid_storage_type! type unless type.in? valid_storage_type_list
88
+ end
89
+ end
90
+
91
+ def valid_storage_type_list
92
+ CarrierWave::FileCardUploader::STORAGE_TYPES
93
+ end
94
+
95
+ def invalid_storage_type! type
96
+ raise Card::Error, tr(:error_invalid_storage_type, type: type)
97
+ end
98
+
99
+ def storage_type_from_content
100
+ case content
101
+ when /^\(/ then :cloud
102
+ when %r{/^https?\:/} then :web
103
+ when /^~/ then :local
104
+ when /^\:/ then :coded
105
+ else
106
+ if deprecated_mod_file?
107
+ :coded
108
+ else
109
+ storage_type_from_config
110
+ end
111
+ end
112
+ end
113
+
114
+ def update_storage_attributes
115
+ @mod = @new_mod if @new_mod
116
+ @bucket = @new_bucket if @new_bucket
117
+ @storage_type = @new_storage_type
118
+ end
119
+
120
+ def storage_type_changed?
121
+ @new_bucket || (@new_storage_type && @new_storage_type != storage_type) || @new_mod
122
+ end
123
+
124
+ def storage_type= value
125
+ known_storage_type? value
126
+ if @action == :update #&& storage_type != value
127
+ # we cant update the storage type directly here
128
+ # if we do then the uploader doesn't find the file we want to update
129
+ @new_storage_type = value
130
+ else
131
+ @storage_type = value
132
+ end
133
+ end
134
+
135
+ def with_storage_options opts={}
136
+ old_values = {}
137
+ validate_temporary_storage_type_change opts[:storage_type]
138
+ %i[storage_type mod bucket].each do |opt_name|
139
+ next unless opts[opt_name]
140
+ old_values[opt_name] = instance_variable_get "@#{opt_name}"
141
+ instance_variable_set "@#{opt_name}", opts[opt_name]
142
+ @temp_storage_type = true
143
+ end
144
+ yield
145
+ ensure
146
+ @temp_storage_type = false
147
+ old_values.each do |key, val|
148
+ instance_variable_set "@#{key}", val
149
+ end
150
+ end
151
+
152
+ def temporary_storage_type_change?
153
+ @temp_storage_type
154
+ end
155
+
156
+ def validate_temporary_storage_type_change new_storage_type=nil
157
+ new_storage_type ||= @new_storage_type
158
+ return unless new_storage_type
159
+ unless known_storage_type? new_storage_type
160
+ raise Error, tr(:unknown_storage_type, new_storage_type: new_storage_type)
161
+ end
162
+
163
+ if new_storage_type == :coded && codename.blank?
164
+ raise Error, "codename needed for storage type :coded"
165
+ end
166
+ end
167
+
168
+ def known_storage_type? type=storage_type
169
+ type.in? CarrierWave::FileCardUploader::STORAGE_TYPES
170
+ end