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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +229 -0
- data/Rakefile +8 -0
- data/app/assets/config/upload_d_n_d_manifest.js +3 -0
- data/app/assets/stylesheets/upload_d_n_d/application.css +604 -0
- data/app/components/upload_d_n_d/upload_component.html.erb +53 -0
- data/app/components/upload_d_n_d/upload_component.rb +204 -0
- data/app/controllers/concerns/upload_d_n_d/rate_limiting.rb +89 -0
- data/app/controllers/upload_d_n_d/attachments_controller.rb +218 -0
- data/app/controllers/upload_d_n_d/base_attachments_controller.rb +250 -0
- data/app/controllers/upload_d_n_d/temporary_attachments_controller.rb +200 -0
- data/app/helpers/upload_d_n_d/application_helper.rb +51 -0
- data/app/helpers/upload_d_n_d/controllers/helpers.rb +21 -0
- data/app/javascript/upload_d_n_d/controllers/preview_controller.js +217 -0
- data/app/javascript/upload_d_n_d/controllers/uploads_controller.js +396 -0
- data/app/javascript/upload_d_n_d/index.js +13 -0
- data/app/jobs/upload_d_n_d/application_job.rb +4 -0
- data/app/jobs/upload_d_n_d/cleanup_temporary_attachments_job.rb +17 -0
- data/app/jobs/upload_d_n_d/purge_orphan_blobs_job.rb +25 -0
- data/app/jobs/upload_d_n_d/purge_rejected_blob_job.rb +24 -0
- data/app/models/upload_d_n_d/application_record.rb +5 -0
- data/app/models/upload_d_n_d/temporary_attachment.rb +129 -0
- data/app/views/upload_d_n_d/shared/_file_icon.html.erb +9 -0
- data/app/views/upload_d_n_d/shared/_files_list.html.erb +63 -0
- data/app/views/upload_d_n_d/shared/_temp_files_list.html.erb +53 -0
- data/config/importmap.rb +5 -0
- data/config/initializers/inflections.rb +11 -0
- data/config/locales/upload_d_n_d.en.yml +80 -0
- data/config/locales/upload_d_n_d.fr.yml +80 -0
- data/config/routes.rb +12 -0
- data/docs/CHANGELOG.md +282 -0
- data/lib/generators/upload_d_n_d/POLICY_README +49 -0
- data/lib/generators/upload_d_n_d/USAGE +80 -0
- data/lib/generators/upload_d_n_d/install_generator.rb +174 -0
- data/lib/generators/upload_d_n_d/mime_type_helper.rb +106 -0
- data/lib/generators/upload_d_n_d/model_generator.rb +313 -0
- data/lib/generators/upload_d_n_d/policy_generator.rb +25 -0
- data/lib/generators/upload_d_n_d/templates/POST_INSTALL +62 -0
- data/lib/generators/upload_d_n_d/templates/concerns/controllers_uploadable.rb +115 -0
- data/lib/generators/upload_d_n_d/templates/concerns/upload_authorization.rb +91 -0
- data/lib/generators/upload_d_n_d/templates/concerns/uploadable.rb +101 -0
- data/lib/generators/upload_d_n_d/templates/controller.rb.tt +60 -0
- data/lib/generators/upload_d_n_d/templates/helpers/upload_helper.rb +105 -0
- data/lib/generators/upload_d_n_d/templates/initializers/upload_d_n_d.rb +69 -0
- data/lib/generators/upload_d_n_d/templates/migrations/create_upload_d_n_d_temporary_attachments.rb +18 -0
- data/lib/generators/upload_d_n_d/templates/policies/upload_d_n_d_attachment_policy.rb +37 -0
- data/lib/generators/upload_d_n_d/templates/policies/upload_d_n_d_temporary_attachment_policy.rb +56 -0
- data/lib/generators/upload_d_n_d/templates/validators/file_validator.rb +171 -0
- data/lib/tasks/upload_d_n_d_storage.rake +27 -0
- data/lib/tasks/upload_d_n_d_tasks.rake +20 -0
- data/lib/upload_d_n_d/attachment_lookup.rb +24 -0
- data/lib/upload_d_n_d/authorization/can_can_can_adapter.rb +29 -0
- data/lib/upload_d_n_d/authorization/no_authorization.rb +13 -0
- data/lib/upload_d_n_d/authorization/pundit_adapter.rb +41 -0
- data/lib/upload_d_n_d/authorization/unconfigured.rb +15 -0
- data/lib/upload_d_n_d/authorization.rb +66 -0
- data/lib/upload_d_n_d/configuration.rb +97 -0
- data/lib/upload_d_n_d/controllers/uploadable.rb +271 -0
- data/lib/upload_d_n_d/engine.rb +50 -0
- data/lib/upload_d_n_d/field_constraints.rb +92 -0
- data/lib/upload_d_n_d/field_reference.rb +44 -0
- data/lib/upload_d_n_d/file_validator.rb +156 -0
- data/lib/upload_d_n_d/mime_types.rb +75 -0
- data/lib/upload_d_n_d/orphan_blobs.rb +151 -0
- data/lib/upload_d_n_d/rejected_blob.rb +42 -0
- data/lib/upload_d_n_d/session_id.rb +25 -0
- data/lib/upload_d_n_d/uploadable.rb +117 -0
- data/lib/upload_d_n_d/version.rb +3 -0
- data/lib/upload_d_n_d.rb +62 -0
- 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
|