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,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
@@ -0,0 +1,3 @@
1
+ module UploadDND
2
+ VERSION = "0.9.1"
3
+ end
@@ -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: []