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,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,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>
|
data/config/importmap.rb
ADDED
|
@@ -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
|