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,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UploadDND
4
+ # Purge un blob refusé (cf. UploadDND::RejectedBlob).
5
+ #
6
+ # Contrairement à ActiveStorage::PurgeJob, revérifie au moment de
7
+ # l'exécution que le blob n'a toujours aucun attachment : il a pu être
8
+ # rattaché entre le refus et l'exécution du job (nouvel essai réussi avec le
9
+ # même signed_id). L'âge n'est pas revérifié : il a été contrôlé au refus, et
10
+ # le délai de la file ne doit pas empêcher la purge.
11
+ class PurgeRejectedBlobJob < ApplicationJob
12
+ queue_as { ActiveStorage.queues[:purge] }
13
+
14
+ retry_on ActiveRecord::Deadlocked, attempts: 10, wait: :polynomially_longer
15
+
16
+ def perform(blob_id)
17
+ blob = ActiveStorage::Blob.find_by(id: blob_id)
18
+ return unless blob
19
+ return unless UploadDND::RejectedBlob.unattached?(blob)
20
+
21
+ blob.purge
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,5 @@
1
+ module UploadDND
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UploadDND
4
+ class TemporaryAttachment < ApplicationRecord
5
+ self.table_name = "upload_d_n_d_temporary_attachments"
6
+
7
+ # Association avec ActiveStorage
8
+ has_one_attached :blob
9
+
10
+ # Validations
11
+ validates :session_id, presence: true
12
+ validates :attribute_name, presence: true
13
+
14
+ # Scopes
15
+ scope :for_session, ->(session_id) { where(session_id: session_id) }
16
+ scope :for_attribute, ->(attribute_name) { where(attribute_name: attribute_name) }
17
+ scope :for_user, ->(user_id) { where(user_id: user_id) }
18
+ scope :expired, -> {
19
+ retention_days = UploadDND.configuration.temporary_files_retention || 7
20
+ where("created_at < ?", retention_days.days.ago)
21
+ }
22
+ # Eager load blob attachments to prevent N+1 queries
23
+ scope :with_blobs, -> { includes(blob_attachment: :blob) }
24
+
25
+ # Callbacks
26
+ before_destroy :purge_blob
27
+
28
+ # Méthodes de classe
29
+ class << self
30
+ # Nettoie les fichiers temporaires expirés
31
+ # À appeler via un job cron ou Sidekiq
32
+ def cleanup_expired!
33
+ expired.find_each do |attachment|
34
+ attachment.destroy
35
+ end
36
+ end
37
+
38
+ # Attache tous les fichiers temporaires d'une session à un record
39
+ #
40
+ # @param session_id [String] l'ID de session
41
+ # @param record [ApplicationRecord] le record cible
42
+ # @param attribute_map [Hash] mapping des attributs (optionnel)
43
+ #
44
+ # @example
45
+ # TemporaryAttachment.attach_to_record(
46
+ # session.id.to_s,
47
+ # @user,
48
+ # { 'avatar' => :avatar, 'documents' => :documents }
49
+ # )
50
+ def attach_to_record(session_id, record, attribute_map = {})
51
+ for_session(session_id).find_each do |temp_attachment|
52
+ source_attr = temp_attachment.attribute_name
53
+ target_attr = attribute_map[source_attr]&.to_s || source_attr
54
+
55
+ # SÉCURITÉ : attribute_name vient du client — seul un attachment
56
+ # déclaré sur le modèle est résolu, jamais une méthode quelconque.
57
+ attachment = UploadDND::AttachmentLookup.find(record, target_attr)
58
+ unless attachment
59
+ temp_attachment.destroy
60
+ next
61
+ end
62
+
63
+ blob_attachment = temp_attachment.blob_attachment
64
+ if blob_attachment.nil? || temp_attachment.violates_constraints_of?(record, target_attr)
65
+ temp_attachment.destroy
66
+ next
67
+ end
68
+
69
+ if attachment.is_a?(ActiveStorage::Attached::Many)
70
+ blob_attachment.update!(record: record, name: target_attr)
71
+ elsif attachment.is_a?(ActiveStorage::Attached::One)
72
+ if attachment.attached?
73
+ temp_attachment.destroy
74
+ next
75
+ end
76
+ blob_attachment.update!(record: record, name: target_attr)
77
+ end
78
+
79
+ temp_attachment.reload
80
+ temp_attachment.destroy
81
+ end
82
+ end
83
+ end
84
+
85
+ # Méthodes d'instance
86
+
87
+ # Le transfert vers le record ne passe pas par la validation du modèle :
88
+ # les limites du champ cible sont vérifiées ici. Couvre aussi un dépôt
89
+ # fait sans référence de champ (limites globales seulement).
90
+ def violates_constraints_of?(record, attribute_name)
91
+ violation = UploadDND::FieldConstraints.for(record.class, attribute_name).violation(blob.blob)
92
+ return false unless violation
93
+
94
+ Rails.logger.warn "UploadDND: temporary file #{id} refused on transfer to " \
95
+ "#{record.class}##{attribute_name} - #{violation}"
96
+ true
97
+ end
98
+ def filename
99
+ blob.blob.filename.to_s if blob.attached?
100
+ end
101
+
102
+ def content_type
103
+ blob.blob.content_type if blob.attached?
104
+ end
105
+
106
+ def byte_size
107
+ blob.blob.byte_size if blob.attached?
108
+ end
109
+
110
+ def url
111
+ # polymorphic_url respecte config.active_storage.resolve_model_to_route
112
+ # de l'app hôte (:rails_storage_redirect par défaut, :rails_storage_proxy
113
+ # pour servir les blobs en same-origin — CSP stricte, bucket externe).
114
+ # only_path: true explicite : la forme _path ne le propage pas aux
115
+ # routes `resolve` d'ActiveStorage hors contexte de requête.
116
+ Rails.application.routes.url_helpers.polymorphic_url(blob.blob, only_path: true) if blob.attached?
117
+ end
118
+
119
+ private
120
+
121
+ def purge_blob
122
+ # Si l'attachment ActiveStorage a été "déplacé" vers un autre record,
123
+ # `blob_attachment` sera nil (après reload) -> on ne purge rien.
124
+ return unless blob_attachment
125
+
126
+ blob.purge if blob.attached?
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,9 @@
1
+ <%# Icône selon le type de fichier. Surchargez ce partial dans votre app
2
+ (app/views/upload_d_n_d/shared/_file_icon.html.erb) pour personnaliser. %>
3
+ <% if blob.image? %>
4
+ <svg class="udnd-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/><circle cx="10" cy="12" r="2"/><path d="m20 17-1.296-1.296a2.41 2.41 0 0 0-3.408 0L9 22"/></svg>
5
+ <% elsif blob.content_type == 'application/pdf' %>
6
+ <svg class="udnd-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg>
7
+ <% else %>
8
+ <svg class="udnd-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/></svg>
9
+ <% end %>
@@ -0,0 +1,63 @@
1
+ <div id="udnd_files_list_<%= attribute_name %>" class="udnd-files-list">
2
+ <% attachments = record&.send(attribute_name) %>
3
+ <% if attachments.present? && attachments.attached? %>
4
+ <ul class="udnd-file-items">
5
+ <%
6
+ # Récupère les attachments ActiveStorage (pas les blobs)
7
+ attachment_list = if attachments.respond_to?(:each)
8
+ attachments # has_many_attached retourne déjà les attachments
9
+ else
10
+ [attachments] # has_one_attached, on met dans un array
11
+ end
12
+ %>
13
+ <% attachment_list.each do |attachment| %>
14
+ <% blob = attachment.blob %>
15
+ <li class="udnd-file-item" id="<%= dom_id(attachment) %>">
16
+ <div class="udnd-file-info">
17
+ <span class="udnd-file-icon">
18
+ <%= render "upload_d_n_d/shared/file_icon", blob: blob %>
19
+ </span>
20
+ <div class="udnd-file-details">
21
+ <span class="udnd-filename"><%= blob.filename.to_s.truncate(30) %></span>
22
+ <span class="udnd-filesize"><%= number_to_human_size(blob.byte_size) %></span>
23
+ </div>
24
+ </div>
25
+
26
+ <% delete_url = udnd_remove_file_url(record, attachment, attribute_name) %>
27
+
28
+ <div class="udnd-file-actions">
29
+ <%# Bouton de prévisualisation (images et PDFs uniquement) %>
30
+ <% if blob.image? || blob.content_type == 'application/pdf' %>
31
+ <button type="button"
32
+ class="udnd-btn-preview"
33
+ data-controller="upload-d-n-d--preview"
34
+ data-action="click->upload-d-n-d--preview#open"
35
+ data-preview-url="<%= main_app.polymorphic_path(blob, disposition: "inline") %>"
36
+ data-preview-filename="<%= blob.filename %>"
37
+ data-preview-content-type="<%= blob.content_type %>"
38
+ data-preview-i18n-unavailable="<%= t('upload_d_n_d.javascript.preview.unavailable') %>"
39
+ data-preview-i18n-download="<%= t('upload_d_n_d.javascript.preview.download') %>"
40
+ data-preview-i18n-close="<%= t('upload_d_n_d.javascript.preview.close') %>"
41
+ title="<%= t('upload_d_n_d.preview_file') %>"
42
+ aria-label="<%= t('upload_d_n_d.preview_file') %> : <%= blob.filename %>">
43
+ <svg class="udnd-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/></svg>
44
+ </button>
45
+ <% end %>
46
+
47
+ <%# Bouton de suppression %>
48
+ <button type="button"
49
+ class="udnd-btn-remove"
50
+ data-action="click->upload-d-n-d--uploads#confirmDelete"
51
+ data-url="<%= delete_url %>"
52
+ title="<%= t('upload_d_n_d.delete_file') %>"
53
+ aria-label="<%= t('upload_d_n_d.delete_file') %> : <%= blob.filename %>">
54
+ <svg class="udnd-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
55
+ </button>
56
+ </div>
57
+ </li>
58
+ <% end %>
59
+ </ul>
60
+ <% else %>
61
+ <p class="udnd-no-files"><%= t('upload_d_n_d.no_files', default: 'Aucun fichier') %></p>
62
+ <% end %>
63
+ </div>
@@ -0,0 +1,53 @@
1
+ <%# app/views/upload_d_n_d/shared/_temp_files_list.html.erb %>
2
+ <div id="udnd_files_list_<%= attribute_name %>" class="udnd-files-list">
3
+ <% if files.any? %>
4
+ <ul class="udnd-file-items">
5
+ <% files.each do |temp_attachment| %>
6
+ <% blob = temp_attachment.blob.blob %>
7
+ <li class="udnd-file-item" id="temp_<%= temp_attachment.id %>">
8
+ <div class="udnd-file-info">
9
+ <span class="udnd-file-icon">
10
+ <%= render "upload_d_n_d/shared/file_icon", blob: blob %>
11
+ </span>
12
+ <div class="udnd-file-details">
13
+ <span class="udnd-filename"><%= blob.filename.to_s.truncate(30) %></span>
14
+ <span class="udnd-filesize"><%= number_to_human_size(blob.byte_size) %></span>
15
+ </div>
16
+ </div>
17
+
18
+ <div class="udnd-file-actions">
19
+ <%# Bouton de prévisualisation (images et PDFs uniquement) %>
20
+ <% if blob.image? || blob.content_type == 'application/pdf' %>
21
+ <button type="button"
22
+ class="udnd-btn-preview"
23
+ data-controller="upload-d-n-d--preview"
24
+ data-action="click->upload-d-n-d--preview#open"
25
+ data-preview-url="<%= main_app.polymorphic_path(blob, disposition: "inline") %>"
26
+ data-preview-filename="<%= blob.filename %>"
27
+ data-preview-content-type="<%= blob.content_type %>"
28
+ data-preview-i18n-unavailable="<%= t('upload_d_n_d.javascript.preview.unavailable') %>"
29
+ data-preview-i18n-download="<%= t('upload_d_n_d.javascript.preview.download') %>"
30
+ data-preview-i18n-close="<%= t('upload_d_n_d.javascript.preview.close') %>"
31
+ title="<%= t('upload_d_n_d.preview_file') %>"
32
+ aria-label="<%= t('upload_d_n_d.preview_file') %> : <%= blob.filename %>">
33
+ <svg class="udnd-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/></svg>
34
+ </button>
35
+ <% end %>
36
+
37
+ <%# Bouton de suppression %>
38
+ <button type="button"
39
+ class="udnd-btn-remove"
40
+ data-action="click->upload-d-n-d--uploads#confirmDelete"
41
+ data-url="<%= UploadDND::Engine.routes.url_helpers.temporary_attachment_path(temp_attachment) %>"
42
+ title="<%= t('upload_d_n_d.delete_file') %>"
43
+ aria-label="<%= t('upload_d_n_d.delete_file') %> : <%= blob.filename %>">
44
+ <svg class="udnd-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
45
+ </button>
46
+ </div>
47
+ </li>
48
+ <% end %>
49
+ </ul>
50
+ <% else %>
51
+ <p class="udnd-no-files"><%= t('upload_d_n_d.no_files', default: 'Aucun fichier sélectionné') %></p>
52
+ <% end %>
53
+ </div>
@@ -0,0 +1,5 @@
1
+ # Point d'entrée principal : import "upload_d_n_d" dans application.js
2
+ pin "upload_d_n_d", to: "upload_d_n_d/index.js"
3
+
4
+ # Pins individuels (pour import sélectif si besoin)
5
+ pin_all_from File.expand_path("../app/javascript/upload_d_n_d/controllers", __dir__), under: "upload_d_n_d/controllers"
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Acronymes de l'app
4
+ # Permet notamment de constantiser correctement `upload_dnd/*` en `UploadDND::*`
5
+ ActiveSupport::Inflector.inflections(:en) do |inflect|
6
+ inflect.acronym "DND"
7
+ end
8
+
9
+ ActiveSupport::Inflector.inflections(:fr) do |inflect|
10
+ inflect.acronym "DND"
11
+ end
@@ -0,0 +1,80 @@
1
+ en:
2
+ upload_d_n_d:
3
+ drag_and_drop: "Drag and drop your files here or click to select"
4
+ no_files: "No files"
5
+ progress: "Upload progress"
6
+ preview_file: "Preview"
7
+ delete_file: "Delete"
8
+
9
+ errors:
10
+ blob_not_found: "File not found"
11
+ blob_already_attached: "This file is already in use elsewhere. Please upload it again."
12
+ corrupted: "The file appears to be corrupted or modified"
13
+ unrepresentable: "The file cannot be processed"
14
+ unauthorized: "You are not authorized to update this file"
15
+ invalid_type: "Invalid file type. Accepted formats: %{types}"
16
+ too_large: "File exceeds maximum size of %{max_size}"
17
+ suspicious_type: "The actual file type does not match the declared type"
18
+ mime_detection_failed: "Unable to verify file type."
19
+ bad_request: "Invalid request."
20
+ invalid_field_reference: "Invalid file field. Please reload the page."
21
+ max_files_exceeded: "You cannot add more than %{max} files"
22
+ file_not_found_or_denied: "File not found or access denied"
23
+ save_failed: "Save failed: %{errors}"
24
+ invalid_attribute: "Attribute %{attribute} does not exist"
25
+ not_active_storage_attachment: "Attribute %{attribute} is not an ActiveStorage attachment"
26
+ record_gid_required: "The record_gid parameter is required to attach to an existing record. Use /temporary_attachments for uploads without a record."
27
+ missing_params: "Missing parameters"
28
+ invalid_attribute_name: "Invalid attribute name"
29
+ invalid_blob_signature: "Invalid blob signature"
30
+ record_not_found: "Record not found"
31
+ upload_error: "An error occurred during upload"
32
+ authentication_required: "You must be logged in to upload files"
33
+ invalid_file_token: "Invalid file token"
34
+ rate_limited: "Too many requests. Please wait %{seconds} seconds."
35
+
36
+ success:
37
+ file_added: "File added successfully"
38
+ file_removed: "File removed successfully"
39
+ file_uploaded: "File uploaded successfully"
40
+
41
+ confirmations:
42
+ delete_file: "Are you sure you want to delete this file?"
43
+ replace_file: "A file is already attached. Do you want to replace it?"
44
+
45
+ javascript:
46
+ alerts:
47
+ single_file_only:
48
+ title: "Single file only"
49
+ text: "You can only upload one file for this field."
50
+ upload_error:
51
+ title: "Error"
52
+ text: "Unable to upload file"
53
+ file_too_large:
54
+ title: "Too large"
55
+ text: "Max: %{max_size}MB"
56
+ empty_file:
57
+ title: "Empty file"
58
+ text: "File cannot be empty"
59
+ invalid_format:
60
+ title: "Invalid format"
61
+ text: "Accepted: %{accepted_types}"
62
+ suspicious_type:
63
+ title: "Suspicious file type"
64
+ text: "The actual file type does not match the extension"
65
+ attach_error:
66
+ title: "Error"
67
+ text: "Unable to attach file"
68
+ delete_error:
69
+ title: "Error"
70
+ text: "Unable to delete file"
71
+ confirms:
72
+ delete_file:
73
+ title: "Delete?"
74
+ text: "Do you really want to remove this file?"
75
+ confirm_button: "Delete"
76
+ cancel_button: "Cancel"
77
+ preview:
78
+ unavailable: "Preview not available for this file type"
79
+ download: "Download file"
80
+ close: "Close"
@@ -0,0 +1,80 @@
1
+ fr:
2
+ upload_d_n_d:
3
+ drag_and_drop: "Glissez-déposez vos fichiers ici ou cliquez pour sélectionner"
4
+ no_files: "Aucun fichier"
5
+ progress: "Progression de l'upload"
6
+ preview_file: "Aperçu"
7
+ delete_file: "Supprimer"
8
+
9
+ errors:
10
+ blob_not_found: "Fichier non trouvé"
11
+ blob_already_attached: "Ce fichier est déjà utilisé ailleurs. Déposez-le à nouveau."
12
+ corrupted: "Le fichier semble corrompu ou modifié"
13
+ unrepresentable: "Le fichier n'est pas traitable"
14
+ unauthorized: "Vous n'êtes pas autorisé à modifier ce fichier"
15
+ invalid_type: "Type de fichier non accepté. Formats autorisés : %{types}"
16
+ too_large: "Le fichier dépasse la taille maximale de %{max_size}"
17
+ suspicious_type: "Le type réel du fichier ne correspond pas au type déclaré"
18
+ mime_detection_failed: "Impossible de vérifier le type de fichier."
19
+ bad_request: "Requête invalide."
20
+ invalid_field_reference: "Champ de fichier invalide. Rechargez la page."
21
+ max_files_exceeded: "Vous ne pouvez pas ajouter plus de %{max} fichiers"
22
+ file_not_found_or_denied: "Fichier non trouvé ou accès refusé"
23
+ save_failed: "Erreur lors de l'enregistrement : %{errors}"
24
+ invalid_attribute: "L'attribut %{attribute} n'existe pas"
25
+ not_active_storage_attachment: "L'attribut %{attribute} n'est pas un attachment ActiveStorage"
26
+ record_gid_required: "Le paramètre record_gid est requis pour attacher à un record existant. Utilisez /temporary_attachments pour les uploads sans record."
27
+ missing_params: "Paramètres manquants"
28
+ invalid_attribute_name: "Nom d'attribut invalide"
29
+ invalid_blob_signature: "Signature du blob invalide"
30
+ record_not_found: "Enregistrement non trouvé"
31
+ upload_error: "Une erreur est survenue lors de l'upload"
32
+ authentication_required: "Vous devez être connecté pour uploader des fichiers"
33
+ invalid_file_token: "Token de fichier invalide"
34
+ rate_limited: "Trop de requêtes. Veuillez patienter %{seconds} secondes."
35
+
36
+ success:
37
+ file_added: "Fichier ajouté avec succès"
38
+ file_removed: "Fichier supprimé avec succès"
39
+ file_uploaded: "Fichier uploadé avec succès"
40
+
41
+ confirmations:
42
+ delete_file: "Êtes-vous sûr de vouloir supprimer ce fichier ?"
43
+ replace_file: "Un fichier est déjà attaché. Voulez-vous le remplacer ?"
44
+
45
+ javascript:
46
+ alerts:
47
+ single_file_only:
48
+ title: "Un seul fichier autorisé"
49
+ text: "Vous ne pouvez uploader qu'un seul fichier pour ce champ."
50
+ upload_error:
51
+ title: "Erreur"
52
+ text: "Impossible d'uploader le fichier"
53
+ file_too_large:
54
+ title: "Trop volumineux"
55
+ text: "Max: %{max_size}MB"
56
+ empty_file:
57
+ title: "Fichier vide"
58
+ text: "Le fichier ne peut pas être vide"
59
+ invalid_format:
60
+ title: "Format invalide"
61
+ text: "Acceptés: %{accepted_types}"
62
+ suspicious_type:
63
+ title: "Type de fichier suspect"
64
+ text: "Le type réel du fichier ne correspond pas à l'extension"
65
+ attach_error:
66
+ title: "Erreur"
67
+ text: "Impossible d'attacher le fichier"
68
+ delete_error:
69
+ title: "Erreur"
70
+ text: "Impossible de supprimer le fichier"
71
+ confirms:
72
+ delete_file:
73
+ title: "Supprimer ?"
74
+ text: "Voulez-vous vraiment retirer ce fichier ?"
75
+ confirm_button: "Supprimer"
76
+ cancel_button: "Annuler"
77
+ preview:
78
+ unavailable: "Aperçu non disponible pour ce type de fichier"
79
+ download: "Télécharger le fichier"
80
+ close: "Fermer"
data/config/routes.rb ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ UploadDND::Engine.routes.draw do
4
+ # Routes pour les attachments
5
+ resources :attachments, only: [ :create, :destroy ]
6
+
7
+ # Routes pour les attachments temporaires
8
+ resources :temporary_attachments, only: [ :index, :create, :destroy ]
9
+ # Purge de tous les fichiers temporaires de la session pour un attribut
10
+ # (ex: bouton « Annuler » d'un formulaire de création)
11
+ delete "temporary_attachments", to: "temporary_attachments#purge", as: :purge_temporary_attachments
12
+ end