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,250 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UploadDND
4
+ # Contrôleur de base contenant toute la logique commune aux attachments
5
+ # permanents et temporaires.
6
+ #
7
+ # Responsabilités:
8
+ # - Validation des paramètres (signed_blob_id, attribute_name)
9
+ # - Authentification et autorisation (Devise, UploadDND::Authorization)
10
+ # - Gestion des erreurs et logging
11
+ # - Helpers de rendu (Turbo Streams, JSON, HTML)
12
+ # - Sécurité (CSRF, validation de signature, sanitisation)
13
+ # - Rate limiting (contrôle du taux de requêtes)
14
+ class BaseAttachmentsController < ActionController::Base
15
+ # Inclure Pundit si disponible
16
+ include Pundit::Authorization if defined?(Pundit)
17
+
18
+ # Inclure le rate limiting
19
+ include UploadDND::RateLimiting
20
+
21
+ # Nom d'attribut autorisé (alphanumérique + underscore uniquement)
22
+ ATTRIBUTE_NAME_FORMAT = /\A[a-z_][a-z0-9_]*\z/
23
+
24
+ # Protection CSRF (désactivée uniquement en test)
25
+ skip_before_action :verify_authenticity_token, if: -> { Rails.env.test? }
26
+
27
+ # Authentification (si disponible dans l'app hôte)
28
+ before_action :authenticate_user_if_available!
29
+
30
+ # Gestion des erreurs
31
+ # IMPORTANT: rescue_from utilise le "dernier match" -> on déclare StandardError en premier
32
+ # pour laisser les handlers spécifiques (RecordNotFound, etc.) prendre le dessus.
33
+ rescue_from StandardError, with: :handle_error
34
+ rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
35
+ rescue_from ActiveStorage::IntegrityError, with: :blob_integrity_error
36
+ rescue_from ActionController::BadRequest, with: :bad_request_error
37
+
38
+ before_action :validate_params, only: [ :create ]
39
+ before_action :validate_signed_blob_id_format, only: [ :create ]
40
+ before_action :find_blob, only: [ :create ]
41
+
42
+ # Déclaré après find_blob (@blob connu) et avant les before_action des
43
+ # sous-classes (autorisation, contraintes), qu'il englobe.
44
+ around_action :discard_blob_on_rejection, only: [ :create ]
45
+
46
+ protected
47
+
48
+ # === Purge des blobs refusés ===
49
+
50
+ # Avec DirectUpload, le blob est déjà dans le bucket : s'il est refusé
51
+ # (réponse en erreur ou exception, relancée), on le purge — sous les
52
+ # garde-fous de UploadDND::RejectedBlob.
53
+ def discard_blob_on_rejection
54
+ yield
55
+ rescue StandardError
56
+ discard_rejected_blob
57
+ raise
58
+ else
59
+ discard_rejected_blob if response.status >= 400
60
+ end
61
+
62
+ # Ne purge que si l'autorisation a été posée comme réussie
63
+ # (grant_upload_authorization!) : une requête refusée faute de droits, ou
64
+ # dont l'autorisation n'a pas abouti (exception, adaptateur inconnu), ne
65
+ # modifie rien. Un échec de la purge ne masque pas l'erreur d'origine : il
66
+ # est journalisé, le blob restera au nettoyage.
67
+ def discard_rejected_blob
68
+ return unless upload_authorized?
69
+
70
+ UploadDND::RejectedBlob.discard(@blob)
71
+ rescue StandardError => e
72
+ Rails.logger.error "UploadDND: rejected blob purge failed - #{e.class}: #{e.message}"
73
+ end
74
+
75
+ # À appeler par chaque contrôle d'autorisation, une fois réussi.
76
+ def grant_upload_authorization!
77
+ @upload_authorized = true
78
+ end
79
+
80
+ def revoke_upload_authorization!
81
+ @upload_authorized = false
82
+ end
83
+
84
+ def upload_authorized?
85
+ @upload_authorized == true
86
+ end
87
+
88
+ # === Callbacks de validation ===
89
+
90
+ # SÉCURITÉ : le signed_id vient du navigateur et figure dans chaque URL de
91
+ # fichier. Un blob déjà rattaché (record ou fichier temporaire) n'est pas
92
+ # rattaché une seconde fois : il serait partagé avec un autre record.
93
+ # À déclarer dans les sous-classes après l'autorisation, avant toute écriture.
94
+ def reject_attached_blob
95
+ return unless @blob&.attachments&.exists?
96
+
97
+ Rails.logger.warn "UploadDND: already attached blob #{@blob.id} refused"
98
+ render_error(I18n.t("upload_d_n_d.errors.blob_already_attached"), :unprocessable_entity)
99
+ end
100
+
101
+ def validate_params
102
+ unless params[:signed_blob_id].present? && params[:attribute_name].present?
103
+ render_error(I18n.t("upload_d_n_d.errors.missing_params"), :bad_request)
104
+ return
105
+ end
106
+
107
+ # Validation du nom d'attribut (alphanumeric + underscore uniquement)
108
+ unless params[:attribute_name].match?(ATTRIBUTE_NAME_FORMAT)
109
+ render_error(I18n.t("upload_d_n_d.errors.invalid_attribute_name"), :bad_request)
110
+ nil
111
+ end
112
+ end
113
+
114
+ def find_blob
115
+ @blob = ActiveStorage::Blob.find_signed(params[:signed_blob_id])
116
+ rescue ActiveSupport::MessageVerifier::InvalidSignature
117
+ render_error(I18n.t("upload_d_n_d.errors.invalid_blob_signature"), :unauthorized)
118
+ nil
119
+ end
120
+
121
+ # === Helpers de rendu ===
122
+
123
+ def render_error(message, status = :unprocessable_entity)
124
+ render json: { error: message }, status: status
125
+ end
126
+
127
+ def render_turbo_stream_replace(target, partial:, locals:)
128
+ html = render_to_string(partial: partial, formats: [ :html ], locals: locals)
129
+ turbo_stream_markup = %(<turbo-stream action="replace" target="#{ERB::Util.html_escape(target)}"><template>#{html}</template></turbo-stream>)
130
+
131
+ render body: turbo_stream_markup, content_type: "text/vnd.turbo-stream.html"
132
+ end
133
+
134
+ # === Gestion des erreurs ===
135
+
136
+ def record_not_found
137
+ render_error(I18n.t("upload_d_n_d.errors.record_not_found"), :not_found)
138
+ end
139
+
140
+ def blob_integrity_error
141
+ render_error(I18n.t("upload_d_n_d.errors.corrupted"), :unprocessable_entity)
142
+ end
143
+
144
+ def handle_error(exception)
145
+ Rails.logger.error "UploadDND Error: #{exception.class} - #{exception.message}"
146
+ Rails.logger.error exception.backtrace.join("\n")
147
+
148
+ render_error(I18n.t("upload_d_n_d.errors.upload_error"), :internal_server_error)
149
+ end
150
+
151
+ # Le détail de l'exception reste dans les logs, jamais dans la réponse.
152
+ def bad_request_error(exception)
153
+ Rails.logger.warn "UploadDND BadRequest: #{exception.message}"
154
+ render_error(I18n.t("upload_d_n_d.errors.bad_request"), :bad_request)
155
+ end
156
+
157
+ # === Logging ===
158
+
159
+ def log_upload_success
160
+ return unless UploadDND.configuration.respond_to?(:on_upload_success)
161
+
162
+ UploadDND.configuration.on_upload_success&.call(@blob, current_user)
163
+ end
164
+
165
+ # === Helpers d'authentification ===
166
+
167
+ def current_user
168
+ # Tente de récupérer current_user depuis l'application hôte
169
+ # En utilisant warden (Devise)
170
+ if defined?(Warden) && request.env["warden"]
171
+ request.env["warden"].user
172
+ elsif respond_to?(:super, true)
173
+ super
174
+ else
175
+ nil
176
+ end
177
+ end
178
+
179
+ def authenticate_user_if_available!
180
+ # Si l'app hôte a Devise, on l'utilise
181
+ if defined?(Devise) && respond_to?(:authenticate_user!, true)
182
+ authenticate_user!
183
+ # Sinon, on vérifie qu'un current_user existe si config l'exige
184
+ elsif UploadDND.configuration.require_authentication
185
+ unless current_user.present?
186
+ render_error(I18n.t("upload_d_n_d.errors.authentication_required"), :unauthorized)
187
+ nil
188
+ end
189
+ end
190
+ end
191
+
192
+ # === Helpers d'autorisation ===
193
+
194
+ # Délègue au port d'autorisation (UploadDND::Authorization) : lève
195
+ # Denied, RuleMissing ou ConfigurationError. Fail-closed sans adaptateur.
196
+ def authorize_with_adapter!(subject, action)
197
+ UploadDND::Authorization.authorize!(self, subject, action)
198
+ end
199
+
200
+ def render_forbidden
201
+ render_error(I18n.t("upload_d_n_d.errors.unauthorized"), :forbidden)
202
+ end
203
+
204
+ # === Helpers de session ===
205
+
206
+ def current_session_id
207
+ UploadDND::SessionId.for(session)
208
+ end
209
+
210
+ # === Validation de sécurité ===
211
+
212
+ def validate_signed_blob_id_format
213
+ signed_blob_id = params[:signed_blob_id]
214
+
215
+ return if signed_blob_id.blank?
216
+
217
+ # Vérifier le format du signed_blob_id
218
+ unless signed_blob_id.is_a?(String) &&
219
+ signed_blob_id.length > 20 &&
220
+ signed_blob_id.match?(/\A[A-Za-z0-9\-_+=]+\z/)
221
+ Rails.logger.warn "UploadDND: Invalid signed_blob_id format attempted"
222
+ render_error(I18n.t("upload_d_n_d.errors.invalid_file_token"), :unauthorized)
223
+ nil
224
+ end
225
+ end
226
+
227
+ def sanitize_filename(filename)
228
+ # Supprime les caractères dangereux du nom de fichier
229
+ filename = filename.to_s.strip
230
+
231
+ # Remplace les caractères dangereux
232
+ filename = filename.gsub(/[^\w\.\-]/, "_")
233
+
234
+ # Limite la longueur
235
+ filename = filename.truncate(255, omission: "")
236
+
237
+ # Assure qu'il reste une extension
238
+ filename = "file#{File.extname(filename)}" if filename.blank?
239
+
240
+ filename
241
+ end
242
+
243
+ # === Support Turbo Streams ===
244
+
245
+ def turbo_stream_request?
246
+ params[:format].to_s == "turbo_stream" ||
247
+ request.headers["Accept"].to_s.include?("text/vnd.turbo-stream.html")
248
+ end
249
+ end
250
+ end
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UploadDND
4
+ # Contrôleur dédié à la gestion des attachments temporaires.
5
+ #
6
+ # Responsabilités:
7
+ # - Créer des TemporaryAttachment pour les formulaires qui n'ont pas encore de record
8
+ # - Supprimer des fichiers temporaires (avant soumission du formulaire)
9
+ # - Renvoyer la liste des fichiers temporaires pour mise à jour UI
10
+ #
11
+ # Utilisé par:
12
+ # - GET /upload_d_n_d/temporary_attachments?attribute_name=x (liste de la session courante)
13
+ # - POST /upload_d_n_d/temporary_attachments (lors de l'upload)
14
+ # - DELETE /upload_d_n_d/temporary_attachments/:id (suppression avant soumission)
15
+ # - DELETE /upload_d_n_d/temporary_attachments?attribute_name=x (purge, ex: annulation du formulaire)
16
+ #
17
+ # Workflow:
18
+ # 1. User upload file in form (no record yet) → create TemporaryAttachment
19
+ # 2. User submit form → UsersController calls attach_upload_d_n_d_files helper
20
+ # 3. Helper transfers temp files to real record and destroys TemporaryAttachment
21
+ class TemporaryAttachmentsController < BaseAttachmentsController
22
+ # Autorisation (UploadDND::Authorization, fail-closed)
23
+ before_action :authorize_upload!, only: [ :create ]
24
+ before_action :authorize_delete!, only: [ :destroy, :purge ]
25
+
26
+ before_action :find_temp_attachment, only: [ :destroy ]
27
+ before_action :reject_attached_blob, only: [ :create ]
28
+ before_action :validate_blob_constraints, only: [ :create ]
29
+
30
+ # GET /upload_d_n_d/temporary_attachments?attribute_name=documents
31
+ #
32
+ # Renvoie le HTML du partial `temp_files_list` pour les fichiers temporaires
33
+ # de la session courante (ex: restaurer la zone après un rechargement de page
34
+ # sans repasser par le composant).
35
+ #
36
+ # Sécurité: pas d'autorisation Pundit — le scope par session_id est le filtre.
37
+ # Un utilisateur ne peut voir que ses propres fichiers temporaires.
38
+ def index
39
+ attribute_name = params[:attribute_name].to_s
40
+ return render_error(I18n.t("upload_d_n_d.errors.missing_params"), :bad_request) if attribute_name.blank?
41
+ return render_error(I18n.t("upload_d_n_d.errors.invalid_attribute_name"), :bad_request) unless attribute_name.match?(ATTRIBUTE_NAME_FORMAT)
42
+
43
+ files = TemporaryAttachment.for_session(current_session_id)
44
+ .for_attribute(attribute_name)
45
+ .with_blobs
46
+
47
+ render partial: "upload_d_n_d/shared/temp_files_list",
48
+ formats: [ :html ],
49
+ locals: { files: files, attribute_name: attribute_name },
50
+ layout: false
51
+ end
52
+
53
+ # POST /upload_d_n_d/temporary_attachments
54
+ #
55
+ # Params:
56
+ # - signed_blob_id: String (ActiveStorage signed blob ID)
57
+ # - attribute_name: String (ex: "avatar", "documents")
58
+ #
59
+ # Returns:
60
+ # - Turbo Stream pour remplacer la liste de fichiers
61
+ # - JSON {status: 'ok'} en mode API
62
+ def create
63
+ attribute_name = params[:attribute_name]
64
+
65
+ # Sanitize filename avant de créer l'attachment
66
+ @blob.update(filename: sanitize_filename(@blob.filename.to_s))
67
+
68
+ # SÉCURITÉ : Pour les attributs au singulier (has_one_attached probable),
69
+ # purger les fichiers temporaires existants avant d'en créer un nouveau
70
+ # Convention Rails : avatar, cover, logo = singulier = has_one
71
+ # documents, images, files = pluriel = has_many
72
+ if attribute_name.to_s == attribute_name.to_s.singularize
73
+ # Attribut au singulier → probablement has_one_attached
74
+ # Purger les fichiers temporaires existants pour cette session + attribut
75
+ TemporaryAttachment.for_session(current_session_id)
76
+ .for_attribute(attribute_name)
77
+ .destroy_all
78
+ end
79
+
80
+ temp_attachment = TemporaryAttachment.create!(
81
+ session_id: current_session_id,
82
+ attribute_name: attribute_name,
83
+ user_id: current_user&.id
84
+ )
85
+ temp_attachment.blob.attach(@blob)
86
+
87
+ render_temp_file_list(attribute_name)
88
+ log_upload_success
89
+ rescue ActiveRecord::RecordInvalid => e
90
+ render_error(I18n.t("upload_d_n_d.errors.save_failed", errors: e.message), :unprocessable_entity)
91
+ end
92
+
93
+ # DELETE /upload_d_n_d/temporary_attachments/:id
94
+ #
95
+ # Supprime un fichier temporaire avant soumission du formulaire.
96
+ #
97
+ # Sécurité: On ne peut supprimer que les fichiers de sa propre session/user_id.
98
+ def destroy
99
+ attribute_name = @temp_attachment.attribute_name
100
+ @temp_attachment.destroy
101
+
102
+ render_temp_file_list(attribute_name)
103
+ log_deletion
104
+ end
105
+
106
+ # DELETE /upload_d_n_d/temporary_attachments?attribute_name=documents
107
+ #
108
+ # Purge tous les fichiers temporaires de la session courante pour cet attribut.
109
+ # Typiquement appelé par le bouton « Annuler » d'un formulaire de création,
110
+ # pour que les fichiers déposés ne réapparaissent pas au prochain formulaire.
111
+ #
112
+ # Sécurité: scopé sur la session courante uniquement. Renvoie 204.
113
+ def purge
114
+ attribute_name = params[:attribute_name].to_s
115
+ return render_error(I18n.t("upload_d_n_d.errors.missing_params"), :bad_request) if attribute_name.blank?
116
+ return render_error(I18n.t("upload_d_n_d.errors.invalid_attribute_name"), :bad_request) unless attribute_name.match?(ATTRIBUTE_NAME_FORMAT)
117
+
118
+ purged = TemporaryAttachment.for_session(current_session_id)
119
+ .for_attribute(attribute_name)
120
+ .destroy_all
121
+
122
+ Rails.logger.info "UploadDND: #{purged.size} temporary file(s) purged - #{attribute_name} by #{current_user&.id || 'anonymous'}"
123
+ head :no_content
124
+ end
125
+
126
+ private
127
+
128
+ def find_temp_attachment
129
+ # Sécurité: on ne peut supprimer que les fichiers de sa propre session ou utilisateur
130
+ scope = TemporaryAttachment.where(session_id: current_session_id)
131
+ scope = scope.or(TemporaryAttachment.where(user_id: current_user&.id)) if current_user
132
+
133
+ @temp_attachment = scope.find(params[:id])
134
+ rescue ActiveRecord::RecordNotFound
135
+ render_error(I18n.t("upload_d_n_d.errors.file_not_found_or_denied"), :not_found)
136
+ end
137
+
138
+ def render_temp_file_list(attribute_name)
139
+ files = TemporaryAttachment.for_session(current_session_id)
140
+ .for_attribute(attribute_name)
141
+ .with_blobs
142
+
143
+ if turbo_stream_request?
144
+ render_turbo_stream_replace(
145
+ "udnd_files_list_#{attribute_name}",
146
+ partial: "upload_d_n_d/shared/temp_files_list",
147
+ locals: { files: files, attribute_name: attribute_name }
148
+ )
149
+ else
150
+ respond_to do |format|
151
+ format.json { render json: { status: "ok", message: I18n.t("upload_d_n_d.success.file_uploaded") }, status: :ok }
152
+ format.html { redirect_back fallback_location: root_path, notice: I18n.t("upload_d_n_d.success.file_added") }
153
+ format.any { render json: { status: "ok", message: I18n.t("upload_d_n_d.success.file_uploaded") }, status: :ok }
154
+ end
155
+ end
156
+ end
157
+
158
+ def log_deletion
159
+ Rails.logger.info "UploadDND: Temporary file deleted - #{@temp_attachment.attribute_name} by #{current_user&.id || 'anonymous'}"
160
+ end
161
+
162
+ # Limites du champ visé : celles du modèle si le composant a fourni sa
163
+ # référence signée (field_ref), sinon la configuration globale (ancien
164
+ # client, appel direct).
165
+ def validate_blob_constraints
166
+ return unless @blob
167
+
168
+ constraints = field_constraints
169
+ return if performed?
170
+
171
+ violation = constraints.violation(@blob)
172
+ render_error(constraints.error_message(violation), :unprocessable_entity) if violation
173
+ end
174
+
175
+ def field_constraints
176
+ return UploadDND::FieldConstraints.global if params[:field_ref].blank?
177
+
178
+ UploadDND::FieldReference.constraints(params[:field_ref], params[:attribute_name])
179
+ rescue UploadDND::FieldReference::Invalid
180
+ Rails.logger.warn "UploadDND: invalid field reference for #{params[:attribute_name]}"
181
+ render_error(I18n.t("upload_d_n_d.errors.invalid_field_reference"), :bad_request)
182
+ nil
183
+ end
184
+
185
+ # === Autorisation ===
186
+
187
+ def authorize_upload!
188
+ authorize_with_adapter!(:upload_d_n_d_temporary_attachment, :create)
189
+ grant_upload_authorization!
190
+ rescue UploadDND::Authorization::Denied
191
+ render_forbidden
192
+ end
193
+
194
+ def authorize_delete!
195
+ authorize_with_adapter!(:upload_d_n_d_temporary_attachment, :destroy)
196
+ rescue UploadDND::Authorization::Denied
197
+ render_forbidden
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UploadDND
4
+ module ApplicationHelper
5
+ # URL de suppression d'un fichier attaché.
6
+ #
7
+ # Mode :engine (défaut) : route DELETE du moteur de la gem, identifiée
8
+ # par record_gid — aucune route requise côté app, quel que soit le
9
+ # routing du record (imbriqué, singulier, namespacé).
10
+ #
11
+ # Mode :host (legacy, config.delete_route = :host) : route membre
12
+ # remove_file du record côté app (convention : resources + member
13
+ # remove_file). Nécessite une route polymorphique — voir README,
14
+ # section "Records imbriqués". Point d'extension : redéfinissez cette
15
+ # méthode dans le ApplicationHelper de votre app.
16
+ def udnd_remove_file_url(record, attachment, attribute_name)
17
+ if UploadDND.configuration.delete_route == :host
18
+ query = { file_id: attachment.id, attribute_name: attribute_name }.to_query
19
+ "#{udnd_record_path(record)}/remove_file?#{query}"
20
+ else
21
+ UploadDND::Engine.routes.url_helpers.attachment_path(
22
+ attachment.id,
23
+ record_gid: record.to_global_id.to_s,
24
+ attribute_name: attribute_name
25
+ )
26
+ end
27
+ end
28
+
29
+ def udnd_record_path(record)
30
+ main_app.polymorphic_path(record)
31
+ rescue NoMethodError, ActionController::UrlGenerationError
32
+ raise UploadDND::UnroutableRecordError, <<~MSG
33
+ #{record.class.name} n'a pas de route polymorphique : impossible de
34
+ construire l'URL de suppression de fichier.
35
+
36
+ Deux remèdes :
37
+
38
+ 1. Déclarer la résolution polymorphique dans config/routes.rb (recommandé).
39
+ Exemple pour une resource singulière imbriquée
40
+ (resource :convention sous resources :projects) :
41
+
42
+ resolve("Convention") { |convention, options| route_for(:project_convention, convention.project, options) }
43
+
44
+ 2. Ou surcharger udnd_remove_file_url(record, attachment, attribute_name)
45
+ dans le ApplicationHelper de votre app.
46
+
47
+ Voir la section "Records imbriqués" du README de upload_d_n_d.
48
+ MSG
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,21 @@
1
+ # lib/upload_d_n_d/controllers/helpers.rb
2
+ module UploadDND
3
+ module Controllers
4
+ module Helpers
5
+ extend ActiveSupport::Concern
6
+
7
+ def attach_upload_d_n_d_files(record, attribute_name)
8
+ # On cherche les fichiers temporaires liés à cette session
9
+ temps = UploadDND::TemporaryAttachment.where(
10
+ session_id: UploadDND::SessionId.for(session),
11
+ attribute_name: attribute_name
12
+ )
13
+
14
+ temps.each do |temp|
15
+ record.send(attribute_name).attach(temp.blob)
16
+ temp.destroy # On nettoie après attachement
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end