upload_d_n_d 0.9.1

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.

Potentially problematic release.


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

Files changed (71) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +229 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/config/upload_d_n_d_manifest.js +3 -0
  6. data/app/assets/stylesheets/upload_d_n_d/application.css +604 -0
  7. data/app/components/upload_d_n_d/upload_component.html.erb +53 -0
  8. data/app/components/upload_d_n_d/upload_component.rb +204 -0
  9. data/app/controllers/concerns/upload_d_n_d/rate_limiting.rb +89 -0
  10. data/app/controllers/upload_d_n_d/attachments_controller.rb +218 -0
  11. data/app/controllers/upload_d_n_d/base_attachments_controller.rb +250 -0
  12. data/app/controllers/upload_d_n_d/temporary_attachments_controller.rb +200 -0
  13. data/app/helpers/upload_d_n_d/application_helper.rb +51 -0
  14. data/app/helpers/upload_d_n_d/controllers/helpers.rb +21 -0
  15. data/app/javascript/upload_d_n_d/controllers/preview_controller.js +217 -0
  16. data/app/javascript/upload_d_n_d/controllers/uploads_controller.js +396 -0
  17. data/app/javascript/upload_d_n_d/index.js +13 -0
  18. data/app/jobs/upload_d_n_d/application_job.rb +4 -0
  19. data/app/jobs/upload_d_n_d/cleanup_temporary_attachments_job.rb +17 -0
  20. data/app/jobs/upload_d_n_d/purge_orphan_blobs_job.rb +25 -0
  21. data/app/jobs/upload_d_n_d/purge_rejected_blob_job.rb +24 -0
  22. data/app/models/upload_d_n_d/application_record.rb +5 -0
  23. data/app/models/upload_d_n_d/temporary_attachment.rb +129 -0
  24. data/app/views/upload_d_n_d/shared/_file_icon.html.erb +9 -0
  25. data/app/views/upload_d_n_d/shared/_files_list.html.erb +63 -0
  26. data/app/views/upload_d_n_d/shared/_temp_files_list.html.erb +53 -0
  27. data/config/importmap.rb +5 -0
  28. data/config/initializers/inflections.rb +11 -0
  29. data/config/locales/upload_d_n_d.en.yml +80 -0
  30. data/config/locales/upload_d_n_d.fr.yml +80 -0
  31. data/config/routes.rb +12 -0
  32. data/docs/CHANGELOG.md +282 -0
  33. data/lib/generators/upload_d_n_d/POLICY_README +49 -0
  34. data/lib/generators/upload_d_n_d/USAGE +80 -0
  35. data/lib/generators/upload_d_n_d/install_generator.rb +174 -0
  36. data/lib/generators/upload_d_n_d/mime_type_helper.rb +106 -0
  37. data/lib/generators/upload_d_n_d/model_generator.rb +313 -0
  38. data/lib/generators/upload_d_n_d/policy_generator.rb +25 -0
  39. data/lib/generators/upload_d_n_d/templates/POST_INSTALL +62 -0
  40. data/lib/generators/upload_d_n_d/templates/concerns/controllers_uploadable.rb +115 -0
  41. data/lib/generators/upload_d_n_d/templates/concerns/upload_authorization.rb +91 -0
  42. data/lib/generators/upload_d_n_d/templates/concerns/uploadable.rb +101 -0
  43. data/lib/generators/upload_d_n_d/templates/controller.rb.tt +60 -0
  44. data/lib/generators/upload_d_n_d/templates/helpers/upload_helper.rb +105 -0
  45. data/lib/generators/upload_d_n_d/templates/initializers/upload_d_n_d.rb +69 -0
  46. data/lib/generators/upload_d_n_d/templates/migrations/create_upload_d_n_d_temporary_attachments.rb +18 -0
  47. data/lib/generators/upload_d_n_d/templates/policies/upload_d_n_d_attachment_policy.rb +37 -0
  48. data/lib/generators/upload_d_n_d/templates/policies/upload_d_n_d_temporary_attachment_policy.rb +56 -0
  49. data/lib/generators/upload_d_n_d/templates/validators/file_validator.rb +171 -0
  50. data/lib/tasks/upload_d_n_d_storage.rake +27 -0
  51. data/lib/tasks/upload_d_n_d_tasks.rake +20 -0
  52. data/lib/upload_d_n_d/attachment_lookup.rb +24 -0
  53. data/lib/upload_d_n_d/authorization/can_can_can_adapter.rb +29 -0
  54. data/lib/upload_d_n_d/authorization/no_authorization.rb +13 -0
  55. data/lib/upload_d_n_d/authorization/pundit_adapter.rb +41 -0
  56. data/lib/upload_d_n_d/authorization/unconfigured.rb +15 -0
  57. data/lib/upload_d_n_d/authorization.rb +66 -0
  58. data/lib/upload_d_n_d/configuration.rb +97 -0
  59. data/lib/upload_d_n_d/controllers/uploadable.rb +271 -0
  60. data/lib/upload_d_n_d/engine.rb +50 -0
  61. data/lib/upload_d_n_d/field_constraints.rb +92 -0
  62. data/lib/upload_d_n_d/field_reference.rb +44 -0
  63. data/lib/upload_d_n_d/file_validator.rb +156 -0
  64. data/lib/upload_d_n_d/mime_types.rb +75 -0
  65. data/lib/upload_d_n_d/orphan_blobs.rb +151 -0
  66. data/lib/upload_d_n_d/rejected_blob.rb +42 -0
  67. data/lib/upload_d_n_d/session_id.rb +25 -0
  68. data/lib/upload_d_n_d/uploadable.rb +117 -0
  69. data/lib/upload_d_n_d/version.rb +3 -0
  70. data/lib/upload_d_n_d.rb +62 -0
  71. metadata +150 -0
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UploadDND
4
+ module Generators
5
+ module MimeTypeHelper
6
+ # Mapping des extensions vers MIME types
7
+ EXTENSION_TO_MIME = {
8
+ # Images
9
+ "jpg" => "image/jpeg",
10
+ "jpeg" => "image/jpeg",
11
+ "png" => "image/png",
12
+ "gif" => "image/gif",
13
+ "webp" => "image/webp",
14
+ "svg" => "image/svg+xml",
15
+ "bmp" => "image/bmp",
16
+ "ico" => "image/x-icon",
17
+ "tiff" => "image/tiff",
18
+ "tif" => "image/tiff",
19
+
20
+ # Documents
21
+ "pdf" => "application/pdf",
22
+ "doc" => "application/msword",
23
+ "docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
24
+ "xls" => "application/vnd.ms-excel",
25
+ "xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
26
+ "ppt" => "application/vnd.ms-powerpoint",
27
+ "pptx" => "application/vnd.openxmlformats-officedocument.presentationml.presentation",
28
+ "odt" => "application/vnd.oasis.opendocument.text",
29
+ "ods" => "application/vnd.oasis.opendocument.spreadsheet",
30
+ "odp" => "application/vnd.oasis.opendocument.presentation",
31
+
32
+ # Texte
33
+ "txt" => "text/plain",
34
+ "csv" => "text/csv",
35
+ "html" => "text/html",
36
+ "htm" => "text/html",
37
+ "xml" => "text/xml",
38
+ "json" => "application/json",
39
+ "md" => "text/markdown",
40
+
41
+ # Archives
42
+ "zip" => "application/zip",
43
+ "rar" => "application/x-rar-compressed",
44
+ "7z" => "application/x-7z-compressed",
45
+ "tar" => "application/x-tar",
46
+ "gz" => "application/gzip",
47
+
48
+ # Audio
49
+ "mp3" => "audio/mpeg",
50
+ "wav" => "audio/wav",
51
+ "ogg" => "audio/ogg",
52
+ "flac" => "audio/flac",
53
+ "m4a" => "audio/mp4",
54
+
55
+ # Vidéo
56
+ "mp4" => "video/mp4",
57
+ "avi" => "video/x-msvideo",
58
+ "mov" => "video/quicktime",
59
+ "wmv" => "video/x-ms-wmv",
60
+ "flv" => "video/x-flv",
61
+ "webm" => "video/webm",
62
+ "mkv" => "video/x-matroska"
63
+ }.freeze
64
+
65
+ # Convertit une liste d'extensions ou de MIME types en MIME types
66
+ # @param types [Array<String>] Liste d'extensions (ex: ['jpg', 'png']) ou MIME types
67
+ # @return [Array<String>] Liste de MIME types
68
+ def convert_to_mime_types(types)
69
+ types.map do |type|
70
+ type = type.to_s.strip.downcase
71
+
72
+ # Si ça commence par un point, on l'enlève
73
+ type = type.delete_prefix(".")
74
+
75
+ # Si c'est déjà un MIME type (contient un /), on le garde tel quel
76
+ if type.include?("/")
77
+ type
78
+ # Sinon, on cherche dans notre mapping
79
+ elsif EXTENSION_TO_MIME.key?(type)
80
+ EXTENSION_TO_MIME[type]
81
+ else
82
+ # Si on ne trouve pas, on affiche un warning et on garde tel quel
83
+ say " ⚠️ Extension inconnue '#{type}', utilisée telle quelle", :yellow
84
+ type
85
+ end
86
+ end.uniq
87
+ end
88
+
89
+ # Retourne une liste lisible des extensions supportées
90
+ def supported_extensions
91
+ EXTENSION_TO_MIME.keys.sort
92
+ end
93
+
94
+ # Retourne des exemples d'extensions par catégorie
95
+ def extension_examples
96
+ {
97
+ "Images" => %w[jpg png gif webp svg],
98
+ "Documents" => %w[pdf doc docx xls xlsx ppt pptx],
99
+ "Archives" => %w[zip rar 7z tar gz],
100
+ "Audio" => %w[mp3 wav ogg flac],
101
+ "Vidéo" => %w[mp4 avi mov webm]
102
+ }
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,313 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+ require_relative "mime_type_helper"
6
+
7
+ module UploadDND
8
+ module Generators
9
+ class ModelGenerator < Rails::Generators::NamedBase
10
+ include MimeTypeHelper
11
+
12
+ source_root File.expand_path("templates", __dir__)
13
+
14
+ desc "Configure un modèle pour utiliser UploadDND avec drag & drop"
15
+
16
+ class_option :attribute,
17
+ type: :string,
18
+ aliases: "-a",
19
+ desc: "Nom de l'attribut uploadable (ex: avatar, documents)",
20
+ required: true
21
+
22
+ class_option :multiple,
23
+ type: :boolean,
24
+ default: false,
25
+ desc: "Upload multiple fichiers (has_many_attached)"
26
+
27
+ class_option :content_type,
28
+ type: :array,
29
+ default: [ "jpg", "png" ],
30
+ desc: "Extensions de fichiers acceptées (ex: jpg png pdf) ou MIME types"
31
+
32
+ class_option :max_size,
33
+ type: :string,
34
+ default: "5",
35
+ desc: "Taille max en MB"
36
+
37
+ class_option :max_files,
38
+ type: :string,
39
+ default: nil,
40
+ desc: "Nombre max de fichiers (pour multiple)"
41
+
42
+ class_option :controller,
43
+ type: :boolean,
44
+ default: true,
45
+ desc: "Générer/mettre à jour le controller"
46
+
47
+ class_option :views,
48
+ type: :boolean,
49
+ default: true,
50
+ desc: "Générer/mettre à jour les vues"
51
+
52
+ def validate_options
53
+ unless options[:attribute].present?
54
+ say "❌ L'option --attribute est requise", :red
55
+ say "Exemple: rails g upload_d_n_d:model User --attribute=avatar", :yellow
56
+ exit(1)
57
+ end
58
+ end
59
+
60
+ def check_model_exists
61
+ unless model_exists?
62
+ say "❌ Le modèle #{model_path} n'existe pas", :red
63
+ say "Créez d'abord le modèle avec: rails g model #{class_name}", :yellow
64
+ exit(1)
65
+ end
66
+ end
67
+
68
+ def add_uploadable_to_model
69
+ say "📝 Configuration du modèle (instructions)", :green
70
+
71
+ model_content = File.read(model_path)
72
+
73
+ already_has_concern = model_content.include?("include UploadDND::Uploadable")
74
+ already_has_uploadable = model_content.match?(/uploadable\s+:#{attribute_name}/)
75
+
76
+ if already_has_concern && already_has_uploadable
77
+ say " ✓ Le modèle semble déjà configuré pour :#{attribute_name}", :yellow
78
+ return
79
+ end
80
+
81
+ say "Ajoutez (ou vérifiez) ces lignes dans #{model_path} :", :yellow
82
+ say ""
83
+
84
+ unless already_has_concern
85
+ say " include UploadDND::Uploadable", :cyan
86
+ end
87
+ unless already_has_uploadable
88
+ uploadable_attribute_code.each_line { |l| say l.rstrip, :cyan }
89
+ end
90
+
91
+ say ""
92
+ say " Note: uploadable déclare automatiquement has_one_attached / has_many_attached.", :yellow
93
+ say " Inutile de le déclarer séparément.", :yellow
94
+ say ""
95
+ end
96
+
97
+ def add_active_storage_association
98
+ # Non-invasif: inclus dans add_uploadable_to_model (instructions), on garde la méthode
99
+ # pour compatibilité de flow / messages.
100
+ end
101
+
102
+ def update_controller
103
+ return unless options[:controller]
104
+
105
+ if controller_exists?
106
+ say "🎮 Controller existant détecté (instructions)", :green
107
+ show_controller_instructions
108
+ else
109
+ say "🎮 Génération du controller...", :green
110
+ generate_controller
111
+ end
112
+ end
113
+
114
+ def add_routes_for_uploads
115
+ say "🛣️ Routes (instructions)", :green
116
+
117
+ say "Ajoutez ces routes dans config/routes.rb (dans le bon scope/namespace) :", :yellow
118
+ say ""
119
+
120
+ if namespaced?
121
+ say " # Exemple si votre ressource est dans un namespace (adaptez au vôtre)", :cyan
122
+ say " namespace :#{namespace_path.underscore} do", :cyan
123
+ say " resources :#{plural_name} do", :cyan
124
+ say " member do", :cyan
125
+ say " post :attach_file", :cyan
126
+ say " delete :remove_file", :cyan
127
+ say " end", :cyan
128
+ say " end", :cyan
129
+ say " end", :cyan
130
+ else
131
+ say " resources :#{plural_name} do", :cyan
132
+ say " member do", :cyan
133
+ say " post :attach_file", :cyan
134
+ say " delete :remove_file", :cyan
135
+ say " end", :cyan
136
+ say " end", :cyan
137
+ end
138
+
139
+ say ""
140
+ end
141
+
142
+ def update_views
143
+ return unless options[:views]
144
+
145
+ say "👁️ Instructions pour les vues", :green
146
+ show_view_instructions
147
+ end
148
+
149
+ def add_uploadable_concern_to_controller
150
+ # Non-invasif: on n'injecte plus dans les controllers existants.
151
+ end
152
+
153
+ def add_actions_to_before_action
154
+ # Non-invasif: instructions via show_controller_instructions
155
+ end
156
+
157
+ def show_summary
158
+ extensions = normalized_content_types
159
+ mime_types = convert_to_mime_types(extensions)
160
+
161
+ say "\n✅ Configuration terminée!", :green
162
+ say "\n📋 Résumé:", :cyan
163
+ say " Modèle: #{class_name}"
164
+ say " Namespace: #{namespace_path}" if namespaced?
165
+ say " Attribut: #{attribute_name}"
166
+ say " Type: #{options[:multiple] ? 'multiple' : 'simple'}"
167
+ say " Extensions: #{extensions.join(', ')}"
168
+ say " MIME types: #{mime_types.join(', ')}"
169
+ say " Taille max: #{options[:max_size]}MB"
170
+ say " Fichiers max: #{options[:max_files]}" if options[:max_files]
171
+
172
+ say "\n📝 Prochaines étapes:", :yellow
173
+ say " 1. Copiez/collez les snippets affichés (modèle/routes/controller si applicable)"
174
+ say " 2. Lancez la migration si nécessaire: rails db:migrate"
175
+ say " 3. Vérifiez #{model_path}"
176
+ say " 4. Vérifiez #{controller_path}" if options[:controller]
177
+ say " 5. Vérifiez les vues dans #{views_path}" if options[:views]
178
+ say "\n💡 Utilisez dans vos vues:", :cyan
179
+ say " <%= upload_field :#{attribute_name}, record: @#{singular_name} %>"
180
+ end
181
+
182
+ private
183
+
184
+ def model_exists?
185
+ File.exist?(model_path)
186
+ end
187
+
188
+ def controller_exists?
189
+ File.exist?(controller_path)
190
+ end
191
+
192
+ def model_path
193
+ @model_path ||= File.join("app/models", "#{file_path}.rb")
194
+ end
195
+
196
+ def controller_path
197
+ @controller_path ||= File.join("app/controllers", namespace_path, "#{plural_name}_controller.rb")
198
+ end
199
+
200
+ def views_path
201
+ @views_path ||= File.join("app/views", namespace_path, plural_name)
202
+ end
203
+
204
+ def namespace_path
205
+ @namespace_path ||= class_path.join("/")
206
+ end
207
+
208
+
209
+ def model_class_name
210
+ class_name.split("::").last
211
+ end
212
+
213
+ def attribute_name
214
+ options[:attribute]
215
+ end
216
+
217
+ def singular_name
218
+ file_name.split("/").last
219
+ end
220
+
221
+ def uploadable_attribute_code
222
+ max_files_option = options[:max_files] ? ",\n max_files: #{options[:max_files]}" : ""
223
+ multiple_option = options[:multiple] ? ",\n multiple: true" : ""
224
+
225
+ # Utilise accept: avec symboles (nouvelle API v0.4.0)
226
+ extensions = normalized_content_types.map { |ext| ":#{ext.delete_prefix('.')}" }
227
+
228
+ <<~RUBY.indent(2)
229
+
230
+ uploadable :#{attribute_name},
231
+ accept: [#{extensions.join(', ')}],
232
+ max_size: #{options[:max_size]}.megabytes#{multiple_option}#{max_files_option},
233
+ strict_mime_check: true
234
+ RUBY
235
+ end
236
+
237
+ def update_existing_controller
238
+ # Non-invasif: on ne modifie plus le controller existant.
239
+ end
240
+
241
+ def generate_controller
242
+ # Génère un controller basique si il n'existe pas
243
+ template "controller.rb.tt", controller_path
244
+ end
245
+
246
+ def show_view_instructions
247
+ say ""
248
+ say "Dans votre formulaire new.html.erb (création avec uploads temporaires) :", :yellow
249
+ say ""
250
+ say " <%= form_with model: @#{singular_name} do |form| %>", :cyan
251
+ say " <%# Champ caché pour lier les uploads temporaires %>", :cyan
252
+ say " <%= upload_session_field %>", :cyan
253
+ say ""
254
+ say " <%# Vos autres champs... %>", :cyan
255
+ say ""
256
+ say " <%# Ajoutez le champ d'upload %>", :cyan
257
+ say " <%= upload_field :#{attribute_name}, record: @#{singular_name} %>", :cyan
258
+ say ""
259
+ say " <%= form.submit %>", :cyan
260
+ say " <% end %>", :cyan
261
+ say ""
262
+ say "Dans votre formulaire edit.html.erb (édition, pas besoin de upload_session_field) :", :yellow
263
+ say ""
264
+ say " <%= form_with model: @#{singular_name} do |form| %>", :cyan
265
+ say " <%# Vos autres champs... %>", :cyan
266
+ say " <%= upload_field :#{attribute_name}, record: @#{singular_name} %>", :cyan
267
+ say " <%= form.submit %>", :cyan
268
+ say " <% end %>", :cyan
269
+ say ""
270
+ say "Dans votre vue show.html.erb (pour afficher les fichiers) :", :yellow
271
+ say ""
272
+ say " <%# Si vous voulez afficher la liste des fichiers : %>", :cyan
273
+ say " <%= render 'upload_d_n_d/shared/files_list',", :cyan
274
+ say " record: @#{singular_name},", :cyan
275
+ say " attribute_name: :#{attribute_name} %>", :cyan
276
+ say ""
277
+ end
278
+
279
+ def show_controller_instructions
280
+ say "Ajoutez (ou vérifiez) ces éléments dans #{controller_path} :", :yellow
281
+ say ""
282
+ say " # 1. Incluez le concern", :cyan
283
+ say " include UploadDND::Controllers::Uploadable", :cyan
284
+ say ""
285
+ say " # 2. Ajoutez :attach_file et :remove_file dans le before_action", :cyan
286
+ say " before_action :set_#{singular_name}, only: [:show, :edit, :update, :destroy, :attach_file, :remove_file]", :cyan
287
+ say ""
288
+ say " # 3. Les actions attach_file et remove_file sont fournies par le concern", :cyan
289
+ say " # Elles utilisent @#{singular_name} automatiquement", :cyan
290
+ say ""
291
+ say " # 4. Pour les uploads temporaires (formulaire new/create) :", :cyan
292
+ say " def create", :cyan
293
+ say " @#{singular_name} = #{class_name}.new(#{singular_name}_params)", :cyan
294
+ say " wire_temporary_uploads(@#{singular_name})", :cyan
295
+ say " # ...", :cyan
296
+ say " end", :cyan
297
+ say ""
298
+ end
299
+
300
+ # Accepte:
301
+ # - --content_type=pdf docx pptx
302
+ # - --content_type=pdf,docx,pptx
303
+ # - mixte (espaces + virgules)
304
+ def normalized_content_types
305
+ Array(options[:content_type])
306
+ .flat_map { |v| v.to_s.split(/[,\s]+/) }
307
+ .map(&:strip)
308
+ .reject(&:empty?)
309
+ .uniq
310
+ end
311
+ end
312
+ end
313
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UploadDND
4
+ module Generators
5
+ class PolicyGenerator < Rails::Generators::Base
6
+ source_root File.expand_path("templates", __dir__)
7
+
8
+ desc "Generate Pundit policies for UploadDND (temporary + permanent attachments)"
9
+
10
+ def create_temporary_attachment_policy
11
+ template "policies/upload_d_n_d_temporary_attachment_policy.rb",
12
+ "app/policies/upload_d_n_d_temporary_attachment_policy.rb"
13
+ end
14
+
15
+ def create_attachment_policy
16
+ template "policies/upload_d_n_d_attachment_policy.rb",
17
+ "app/policies/upload_d_n_d_attachment_policy.rb"
18
+ end
19
+
20
+ def show_readme
21
+ readme "POLICY_README" if behavior == :invoke
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,62 @@
1
+ ===============================================================================
2
+
3
+ UploadDND v0.5.0 installé avec succès !
4
+
5
+ ===============================================================================
6
+
7
+ Prochaines étapes :
8
+
9
+ 1. Exécuter les migrations :
10
+
11
+ rails db:migrate
12
+
13
+ 2. (Si pas déjà fait) Installer ActiveStorage :
14
+
15
+ rails active_storage:install
16
+ rails db:migrate
17
+
18
+ 3. Ajouter l'import JavaScript dans votre application.js :
19
+
20
+ import "upload_d_n_d"
21
+
22
+ Les contrôleurs Stimulus sont auto-enregistrés depuis la gem.
23
+
24
+ 4. Ajouter le CSS dans votre layout :
25
+
26
+ <%= stylesheet_link_tag "upload_d_n_d/application", media: "all" %>
27
+
28
+ 5. (Optionnel) Configurer les policies Pundit :
29
+
30
+ rails generate upload_d_n_d:policy
31
+
32
+ Ceci génère les deux policies :
33
+ - app/policies/upload_d_n_d_temporary_attachment_policy.rb
34
+ - app/policies/upload_d_n_d_attachment_policy.rb
35
+
36
+ 6. Utiliser le générateur de modèle (recommandé) :
37
+
38
+ rails g upload_d_n_d:model User --attribute=avatar
39
+ rails g upload_d_n_d:model Project --attribute=documents --multiple --content_type=pdf docx xlsx
40
+
41
+ 7. Utiliser dans vos vues :
42
+
43
+ <%= upload_field :avatar, record: @user %>
44
+
45
+ 8. Architecture v0.5.0 :
46
+
47
+ Tout est servi directement depuis la gem :
48
+ - UploadDND::Uploadable (model concern)
49
+ - UploadDND::Controllers::Uploadable (controller concern)
50
+ - UploadDND::FileValidator (file validation)
51
+ - Contrôleurs Stimulus (auto-enregistrés via importmap)
52
+ - CSS (servi via asset pipeline)
53
+ - ViewComponent (servi depuis la gem)
54
+
55
+ Un simple `bundle update` suffit pour tout mettre à jour.
56
+
57
+ Fichiers personnalisables (copiés dans votre app) :
58
+ - app/controllers/concerns/upload_d_n_d/upload_authorization.rb
59
+ - app/helpers/upload_d_n_d/upload_helper.rb
60
+ - config/initializers/upload_d_n_d.rb
61
+
62
+ ===============================================================================
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UploadDND
4
+ module Controllers
5
+ module Uploadable
6
+ extend ActiveSupport::Concern
7
+
8
+ # Action pour attacher un fichier à un record existant
9
+ # POST /:resource/:id/attach_file
10
+ def attach_file
11
+ record = instance_variable_get("@#{controller_name.singularize}")
12
+ attribute_name = params[:attribute_name]
13
+ blob_id = params[:blob_id]
14
+
15
+ unless record
16
+ render json: { error: "Enregistrement non trouvé" }, status: :not_found
17
+ return
18
+ end
19
+
20
+ unless attribute_name.present? && blob_id.present?
21
+ render json: { error: "Paramètres manquants" }, status: :bad_request
22
+ return
23
+ end
24
+
25
+ # Récupère le blob ActiveStorage
26
+ begin
27
+ blob = ActiveStorage::Blob.find(blob_id)
28
+ rescue ActiveRecord::RecordNotFound
29
+ render json: { error: "Fichier non trouvé" }, status: :not_found
30
+ return
31
+ end
32
+
33
+ # Attache le blob au record
34
+ attachment = record.send(attribute_name)
35
+ if attachment.respond_to?(:attach)
36
+ attachment.attach(blob)
37
+ else
38
+ record.send("#{attribute_name}=", blob)
39
+ end
40
+
41
+ if record.save
42
+ render_file_list(record, attribute_name)
43
+ else
44
+ render json: { error: record.errors.full_messages.join(", ") }, status: :unprocessable_entity
45
+ end
46
+ end
47
+
48
+ # Action pour supprimer un fichier d'un record
49
+ # DELETE /:resource/:id/remove_file
50
+ def remove_file
51
+ record = instance_variable_get("@#{controller_name.singularize}")
52
+ attribute_name = params[:attribute_name]
53
+ file_id = params[:file_id]
54
+
55
+ unless record
56
+ render json: { error: "Enregistrement non trouvé" }, status: :not_found
57
+ return
58
+ end
59
+
60
+ unless attribute_name.present? && file_id.present?
61
+ render json: { error: "Paramètres manquants" }, status: :bad_request
62
+ return
63
+ end
64
+
65
+ # Sécurité : vérifie que l'attribut existe et est un attachement
66
+ unless record.respond_to?(attribute_name)
67
+ render json: { error: "Attribut inexistant" }, status: :unprocessable_entity
68
+ return
69
+ end
70
+
71
+ # Trouve et supprime l'attachment
72
+ begin
73
+ attachment = ActiveStorage::Attachment.find(file_id)
74
+
75
+ # Sécurité : vérifie que l'attachment appartient bien à ce record
76
+ unless attachment.record_id == record.id && attachment.name == attribute_name
77
+ render json: { error: "Accès refusé" }, status: :forbidden
78
+ return
79
+ end
80
+
81
+ attachment.purge
82
+ render_file_list(record, attribute_name)
83
+ rescue ActiveRecord::RecordNotFound
84
+ render json: { error: "Fichier non trouvé" }, status: :not_found
85
+ end
86
+ end
87
+
88
+ # Lie les uploads temporaires (session) au record avant save
89
+ # Utiliser dans l'action create :
90
+ # wire_temporary_uploads(@record)
91
+ def wire_temporary_uploads(record)
92
+ sid = params[:temp_upload_session_id]
93
+ if sid.present? && sid == UploadDND::SessionId.for(session)
94
+ record.temp_upload_session_id = sid
95
+ end
96
+ end
97
+
98
+ private
99
+
100
+ def render_file_list(record, attribute_name)
101
+ respond_to do |format|
102
+ format.turbo_stream do
103
+ render turbo_stream: turbo_stream.replace(
104
+ "udnd_files_list_#{attribute_name}",
105
+ partial: "upload_d_n_d/shared/files_list",
106
+ locals: { record: record, attribute_name: attribute_name }
107
+ )
108
+ end
109
+ format.json { render json: { status: "ok" }, status: :ok }
110
+ format.html { redirect_back fallback_location: root_path, notice: "Fichier mis à jour" }
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end