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,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UploadDND
|
|
4
|
+
# Blobs sans rattachement qu'on peut supprimer : envois DirectUpload ratés ou
|
|
5
|
+
# abandonnés (refus, coupure réseau, onglet fermé, image retirée d'un éditeur).
|
|
6
|
+
# Le service ne supprime jamais : il LISTE, chaque blob accompagné de ses
|
|
7
|
+
# citations. C'est l'appelant (job, tâche rake) qui décide de purger.
|
|
8
|
+
#
|
|
9
|
+
# Un blob est supprimable seulement si, cumulativement :
|
|
10
|
+
# - aucun ActiveStorage::Attachment (record, variante, Action Text) ;
|
|
11
|
+
# - créé depuis plus de min_age ;
|
|
12
|
+
# - non gardé par un UploadDND::TemporaryAttachment (son blob est rattaché au
|
|
13
|
+
# temporaire, donc déjà exclu par where.missing(:attachments)) ;
|
|
14
|
+
# - non cité dans un texte de la base (colonnes texte ET JSON) : par la clé du
|
|
15
|
+
# bucket, par le signed_id (brut et échappé), ou par sgid (fichier inséré
|
|
16
|
+
# avec l'éditeur dans un HTML stocké hors Action Text). Jamais par le nom.
|
|
17
|
+
#
|
|
18
|
+
# Le scan des citations ne lit que les textes contenant une adresse de
|
|
19
|
+
# stockage ou un sgid (filtre SQL LIKE), une seule fois pour tous les
|
|
20
|
+
# candidats. PostgreSQL requis (CAST AS text + ILIKE).
|
|
21
|
+
class OrphanBlobs
|
|
22
|
+
Result = Data.define(:blob, :citations) do
|
|
23
|
+
def purgeable? = citations.empty?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Tables sans texte de contenu, ou dont les adresses ne comptent pas.
|
|
27
|
+
DEFAULT_EXCLUDED_TABLES = %w[
|
|
28
|
+
active_storage_blobs active_storage_attachments active_storage_variant_records
|
|
29
|
+
upload_d_n_d_temporary_attachments
|
|
30
|
+
schema_migrations ar_internal_metadata
|
|
31
|
+
].freeze
|
|
32
|
+
|
|
33
|
+
# Seuls les textes contenant une adresse Active Storage ou un fichier inséré
|
|
34
|
+
# (sgid) sont lus. L'app ajoute l'hôte de son bucket via orphan_scan_patterns
|
|
35
|
+
# pour couvrir les citations par URL directe (image collée).
|
|
36
|
+
DEFAULT_PATTERNS = %w[%active_storage% %sgid%].freeze
|
|
37
|
+
|
|
38
|
+
TEXT_TYPES = %i[string text json jsonb].freeze
|
|
39
|
+
|
|
40
|
+
def self.call(**) = new(**).call
|
|
41
|
+
|
|
42
|
+
def initialize(now: Time.current, min_age: nil)
|
|
43
|
+
@now = now
|
|
44
|
+
@min_age = min_age || UploadDND.configuration.orphan_blob_min_age
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def call
|
|
48
|
+
ensure_postgresql!
|
|
49
|
+
candidates.map { |blob| Result.new(blob: blob, citations: citations_for(blob)) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def ensure_postgresql!
|
|
55
|
+
adapter = connection.adapter_name.to_s.downcase
|
|
56
|
+
return if adapter.include?("postgresql") || adapter.include?("postgis")
|
|
57
|
+
|
|
58
|
+
raise UploadDND::Error,
|
|
59
|
+
"UploadDND::OrphanBlobs : le scan des citations nécessite PostgreSQL " \
|
|
60
|
+
"(adaptateur courant : #{connection.adapter_name}). " \
|
|
61
|
+
"Sans ce scan, un blob cité dans un texte serait supprimé à tort."
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def candidates
|
|
65
|
+
ActiveStorage::Blob
|
|
66
|
+
.where.missing(:attachments)
|
|
67
|
+
.where(created_at: ...(@now - @min_age))
|
|
68
|
+
.order(:id)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Une adresse de fichier contient la clé (bucket) ou le signed_id
|
|
72
|
+
# (Active Storage, stable par défaut).
|
|
73
|
+
def citations_for(blob)
|
|
74
|
+
tokens = [ blob.key, blob.signed_id, CGI.escape(blob.signed_id) ].uniq
|
|
75
|
+
by_url = texts_citing_storage.filter_map do |source, text|
|
|
76
|
+
source if tokens.any? { |token| text.include?(token) }
|
|
77
|
+
end
|
|
78
|
+
(by_url + embedded_blob_sources.fetch(blob.id, [])).uniq
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# { blob_id => ["table.colonne#id", ...] } pour les fichiers insérés (sgid).
|
|
82
|
+
def embedded_blob_sources
|
|
83
|
+
@embedded_blob_sources ||= compute_embedded_blob_sources
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def compute_embedded_blob_sources
|
|
87
|
+
return {} unless defined?(ActionText)
|
|
88
|
+
|
|
89
|
+
texts_citing_storage.each_with_object(Hash.new { |hash, key| hash[key] = [] }) do |(source, text), sources|
|
|
90
|
+
next unless text.include?("sgid")
|
|
91
|
+
|
|
92
|
+
ActionText::Content.new(text).attachables.grep(ActiveStorage::Blob).each do |blob|
|
|
93
|
+
sources[blob.id] << source
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# [["table.colonne#id", texte], ...], lus une seule fois.
|
|
99
|
+
def texts_citing_storage
|
|
100
|
+
@texts_citing_storage ||= scanned_columns.flat_map do |table_name, column, has_id|
|
|
101
|
+
table = Arel::Table.new(table_name)
|
|
102
|
+
as_text = Arel::Nodes::NamedFunction.new("CAST", [ table[column].as("text") ])
|
|
103
|
+
query = table.project(has_id ? table[:id] : Arel.sql("NULL"), as_text)
|
|
104
|
+
.where(patterns.map { |pattern| as_text.matches(pattern) }.reduce(:or))
|
|
105
|
+
connection.select_rows(query.to_sql).map do |id, text|
|
|
106
|
+
[ "#{table_name}.#{column}#{"##{id}" if id}", text.to_s ]
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Colonnes à scanner : la liste blanche de l'app si elle est renseignée,
|
|
112
|
+
# sinon toutes les colonnes texte/JSON des tables non exclues.
|
|
113
|
+
def scanned_columns
|
|
114
|
+
whitelist = UploadDND.configuration.orphan_scan_columns
|
|
115
|
+
whitelist.present? ? whitelisted_columns(whitelist) : schema_columns
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def whitelisted_columns(entries)
|
|
119
|
+
Array(entries).filter_map do |entry|
|
|
120
|
+
table, column = entry.to_s.split(".", 2)
|
|
121
|
+
next if table.blank? || column.blank?
|
|
122
|
+
next unless connection.data_source_exists?(table)
|
|
123
|
+
|
|
124
|
+
[ table, column, has_id_column?(table) ]
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def schema_columns
|
|
129
|
+
(connection.tables - excluded_tables).flat_map do |table|
|
|
130
|
+
columns = connection.columns(table)
|
|
131
|
+
has_id = columns.any? { |column| column.name == "id" }
|
|
132
|
+
columns.select { |column| TEXT_TYPES.include?(column.type) }
|
|
133
|
+
.map { |column| [ table, column.name, has_id ] }
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def has_id_column?(table)
|
|
138
|
+
connection.columns(table).any? { |column| column.name == "id" }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def excluded_tables
|
|
142
|
+
DEFAULT_EXCLUDED_TABLES + Array(UploadDND.configuration.orphan_scan_excluded_tables).map(&:to_s)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def patterns
|
|
146
|
+
(DEFAULT_PATTERNS + Array(UploadDND.configuration.orphan_scan_patterns)).uniq
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def connection = ActiveRecord::Base.connection
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UploadDND
|
|
4
|
+
# Blob envoyé par DirectUpload puis refusé par le serveur (contraintes,
|
|
5
|
+
# validation du modèle, erreur à l'enregistrement).
|
|
6
|
+
#
|
|
7
|
+
# Avec l'envoi direct, le fichier est déjà dans le bucket quand le serveur
|
|
8
|
+
# le contrôle : sans purge, chaque refus (et chaque nouvel essai) laisse un
|
|
9
|
+
# blob orphelin.
|
|
10
|
+
#
|
|
11
|
+
# Règle : on ne supprime jamais un fichier qui appartient à quelqu'un. Le
|
|
12
|
+
# signed_id vient du navigateur, il peut désigner un blob existant :
|
|
13
|
+
# - un blob qui a un attachment (record, ou TemporaryAttachment) est intact ;
|
|
14
|
+
# - un blob ancien est intact, même sans attachment : il peut être cité dans
|
|
15
|
+
# un HTML stocké (sgid, URL) — seul un envoi récent est purgé.
|
|
16
|
+
module RejectedBlob
|
|
17
|
+
# Programme la purge du blob s'il est purgeable. La purge effective
|
|
18
|
+
# revérifie l'absence d'attachment (cf. PurgeRejectedBlobJob).
|
|
19
|
+
def self.discard(blob)
|
|
20
|
+
return unless discardable?(blob)
|
|
21
|
+
|
|
22
|
+
Rails.logger.info "UploadDND: rejected blob #{blob.id} scheduled for purge"
|
|
23
|
+
UploadDND::PurgeRejectedBlobJob.perform_later(blob.id)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.discardable?(blob)
|
|
27
|
+
blob.is_a?(ActiveStorage::Blob) &&
|
|
28
|
+
blob.persisted? &&
|
|
29
|
+
recent?(blob) &&
|
|
30
|
+
unattached?(blob)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.unattached?(blob)
|
|
34
|
+
!blob.attachments.exists?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.recent?(blob)
|
|
38
|
+
window = UploadDND.configuration.rejected_blob_purge_window
|
|
39
|
+
window.present? && blob.created_at >= window.ago
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UploadDND
|
|
4
|
+
# Identifiant de session utilisé pour scoper les fichiers temporaires.
|
|
5
|
+
#
|
|
6
|
+
# Sur une session vierge (aucun cookie), `session.id` est nil tant que rien
|
|
7
|
+
# n'a été écrit en session : le premier upload échouerait (session_id vide)
|
|
8
|
+
# et l'id lu au rendu du formulaire ne correspondrait pas à celui des uploads.
|
|
9
|
+
#
|
|
10
|
+
# `SessionId.for` amorce la session en y écrivant une clé neutre, ce qui force
|
|
11
|
+
# l'émission d'un id et du cookie dès la réponse courante, quel que soit le store.
|
|
12
|
+
# L'id est ainsi stable entre le rendu du composant et les requêtes suivantes.
|
|
13
|
+
module SessionId
|
|
14
|
+
KEY = "upload_d_n_d.session"
|
|
15
|
+
|
|
16
|
+
def self.for(session)
|
|
17
|
+
return "" if session.nil?
|
|
18
|
+
|
|
19
|
+
session[KEY] ||= true if session.respond_to?(:[]=)
|
|
20
|
+
|
|
21
|
+
sid = session.respond_to?(:id) ? session.id : (session[:id] || session["id"])
|
|
22
|
+
sid.to_s
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UploadDND
|
|
4
|
+
# Concern modèle pour déclarer des attachments "uploadables" et transférer
|
|
5
|
+
# automatiquement les fichiers temporaires lors de la création d'un record.
|
|
6
|
+
#
|
|
7
|
+
# Remarque: ce module existait historiquement comme template de générateur.
|
|
8
|
+
# Il est maintenant livré par la gem pour éviter un prérequis implicite
|
|
9
|
+
# (et coller au README).
|
|
10
|
+
module Uploadable
|
|
11
|
+
extend ActiveSupport::Concern
|
|
12
|
+
|
|
13
|
+
included do
|
|
14
|
+
# Callbacks pour attacher les fichiers temporaires après création
|
|
15
|
+
after_create :attach_temporary_uploads
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Stocke temporairement le session_id pour l'upload
|
|
19
|
+
# Utilise une variable d'instance privée plutôt qu'attr_accessor
|
|
20
|
+
def temp_upload_session_id=(value)
|
|
21
|
+
@_temp_upload_session_id = value
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def temp_upload_session_id
|
|
25
|
+
@_temp_upload_session_id
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class_methods do
|
|
29
|
+
# Déclare un champ uploadable avec validation
|
|
30
|
+
#
|
|
31
|
+
# @param attribute_name [Symbol] le nom de l'attribut (ex: :avatar, :documents)
|
|
32
|
+
# @param options [Hash] les options de validation
|
|
33
|
+
# @option options [Array<String>] :content_type les types MIME autorisés
|
|
34
|
+
# @option options [Integer] :max_size la taille max en bytes
|
|
35
|
+
# @option options [Integer] :max_files le nombre max de fichiers (pour has_many)
|
|
36
|
+
# @option options [Boolean] :multiple autoriser plusieurs fichiers (auto-détecté depuis has_one/has_many par défaut)
|
|
37
|
+
# @option options [String] :message message d'erreur personnalisé
|
|
38
|
+
# @option options [Boolean] :strict_mime_check active la détection MIME côté serveur (anti-spoofing, plus coûteux)
|
|
39
|
+
# @option options [String] :spoofing_message message d'erreur personnalisé pour les erreurs de spoofing
|
|
40
|
+
def uploadable(attribute_name, **options)
|
|
41
|
+
# Résoudre accept: en content_type: (symboles → MIME strings)
|
|
42
|
+
if options[:accept]
|
|
43
|
+
options[:content_type] = UploadDND::MimeTypes.resolve(options.delete(:accept))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Auto-déclarer has_one_attached / has_many_attached si pas déjà fait
|
|
47
|
+
unless reflect_on_attachment(attribute_name)
|
|
48
|
+
if options[:multiple]
|
|
49
|
+
has_many_attached attribute_name
|
|
50
|
+
else
|
|
51
|
+
has_one_attached attribute_name
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
validates attribute_name,
|
|
56
|
+
file: options.slice(:content_type, :max_size, :max_files, :message, :strict_mime_check, :spoofing_message)
|
|
57
|
+
|
|
58
|
+
# Stocke les options pour usage ultérieur (ViewComponent, helpers, etc.)
|
|
59
|
+
(@uploadable_attributes ||= {})[attribute_name] = options
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def uploadable_attributes
|
|
63
|
+
@uploadable_attributes || {}
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def attach_temporary_uploads
|
|
70
|
+
return unless temp_upload_session_id.present?
|
|
71
|
+
return unless UploadDND::TemporaryAttachment.exists?(session_id: temp_upload_session_id)
|
|
72
|
+
|
|
73
|
+
UploadDND::TemporaryAttachment.where(session_id: temp_upload_session_id).find_each do |temp_attachment|
|
|
74
|
+
attribute_name = temp_attachment.attribute_name
|
|
75
|
+
|
|
76
|
+
# SÉCURITÉ : attribute_name vient du client — seul un attachment déclaré
|
|
77
|
+
# sur le modèle est résolu. Détection serveur du type (has_one vs has_many).
|
|
78
|
+
attachment = UploadDND::AttachmentLookup.find(self, attribute_name)
|
|
79
|
+
unless attachment
|
|
80
|
+
# Si l'attribut n'est pas un attachment du record, on nettoie quand même
|
|
81
|
+
# pour éviter de laisser des fichiers orphelins en base.
|
|
82
|
+
temp_attachment.destroy
|
|
83
|
+
next
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
blob_attachment = temp_attachment.blob_attachment
|
|
87
|
+
if blob_attachment.nil? || temp_attachment.violates_constraints_of?(self, attribute_name)
|
|
88
|
+
temp_attachment.destroy
|
|
89
|
+
next
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
if attachment.is_a?(ActiveStorage::Attached::Many)
|
|
93
|
+
# has_many_attached: on "déplace" l'attachment (pas de purge du blob)
|
|
94
|
+
blob_attachment.update!(record: self, name: attribute_name)
|
|
95
|
+
elsif attachment.is_a?(ActiveStorage::Attached::One)
|
|
96
|
+
# has_one_attached: utiliser = (remplace automatiquement)
|
|
97
|
+
# Ne transférer que le PREMIER fichier temporaire si plusieurs existent
|
|
98
|
+
if attachment.attached?
|
|
99
|
+
# Le record a déjà un fichier: on purgera le temporaire.
|
|
100
|
+
temp_attachment.destroy
|
|
101
|
+
next
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
blob_attachment.update!(record: self, name: attribute_name)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Après déplacement, temp_attachment n'a plus d'attachment ActiveStorage.
|
|
108
|
+
# On reload pour éviter les caches d'association avant destroy.
|
|
109
|
+
temp_attachment.reload
|
|
110
|
+
temp_attachment.destroy
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Nettoie le session_id après utilisation
|
|
114
|
+
@_temp_upload_session_id = nil
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
data/lib/upload_d_n_d.rb
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "upload_d_n_d/version"
|
|
4
|
+
require "upload_d_n_d/engine"
|
|
5
|
+
require "upload_d_n_d/configuration"
|
|
6
|
+
require "upload_d_n_d/session_id"
|
|
7
|
+
require "upload_d_n_d/attachment_lookup"
|
|
8
|
+
require "upload_d_n_d/rejected_blob"
|
|
9
|
+
require "upload_d_n_d/orphan_blobs"
|
|
10
|
+
require "upload_d_n_d/file_validator"
|
|
11
|
+
require "upload_d_n_d/mime_types"
|
|
12
|
+
require "upload_d_n_d/uploadable"
|
|
13
|
+
require "upload_d_n_d/controllers/uploadable"
|
|
14
|
+
|
|
15
|
+
module UploadDND
|
|
16
|
+
class Error < StandardError; end
|
|
17
|
+
|
|
18
|
+
# Levée quand un record affiché dans la liste de fichiers n'a pas de route
|
|
19
|
+
# polymorphique (resource imbriquée, singulière, ou non routée).
|
|
20
|
+
# Remède côté app : resolve() dans config/routes.rb, ou surcharge du helper
|
|
21
|
+
# udnd_remove_file_url. Voir README, section "Records imbriqués".
|
|
22
|
+
class UnroutableRecordError < Error; end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
require "upload_d_n_d/authorization"
|
|
26
|
+
require "upload_d_n_d/field_constraints"
|
|
27
|
+
require "upload_d_n_d/field_reference"
|
|
28
|
+
|
|
29
|
+
module UploadDND
|
|
30
|
+
# Autoload des concerns, helpers et autres composants
|
|
31
|
+
# Ces fichiers seront chargés automatiquement par le moteur Rails
|
|
32
|
+
|
|
33
|
+
# Helpers publics pour les routes de la gem
|
|
34
|
+
class << self
|
|
35
|
+
# Retourne le path pour créer un attachement
|
|
36
|
+
# @return [String] le chemin vers la création d'attachement
|
|
37
|
+
def attachments_path
|
|
38
|
+
Engine.routes.url_helpers.attachments_path
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Retourne le path pour un attachement spécifique
|
|
42
|
+
# @param id [Integer, String] l'ID de l'attachement
|
|
43
|
+
# @param options [Hash] options supplémentaires (ex: record_gid)
|
|
44
|
+
# @return [String] le chemin vers l'attachement
|
|
45
|
+
def attachment_path(id, **options)
|
|
46
|
+
Engine.routes.url_helpers.attachment_path(id, **options)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Retourne le path pour créer un attachement temporaire
|
|
50
|
+
# @return [String] le chemin vers la création d'attachement temporaire
|
|
51
|
+
def temporary_attachments_path
|
|
52
|
+
Engine.routes.url_helpers.temporary_attachments_path
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Retourne le path pour un attachement temporaire spécifique
|
|
56
|
+
# @param id [Integer, String] l'ID de l'attachement temporaire
|
|
57
|
+
# @return [String] le chemin vers l'attachement temporaire
|
|
58
|
+
def temporary_attachment_path(id)
|
|
59
|
+
Engine.routes.url_helpers.temporary_attachment_path(id)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: upload_d_n_d
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.9.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Pierre Cherbero
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rails
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 7.2.3
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '8.2'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: 7.2.3
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '8.2'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: view_component
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '3.0'
|
|
39
|
+
type: :runtime
|
|
40
|
+
prerelease: false
|
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - ">="
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '3.0'
|
|
46
|
+
description: UploadDND provides a drag & drop upload interface for Rails apps, built
|
|
47
|
+
on ActiveStorage, Stimulus and ViewComponent. It validates files server-side (MIME
|
|
48
|
+
sniffing, size, count), supports temporary uploads for new records, and authorizes
|
|
49
|
+
every request fail-closed through Pundit or CanCanCan.
|
|
50
|
+
email:
|
|
51
|
+
- cameleon24@outlook.fr
|
|
52
|
+
executables: []
|
|
53
|
+
extensions: []
|
|
54
|
+
extra_rdoc_files: []
|
|
55
|
+
files:
|
|
56
|
+
- MIT-LICENSE
|
|
57
|
+
- README.md
|
|
58
|
+
- Rakefile
|
|
59
|
+
- app/assets/config/upload_d_n_d_manifest.js
|
|
60
|
+
- app/assets/stylesheets/upload_d_n_d/application.css
|
|
61
|
+
- app/components/upload_d_n_d/upload_component.html.erb
|
|
62
|
+
- app/components/upload_d_n_d/upload_component.rb
|
|
63
|
+
- app/controllers/concerns/upload_d_n_d/rate_limiting.rb
|
|
64
|
+
- app/controllers/upload_d_n_d/attachments_controller.rb
|
|
65
|
+
- app/controllers/upload_d_n_d/base_attachments_controller.rb
|
|
66
|
+
- app/controllers/upload_d_n_d/temporary_attachments_controller.rb
|
|
67
|
+
- app/helpers/upload_d_n_d/application_helper.rb
|
|
68
|
+
- app/helpers/upload_d_n_d/controllers/helpers.rb
|
|
69
|
+
- app/javascript/upload_d_n_d/controllers/preview_controller.js
|
|
70
|
+
- app/javascript/upload_d_n_d/controllers/uploads_controller.js
|
|
71
|
+
- app/javascript/upload_d_n_d/index.js
|
|
72
|
+
- app/jobs/upload_d_n_d/application_job.rb
|
|
73
|
+
- app/jobs/upload_d_n_d/cleanup_temporary_attachments_job.rb
|
|
74
|
+
- app/jobs/upload_d_n_d/purge_orphan_blobs_job.rb
|
|
75
|
+
- app/jobs/upload_d_n_d/purge_rejected_blob_job.rb
|
|
76
|
+
- app/models/upload_d_n_d/application_record.rb
|
|
77
|
+
- app/models/upload_d_n_d/temporary_attachment.rb
|
|
78
|
+
- app/views/upload_d_n_d/shared/_file_icon.html.erb
|
|
79
|
+
- app/views/upload_d_n_d/shared/_files_list.html.erb
|
|
80
|
+
- app/views/upload_d_n_d/shared/_temp_files_list.html.erb
|
|
81
|
+
- config/importmap.rb
|
|
82
|
+
- config/initializers/inflections.rb
|
|
83
|
+
- config/locales/upload_d_n_d.en.yml
|
|
84
|
+
- config/locales/upload_d_n_d.fr.yml
|
|
85
|
+
- config/routes.rb
|
|
86
|
+
- docs/CHANGELOG.md
|
|
87
|
+
- lib/generators/upload_d_n_d/POLICY_README
|
|
88
|
+
- lib/generators/upload_d_n_d/USAGE
|
|
89
|
+
- lib/generators/upload_d_n_d/install_generator.rb
|
|
90
|
+
- lib/generators/upload_d_n_d/mime_type_helper.rb
|
|
91
|
+
- lib/generators/upload_d_n_d/model_generator.rb
|
|
92
|
+
- lib/generators/upload_d_n_d/policy_generator.rb
|
|
93
|
+
- lib/generators/upload_d_n_d/templates/POST_INSTALL
|
|
94
|
+
- lib/generators/upload_d_n_d/templates/concerns/controllers_uploadable.rb
|
|
95
|
+
- lib/generators/upload_d_n_d/templates/concerns/upload_authorization.rb
|
|
96
|
+
- lib/generators/upload_d_n_d/templates/concerns/uploadable.rb
|
|
97
|
+
- lib/generators/upload_d_n_d/templates/controller.rb.tt
|
|
98
|
+
- lib/generators/upload_d_n_d/templates/helpers/upload_helper.rb
|
|
99
|
+
- lib/generators/upload_d_n_d/templates/initializers/upload_d_n_d.rb
|
|
100
|
+
- lib/generators/upload_d_n_d/templates/migrations/create_upload_d_n_d_temporary_attachments.rb
|
|
101
|
+
- lib/generators/upload_d_n_d/templates/policies/upload_d_n_d_attachment_policy.rb
|
|
102
|
+
- lib/generators/upload_d_n_d/templates/policies/upload_d_n_d_temporary_attachment_policy.rb
|
|
103
|
+
- lib/generators/upload_d_n_d/templates/validators/file_validator.rb
|
|
104
|
+
- lib/tasks/upload_d_n_d_storage.rake
|
|
105
|
+
- lib/tasks/upload_d_n_d_tasks.rake
|
|
106
|
+
- lib/upload_d_n_d.rb
|
|
107
|
+
- lib/upload_d_n_d/attachment_lookup.rb
|
|
108
|
+
- lib/upload_d_n_d/authorization.rb
|
|
109
|
+
- lib/upload_d_n_d/authorization/can_can_can_adapter.rb
|
|
110
|
+
- lib/upload_d_n_d/authorization/no_authorization.rb
|
|
111
|
+
- lib/upload_d_n_d/authorization/pundit_adapter.rb
|
|
112
|
+
- lib/upload_d_n_d/authorization/unconfigured.rb
|
|
113
|
+
- lib/upload_d_n_d/configuration.rb
|
|
114
|
+
- lib/upload_d_n_d/controllers/uploadable.rb
|
|
115
|
+
- lib/upload_d_n_d/engine.rb
|
|
116
|
+
- lib/upload_d_n_d/field_constraints.rb
|
|
117
|
+
- lib/upload_d_n_d/field_reference.rb
|
|
118
|
+
- lib/upload_d_n_d/file_validator.rb
|
|
119
|
+
- lib/upload_d_n_d/mime_types.rb
|
|
120
|
+
- lib/upload_d_n_d/orphan_blobs.rb
|
|
121
|
+
- lib/upload_d_n_d/rejected_blob.rb
|
|
122
|
+
- lib/upload_d_n_d/session_id.rb
|
|
123
|
+
- lib/upload_d_n_d/uploadable.rb
|
|
124
|
+
- lib/upload_d_n_d/version.rb
|
|
125
|
+
homepage: https://github.com/Peyochanchan/upload_d_n_d
|
|
126
|
+
licenses:
|
|
127
|
+
- MIT
|
|
128
|
+
metadata:
|
|
129
|
+
source_code_uri: https://github.com/Peyochanchan/upload_d_n_d
|
|
130
|
+
changelog_uri: https://github.com/Peyochanchan/upload_d_n_d/blob/main/docs/CHANGELOG.md
|
|
131
|
+
bug_tracker_uri: https://github.com/Peyochanchan/upload_d_n_d/issues
|
|
132
|
+
rubygems_mfa_required: 'true'
|
|
133
|
+
rdoc_options: []
|
|
134
|
+
require_paths:
|
|
135
|
+
- lib
|
|
136
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
137
|
+
requirements:
|
|
138
|
+
- - ">="
|
|
139
|
+
- !ruby/object:Gem::Version
|
|
140
|
+
version: '3.2'
|
|
141
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
142
|
+
requirements:
|
|
143
|
+
- - ">="
|
|
144
|
+
- !ruby/object:Gem::Version
|
|
145
|
+
version: '0'
|
|
146
|
+
requirements: []
|
|
147
|
+
rubygems_version: 4.0.5
|
|
148
|
+
specification_version: 4
|
|
149
|
+
summary: Secure drag & drop file uploads for Rails, built on ActiveStorage
|
|
150
|
+
test_files: []
|