upload_d_n_d 0.9.2
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.
- 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 +292 -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 +90 -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,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UploadDND
|
|
4
|
+
module UploadAuthorization
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
before_action :authorize_upload!, only: [ :create ]
|
|
9
|
+
before_action :authorize_destroy!, only: [ :destroy ]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
# Vérifie si l'utilisateur peut uploader sur cet attribut
|
|
15
|
+
# Peut être surchargée dans votre controller
|
|
16
|
+
def authorize_upload!
|
|
17
|
+
# Si vous utilisez Pundit
|
|
18
|
+
if defined?(Pundit)
|
|
19
|
+
authorize_with_pundit
|
|
20
|
+
# Si vous utilisez CanCanCan
|
|
21
|
+
elsif defined?(CanCan)
|
|
22
|
+
authorize_with_cancan
|
|
23
|
+
else
|
|
24
|
+
# Autorisation par défaut : l'utilisateur doit être connecté
|
|
25
|
+
authorize_basic_upload!
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Vérifie si l'utilisateur peut supprimer ce fichier
|
|
30
|
+
def authorize_destroy!
|
|
31
|
+
if defined?(Pundit)
|
|
32
|
+
authorize @temp_attachment || @attachment
|
|
33
|
+
elsif defined?(CanCan)
|
|
34
|
+
authorize! :destroy, @temp_attachment || @attachment
|
|
35
|
+
else
|
|
36
|
+
authorize_basic_destroy!
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Autorisation basique : utilisateur connecté
|
|
41
|
+
def authorize_basic_upload!
|
|
42
|
+
return if current_user.present?
|
|
43
|
+
|
|
44
|
+
render json: { error: "Vous devez être connecté pour uploader des fichiers" },
|
|
45
|
+
status: :unauthorized
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def authorize_basic_destroy!
|
|
49
|
+
# Vérifie que le fichier appartient à la session ou à l'utilisateur
|
|
50
|
+
return if owner_matches_current_user?
|
|
51
|
+
|
|
52
|
+
render json: { error: "Vous ne pouvez pas supprimer ce fichier" },
|
|
53
|
+
status: :forbidden
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def authorize_with_pundit
|
|
57
|
+
record = find_or_build_record
|
|
58
|
+
authorize record, :update?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def authorize_with_cancan
|
|
62
|
+
record = find_or_build_record
|
|
63
|
+
authorize! :update, record
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def owner_matches_current_user?
|
|
67
|
+
if @temp_attachment
|
|
68
|
+
@temp_attachment.session_id == UploadDND::SessionId.for(session) ||
|
|
69
|
+
@temp_attachment.user_id == current_user&.id
|
|
70
|
+
elsif @attachment
|
|
71
|
+
@attachment.record.user_id == current_user&.id if @attachment.record.respond_to?(:user_id)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def find_or_build_record
|
|
76
|
+
if params[:record_gid].present?
|
|
77
|
+
GlobalID::Locator.locate(params[:record_gid])
|
|
78
|
+
else
|
|
79
|
+
# Pour les uploads temporaires, on ne peut pas vraiment autoriser
|
|
80
|
+
# car le record n'existe pas encore
|
|
81
|
+
nil
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def current_user
|
|
86
|
+
# À adapter selon votre système d'authentification
|
|
87
|
+
# Par défaut, on suppose que vous utilisez Devise ou similaire
|
|
88
|
+
respond_to?(:current_user, true) ? super : nil
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UploadDND
|
|
4
|
+
module Uploadable
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
# Callbacks pour attacher les fichiers temporaires après création
|
|
9
|
+
after_create :attach_temporary_uploads
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Stocke temporairement le session_id pour l'upload
|
|
13
|
+
# Utilise une variable d'instance privée plutôt qu'attr_accessor
|
|
14
|
+
def temp_upload_session_id=(value)
|
|
15
|
+
@_temp_upload_session_id = value
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def temp_upload_session_id
|
|
19
|
+
@_temp_upload_session_id
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class_methods do
|
|
23
|
+
# Déclare un champ uploadable avec validation
|
|
24
|
+
#
|
|
25
|
+
# @param attribute_name [Symbol] le nom de l'attribut (ex: :avatar, :documents)
|
|
26
|
+
# @param options [Hash] les options de validation
|
|
27
|
+
# @option options [Array<Symbol>] :accept extensions acceptées (ex: [:jpg, :png, :pdf])
|
|
28
|
+
# @option options [Array<String>] :content_type types MIME autorisés (alternative à :accept)
|
|
29
|
+
# @option options [Integer] :max_size la taille max en bytes
|
|
30
|
+
# @option options [Integer] :max_files le nombre max de fichiers (pour has_many)
|
|
31
|
+
# @option options [Boolean] :multiple autoriser plusieurs fichiers (déclare has_many_attached)
|
|
32
|
+
# @option options [String] :message message d'erreur personnalisé
|
|
33
|
+
# @option options [Boolean] :strict_mime_check active la détection MIME côté serveur (anti-spoofing)
|
|
34
|
+
# @option options [String] :spoofing_message message d'erreur personnalisé pour les erreurs de spoofing
|
|
35
|
+
#
|
|
36
|
+
# @example Image unique (has_one_attached déclaré automatiquement)
|
|
37
|
+
# class User < ApplicationRecord
|
|
38
|
+
# include UploadDND::Uploadable
|
|
39
|
+
#
|
|
40
|
+
# uploadable :avatar,
|
|
41
|
+
# accept: [:jpg, :png],
|
|
42
|
+
# max_size: 5.megabytes,
|
|
43
|
+
# strict_mime_check: true
|
|
44
|
+
# end
|
|
45
|
+
#
|
|
46
|
+
# @example Documents multiples (has_many_attached déclaré automatiquement)
|
|
47
|
+
# class Project < ApplicationRecord
|
|
48
|
+
# include UploadDND::Uploadable
|
|
49
|
+
#
|
|
50
|
+
# uploadable :documents,
|
|
51
|
+
# accept: [:pdf, :doc, :docx],
|
|
52
|
+
# max_size: 10.megabytes,
|
|
53
|
+
# multiple: true,
|
|
54
|
+
# max_files: 5,
|
|
55
|
+
# strict_mime_check: true
|
|
56
|
+
# end
|
|
57
|
+
def uploadable(attribute_name, **options)
|
|
58
|
+
validates attribute_name,
|
|
59
|
+
file: options.slice(:content_type, :max_size, :max_files, :message, :strict_mime_check, :spoofing_message)
|
|
60
|
+
|
|
61
|
+
# Stocke les options pour usage ultérieur
|
|
62
|
+
(@uploadable_attributes ||= {})[attribute_name] = options
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def uploadable_attributes
|
|
66
|
+
@uploadable_attributes || {}
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def attach_temporary_uploads
|
|
73
|
+
return unless temp_upload_session_id.present?
|
|
74
|
+
return unless UploadDND::TemporaryAttachment.exists?(session_id: temp_upload_session_id)
|
|
75
|
+
|
|
76
|
+
UploadDND::TemporaryAttachment.where(session_id: temp_upload_session_id).find_each do |temp_attachment|
|
|
77
|
+
attribute_name = temp_attachment.attribute_name
|
|
78
|
+
next unless respond_to?(attribute_name)
|
|
79
|
+
|
|
80
|
+
# SÉCURITÉ : Détection serveur du type d'attachment (has_one vs has_many)
|
|
81
|
+
attachment = send(attribute_name)
|
|
82
|
+
|
|
83
|
+
if attachment.is_a?(ActiveStorage::Attached::Many)
|
|
84
|
+
# has_many_attached: ajouter à la collection
|
|
85
|
+
attachment.attach(temp_attachment.blob.blob)
|
|
86
|
+
elsif attachment.is_a?(ActiveStorage::Attached::One)
|
|
87
|
+
# has_one_attached: utiliser = (remplace automatiquement)
|
|
88
|
+
# Ne transférer que le PREMIER fichier temporaire si plusieurs existent
|
|
89
|
+
unless attachment.attached?
|
|
90
|
+
send("#{attribute_name}=", temp_attachment.blob.blob)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
temp_attachment.destroy
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Nettoie le session_id après utilisation
|
|
98
|
+
@_temp_upload_session_id = nil
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
<% module_namespacing do -%>
|
|
4
|
+
class <%= plural_name.camelize %>Controller < ApplicationController
|
|
5
|
+
include UploadDND::Controllers::Uploadable
|
|
6
|
+
|
|
7
|
+
before_action :set_<%= singular_name %>, only: [:show, :edit, :update, :destroy, :attach_file, :remove_file]
|
|
8
|
+
|
|
9
|
+
def index
|
|
10
|
+
@<%= plural_name %> = <%= class_name %>.all
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def show
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def new
|
|
17
|
+
@<%= singular_name %> = <%= class_name %>.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def edit
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def create
|
|
24
|
+
@<%= singular_name %> = <%= class_name %>.new(<%= singular_name %>_params)
|
|
25
|
+
|
|
26
|
+
# Lie les uploads temporaires (session) au record
|
|
27
|
+
wire_temporary_uploads(@<%= singular_name %>)
|
|
28
|
+
|
|
29
|
+
if @<%= singular_name %>.save
|
|
30
|
+
redirect_to @<%= singular_name %>, notice: '<%= class_name %> créé avec succès'
|
|
31
|
+
else
|
|
32
|
+
render :new, status: :unprocessable_entity
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def update
|
|
37
|
+
if @<%= singular_name %>.update(<%= singular_name %>_params)
|
|
38
|
+
redirect_to @<%= singular_name %>, notice: '<%= class_name %> mis à jour avec succès'
|
|
39
|
+
else
|
|
40
|
+
render :edit, status: :unprocessable_entity
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def destroy
|
|
45
|
+
@<%= singular_name %>.destroy
|
|
46
|
+
redirect_to <%= plural_name %>_url, notice: '<%= class_name %> supprimé avec succès'
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def set_<%= singular_name %>
|
|
52
|
+
@<%= singular_name %> = <%= class_name %>.find(params[:id])
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def <%= singular_name %>_params
|
|
56
|
+
params.require(:<%= singular_name %>).permit(:name) # Ajoutez vos attributs ici
|
|
57
|
+
# Note: les fichiers uploadés ne sont PAS dans strong params (gérés par UploadDND)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
<% end -%>
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UploadDND
|
|
4
|
+
module UploadHelper
|
|
5
|
+
# Rendu du composant d'upload
|
|
6
|
+
#
|
|
7
|
+
# @param attribute_name [Symbol] le nom de l'attribut (ex: :avatar, :documents)
|
|
8
|
+
# @param record [ApplicationRecord, nil] le record à attacher (nil pour upload temporaire)
|
|
9
|
+
# @param options [Hash] options supplémentaires
|
|
10
|
+
# @option options [Boolean] :multiple (false) autoriser plusieurs fichiers
|
|
11
|
+
# @option options [String] :accepted_types types de fichiers acceptés (ex: '.jpg,.png')
|
|
12
|
+
# @option options [Integer] :max_size taille max en bytes
|
|
13
|
+
# @option options [String] :label label personnalisé
|
|
14
|
+
# @option options [Hash] :validation_options options passées au FileValidator
|
|
15
|
+
#
|
|
16
|
+
# @example Upload simple
|
|
17
|
+
# <%= upload_field :avatar, record: @user %>
|
|
18
|
+
#
|
|
19
|
+
# @example Upload multiple avec validation
|
|
20
|
+
# <%= upload_field :documents,
|
|
21
|
+
# record: @project,
|
|
22
|
+
# multiple: true,
|
|
23
|
+
# accepted_types: '.pdf,.docx',
|
|
24
|
+
# max_size: 10.megabytes %>
|
|
25
|
+
#
|
|
26
|
+
# @example Upload temporaire (record pas encore créé)
|
|
27
|
+
# <%= upload_field :logo, accepted_types: '.png,.jpg' %>
|
|
28
|
+
#
|
|
29
|
+
def upload_field(attribute_name, record: nil, **options)
|
|
30
|
+
render UploadDND::UploadComponent.new(
|
|
31
|
+
attribute_name: attribute_name,
|
|
32
|
+
record: record,
|
|
33
|
+
user: current_user_for_upload,
|
|
34
|
+
**options
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Génère un champ caché avec le session_id pour les uploads temporaires.
|
|
39
|
+
# À placer dans les formulaires qui créent un nouveau record avec upload.
|
|
40
|
+
def upload_session_field
|
|
41
|
+
tag.input(type: "hidden", name: "temp_upload_session_id", value: UploadDND::SessionId.for(session))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Affiche la liste des fichiers uploadés
|
|
45
|
+
#
|
|
46
|
+
# @param attribute_name [Symbol] le nom de l'attribut
|
|
47
|
+
# @param record [ApplicationRecord] le record
|
|
48
|
+
#
|
|
49
|
+
def uploaded_files_list(attribute_name, record:)
|
|
50
|
+
return unless record&.respond_to?(attribute_name)
|
|
51
|
+
|
|
52
|
+
attachments = record.send(attribute_name)
|
|
53
|
+
return unless attachments.attached?
|
|
54
|
+
|
|
55
|
+
files = attachments.respond_to?(:each) ? attachments : [ attachments ]
|
|
56
|
+
|
|
57
|
+
render partial: "upload_d_n_d/shared/files_list",
|
|
58
|
+
locals: { files: files, attribute_name: attribute_name }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Récupère l'utilisateur courant pour l'upload
|
|
62
|
+
# Peut être surchargé dans votre application
|
|
63
|
+
def current_user_for_upload
|
|
64
|
+
respond_to?(:current_user, true) ? current_user : nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Icône selon le type de fichier
|
|
68
|
+
def file_icon_class(filename)
|
|
69
|
+
extension = File.extname(filename).downcase
|
|
70
|
+
|
|
71
|
+
case extension
|
|
72
|
+
when ".pdf"
|
|
73
|
+
"fa-file-pdf"
|
|
74
|
+
when ".doc", ".docx"
|
|
75
|
+
"fa-file-word"
|
|
76
|
+
when ".xls", ".xlsx"
|
|
77
|
+
"fa-file-excel"
|
|
78
|
+
when ".jpg", ".jpeg", ".png", ".gif", ".svg"
|
|
79
|
+
"fa-file-image"
|
|
80
|
+
when ".zip", ".rar", ".7z"
|
|
81
|
+
"fa-file-archive"
|
|
82
|
+
when ".mp4", ".avi", ".mov"
|
|
83
|
+
"fa-file-video"
|
|
84
|
+
else
|
|
85
|
+
"fa-file"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Badge de couleur selon le type
|
|
90
|
+
def file_type_badge_class(content_type)
|
|
91
|
+
case content_type
|
|
92
|
+
when /^image\//
|
|
93
|
+
"badge-success"
|
|
94
|
+
when /^application\/pdf/
|
|
95
|
+
"badge-danger"
|
|
96
|
+
when /^application\/.*word/
|
|
97
|
+
"badge-primary"
|
|
98
|
+
when /^application\/.*excel/, /^application\/.*spreadsheet/
|
|
99
|
+
"badge-success"
|
|
100
|
+
else
|
|
101
|
+
"badge-secondary"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Configuration de UploadDND
|
|
4
|
+
UploadDND.configure do |config|
|
|
5
|
+
# Taille maximale par défaut des fichiers (en bytes)
|
|
6
|
+
# Peut être surchargé au niveau du composant
|
|
7
|
+
config.max_size = 10.megabytes
|
|
8
|
+
|
|
9
|
+
# Types de fichiers acceptés par défaut
|
|
10
|
+
# Peut être surchargé au niveau du composant
|
|
11
|
+
config.accepted_types = ".pdf,.doc,.docx,.jpg,.jpeg,.png,.gif"
|
|
12
|
+
|
|
13
|
+
# Vérification stricte du type MIME (anti-spoofing)
|
|
14
|
+
# Analyse le contenu réel du fichier via Marcel (plus coûteux)
|
|
15
|
+
# config.strict_mime_check = false
|
|
16
|
+
|
|
17
|
+
# Couleur principale du thème (utilisée pour les boutons, progress bar, etc.)
|
|
18
|
+
config.main_color = "#E97451"
|
|
19
|
+
|
|
20
|
+
# Autoriser les uploads sans authentification (déconseillé en production)
|
|
21
|
+
# config.allow_anonymous_uploads = false
|
|
22
|
+
|
|
23
|
+
# Durée de conservation des fichiers temporaires (en jours)
|
|
24
|
+
# Les fichiers temporaires non attachés seront supprimés après ce délai
|
|
25
|
+
# config.temporary_files_retention = 7
|
|
26
|
+
|
|
27
|
+
# Un fichier refusé par le serveur (taille, type, validation...) est purgé
|
|
28
|
+
# s'il a été envoyé dans cette fenêtre et n'est rattaché à rien.
|
|
29
|
+
# nil désactive la purge des fichiers refusés.
|
|
30
|
+
# config.rejected_blob_purge_window = 1.hour
|
|
31
|
+
|
|
32
|
+
# Nettoyage des blobs orphelins (opt-in : UploadDND::PurgeOrphanBlobsJob ou
|
|
33
|
+
# la tâche rake upload_d_n_d:purge_orphans). PostgreSQL requis.
|
|
34
|
+
# config.orphan_blob_min_age = 2.days
|
|
35
|
+
# Limite le scan des citations aux colonnes de texte riche (borne la mémoire).
|
|
36
|
+
# Sans liste, toutes les colonnes texte/JSON sont scannées.
|
|
37
|
+
# config.orphan_scan_columns = ["projects.description", "pages.body_translations"]
|
|
38
|
+
# Motifs d'adresse supplémentaires (hôte/bucket du service de stockage).
|
|
39
|
+
# config.orphan_scan_patterns = ["%votre-bucket.example.com%"]
|
|
40
|
+
# config.orphan_scan_excluded_tables = []
|
|
41
|
+
|
|
42
|
+
# Autorisation : :pundit ou :cancancan (détection automatique), ou :none
|
|
43
|
+
# pour s'en passer. Sans adaptateur utilisable, tout est refusé (403).
|
|
44
|
+
# config.authorization_adapter = :pundit # ou :cancancan, ou :none
|
|
45
|
+
|
|
46
|
+
# Validateur personnalisé
|
|
47
|
+
# Par défaut, utilise FileValidator si disponible
|
|
48
|
+
# config.validator_class = 'FileValidator'
|
|
49
|
+
|
|
50
|
+
# Configuration du stockage ActiveStorage (optionnel)
|
|
51
|
+
# Exemple: forcer l'utilisation d'un service spécifique
|
|
52
|
+
# config.active_storage_service = :amazon
|
|
53
|
+
|
|
54
|
+
# Callbacks personnalisés
|
|
55
|
+
# config.on_upload_success = ->(blob, user) {
|
|
56
|
+
# # Logger ou notifier
|
|
57
|
+
# Rails.logger.info "File uploaded: #{blob.filename} by #{user&.email}"
|
|
58
|
+
# }
|
|
59
|
+
#
|
|
60
|
+
# config.on_upload_error = ->(error, user) {
|
|
61
|
+
# # Logger ou notifier
|
|
62
|
+
# Rails.logger.error "Upload failed: #{error.message} for #{user&.email}"
|
|
63
|
+
# }
|
|
64
|
+
|
|
65
|
+
# Rate limiting (compteur basé sur Rails.cache ; aucune gem requise)
|
|
66
|
+
# config.rate_limit_enabled = true
|
|
67
|
+
# config.rate_limit_max_requests = 10
|
|
68
|
+
# config.rate_limit_period = 1.minute
|
|
69
|
+
end
|
data/lib/generators/upload_d_n_d/templates/migrations/create_upload_d_n_d_temporary_attachments.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateUploadDNDTemporaryAttachments < ActiveRecord::Migration[7.2]
|
|
4
|
+
def change
|
|
5
|
+
create_table :upload_d_n_d_temporary_attachments do |t|
|
|
6
|
+
t.string :session_id, null: false
|
|
7
|
+
t.string :attribute_name, null: false
|
|
8
|
+
t.integer :user_id
|
|
9
|
+
|
|
10
|
+
t.timestamps
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
add_index :upload_d_n_d_temporary_attachments, :session_id
|
|
14
|
+
add_index :upload_d_n_d_temporary_attachments, :user_id
|
|
15
|
+
add_index :upload_d_n_d_temporary_attachments, [ :session_id, :attribute_name ],
|
|
16
|
+
name: "index_temp_attachments_on_session_and_attribute"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Policy Pundit pour l'upload de fichiers permanents avec UploadDND
|
|
4
|
+
#
|
|
5
|
+
# Cette policy contrôle qui peut attacher et supprimer des fichiers
|
|
6
|
+
# sur des records existants (via le moteur UploadDND).
|
|
7
|
+
#
|
|
8
|
+
# Le controller du moteur autorise via `authorize :upload_d_n_d_attachment`
|
|
9
|
+
# (record symbolique), ce qui amène Pundit à chercher cette classe.
|
|
10
|
+
#
|
|
11
|
+
class UploadDNDAttachmentPolicy < ApplicationPolicy
|
|
12
|
+
# Détermine qui peut attacher un fichier à un record existant
|
|
13
|
+
#
|
|
14
|
+
# Par défaut : tous les utilisateurs authentifiés
|
|
15
|
+
# Exemples de personnalisation :
|
|
16
|
+
# - Seulement les admins : user&.admin?
|
|
17
|
+
# - Seulement certains rôles : user&.admin? || user&.editor?
|
|
18
|
+
#
|
|
19
|
+
def create?
|
|
20
|
+
# PERSONNALISEZ CETTE MÉTHODE selon vos besoins
|
|
21
|
+
user.present?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Détermine qui peut modifier les fichiers d'un record
|
|
25
|
+
#
|
|
26
|
+
def update?
|
|
27
|
+
# PERSONNALISEZ CETTE MÉTHODE selon vos besoins
|
|
28
|
+
user.present?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Détermine qui peut supprimer un fichier d'un record
|
|
32
|
+
#
|
|
33
|
+
def destroy?
|
|
34
|
+
# PERSONNALISEZ CETTE MÉTHODE selon vos besoins
|
|
35
|
+
user.present?
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/generators/upload_d_n_d/templates/policies/upload_d_n_d_temporary_attachment_policy.rb
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Policy Pundit pour l'upload de fichiers temporaires avec UploadDND
|
|
4
|
+
#
|
|
5
|
+
# Cette policy contrôle qui peut uploader et supprimer des fichiers temporaires.
|
|
6
|
+
# Personnalisez les méthodes ci-dessous selon vos besoins d'autorisation.
|
|
7
|
+
#
|
|
8
|
+
class UploadDNDTemporaryAttachmentPolicy < ApplicationPolicy
|
|
9
|
+
# Détermine qui peut créer (uploader) un fichier temporaire
|
|
10
|
+
#
|
|
11
|
+
# Par défaut : tous les utilisateurs authentifiés
|
|
12
|
+
# Exemples de personnalisation :
|
|
13
|
+
# - Seulement les admins : user&.admin?
|
|
14
|
+
# - Seulement certains rôles : user&.admin? || user&.editor?
|
|
15
|
+
# - Selon un attribut : user&.can_upload?
|
|
16
|
+
#
|
|
17
|
+
def create?
|
|
18
|
+
# PERSONNALISEZ CETTE MÉTHODE selon vos besoins
|
|
19
|
+
user.present?
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Détermine qui peut supprimer un fichier temporaire
|
|
23
|
+
#
|
|
24
|
+
# Par défaut : tous les utilisateurs authentifiés
|
|
25
|
+
# La vérification de propriété (session) est déjà faite dans le controller
|
|
26
|
+
#
|
|
27
|
+
def destroy?
|
|
28
|
+
# PERSONNALISEZ CETTE MÉTHODE selon vos besoins
|
|
29
|
+
user.present?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Détermine qui peut nettoyer les fichiers temporaires expirés
|
|
33
|
+
# (utilisé par les tâches cron/jobs)
|
|
34
|
+
#
|
|
35
|
+
def cleanup?
|
|
36
|
+
# PERSONNALISEZ CETTE MÉTHODE selon vos besoins
|
|
37
|
+
user&.admin?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Exemple d'autorisation plus stricte (commenté par défaut)
|
|
41
|
+
# Décommentez et adaptez selon vos besoins
|
|
42
|
+
#
|
|
43
|
+
# def create?
|
|
44
|
+
# return false unless user.present?
|
|
45
|
+
#
|
|
46
|
+
# # Seulement les admins et éditeurs
|
|
47
|
+
# user.admin? || user.editor?
|
|
48
|
+
# end
|
|
49
|
+
#
|
|
50
|
+
# def destroy?
|
|
51
|
+
# return false unless user.present?
|
|
52
|
+
#
|
|
53
|
+
# # Seulement les admins, éditeurs, ou l'équipe régionale
|
|
54
|
+
# user.admin? || user.editor? || user.team&.regional_team?
|
|
55
|
+
# end
|
|
56
|
+
end
|