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.
- 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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 89eeed42abead646dab758d0391d98faeb9e2387f5e716a0fd9004e36448fa99
|
|
4
|
+
data.tar.gz: 349ff62605b1ae15fe685bd28882a07a3a459a5e5d47e0c34581eb4aef01e46c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: bcb751f061b219670240f8ffcbe0ebd1a7b21034668b299d335b2511dfd67f570554ce6a66e4ad918e4a11e2ee53e7e5744f7c53e8578e5f604a86d274286665
|
|
7
|
+
data.tar.gz: b86e190d252c90ebc9a99622bd20d81b05957d37fea6e6c42d521a8036e9c787bf11ab9645e59ac98c45bed8393a024f7cff5efbfa4fa310650e25fc1862ac07
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright (c) 2025-2026 Pierre Cherbero
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# UploadDND
|
|
2
|
+
|
|
3
|
+
[](https://github.com/Peyochanchan/upload_d_n_d/actions/workflows/ci.yml)
|
|
4
|
+
[](MIT-LICENSE)
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
🇫🇷 [Version française](README.fr.md)
|
|
9
|
+
|
|
10
|
+
Secure drag & drop file uploads for Rails, built on ActiveStorage, Stimulus and ViewComponent.
|
|
11
|
+
|
|
12
|
+
One line in a view gives you a drop zone with progress, previews and removal — while every file is checked server-side against the limits declared on the model, and every request goes through your authorization layer, fail-closed.
|
|
13
|
+
|
|
14
|
+
```erb
|
|
15
|
+
<%= upload_field :documents, record: @project %>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Why
|
|
19
|
+
|
|
20
|
+
ActiveStorage handles storage; it does not handle the hard parts of a production upload flow:
|
|
21
|
+
|
|
22
|
+
- **Trusting the browser.** `accept` attributes and JS size checks are UX, not security. UploadDND re-applies the model's limits on the server as soon as the file lands, and optionally sniffs the real content type (Marcel) to catch spoofed extensions.
|
|
23
|
+
- **Uploading before the record exists.** Creation forms need files before `save`. Temporary uploads are scoped to the session, survive a page reload, and are re-validated when wired to the record.
|
|
24
|
+
- **Authorization gaps.** Upload endpoints are easy to leave open. Here every upload and deletion goes through Pundit or CanCanCan; with no usable adapter, everything is refused (403). Disabling it (`:none`) is explicit and reported at boot.
|
|
25
|
+
- **Storage garbage.** DirectUpload puts the file in your bucket before anyone validates it. Rejected blobs are purged in the background, and an opt-in job removes orphan blobs — without touching files still referenced in stored HTML.
|
|
26
|
+
|
|
27
|
+
## Features
|
|
28
|
+
|
|
29
|
+
- Drag & drop with progress bar, image and PDF previews
|
|
30
|
+
- `has_one_attached` and `has_many_attached`, with `max_files`
|
|
31
|
+
- Server-side validation: allowlisted MIME types, size, count, optional anti-spoofing
|
|
32
|
+
- Temporary uploads for creation forms, persisted across reloads
|
|
33
|
+
- Fail-closed authorization (Pundit, CanCanCan, or explicit opt-out)
|
|
34
|
+
- One file, one owner: an already-attached `signed_id` can't be re-attached elsewhere
|
|
35
|
+
- Rate limiting on the engine endpoints
|
|
36
|
+
- Cleanup jobs for expired temporary files, rejected and orphan blobs
|
|
37
|
+
- Works in any namespace (`Admin::`, `Manager::`…), no routes required in the host app
|
|
38
|
+
- Generators for install, models and Pundit policies
|
|
39
|
+
- I18n (EN/FR) server messages
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
# Gemfile
|
|
45
|
+
gem "upload_d_n_d", github: "Peyochanchan/upload_d_n_d"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
bundle install
|
|
50
|
+
bin/rails generate upload_d_n_d:install
|
|
51
|
+
bin/rails db:migrate
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The installer adds `view_component` and `stimulus-rails` if missing, creates the temporary uploads migration, mounts the engine, writes the initializer and, if Pundit is present, the policies.
|
|
55
|
+
|
|
56
|
+
Everything else (JS, CSS, components, concerns) is served from the gem: upgrading is a `bundle update`.
|
|
57
|
+
|
|
58
|
+
ActiveStorage must be installed (`bin/rails active_storage:install`). With an external S3-like bucket and a strict CSP, serve files same-origin:
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
config.active_storage.resolve_model_to_route = :rails_storage_proxy
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Usage
|
|
65
|
+
|
|
66
|
+
### Declare the field on the model
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
class Project < ApplicationRecord
|
|
70
|
+
include UploadDND::Uploadable
|
|
71
|
+
|
|
72
|
+
uploadable :cover, # declares has_one_attached
|
|
73
|
+
accept: [:jpg, :png, :webp],
|
|
74
|
+
max_size: 5.megabytes,
|
|
75
|
+
strict_mime_check: true
|
|
76
|
+
|
|
77
|
+
uploadable :documents, # declares has_many_attached
|
|
78
|
+
accept: [:pdf, :docx],
|
|
79
|
+
max_size: 20.megabytes,
|
|
80
|
+
multiple: true,
|
|
81
|
+
max_files: 10
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The model is the source of truth: the same limits drive the browser checks and the server checks. Options passed to the view helper can only narrow them.
|
|
86
|
+
|
|
87
|
+
`strict_mime_check: true` sniffs the file's real bytes (Marcel) to catch a spoofed extension. It works both ways: with the gem's dropzone (DirectUpload — the content is read back from storage, where the file already sits) and with a plain Rails `file_field` (the file rides along with the form — the content is read from the pending upload, before it reaches storage). Either way the check runs during model validation, so a spoofed file is never persisted.
|
|
88
|
+
|
|
89
|
+
Or let the generator print the snippets for you:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
bin/rails g upload_d_n_d:model Project --attribute=documents --multiple --content_type=pdf docx --max_size=20
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Use it in a form
|
|
96
|
+
|
|
97
|
+
```erb
|
|
98
|
+
<%= form_with model: @project do |f| %>
|
|
99
|
+
<%= upload_session_field %> <%# only needed for new records %>
|
|
100
|
+
|
|
101
|
+
<%= f.text_field :name %>
|
|
102
|
+
<%= upload_field :cover, record: @project %>
|
|
103
|
+
<%= upload_field :documents, record: @project %>
|
|
104
|
+
|
|
105
|
+
<%= f.submit %>
|
|
106
|
+
<% end %>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
For a new record, files are stored temporarily and attached on save:
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
class ProjectsController < ApplicationController
|
|
113
|
+
include UploadDND::Controllers::Uploadable
|
|
114
|
+
|
|
115
|
+
def create
|
|
116
|
+
@project = Project.new(project_params)
|
|
117
|
+
wire_temporary_uploads(@project) # checks the session, re-validates each file
|
|
118
|
+
|
|
119
|
+
if @project.save
|
|
120
|
+
redirect_to @project
|
|
121
|
+
else
|
|
122
|
+
render :new, status: :unprocessable_entity
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Authorize
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
# Pundit — uploads and deletions on a record require update?
|
|
132
|
+
class ProjectPolicy < ApplicationPolicy
|
|
133
|
+
def update?
|
|
134
|
+
record.members.include?(user)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
# CanCanCan
|
|
141
|
+
can :create, :upload_d_n_d_attachment
|
|
142
|
+
can [:create, :destroy], :upload_d_n_d_temporary_attachment
|
|
143
|
+
can :update, Project, members: { id: user.id }
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`bin/rails g upload_d_n_d:policy` generates the gem's generic Pundit policies. The full rule matrix is in [docs/AUTHORIZATION.md](docs/AUTHORIZATION.md).
|
|
147
|
+
|
|
148
|
+
## Security model
|
|
149
|
+
|
|
150
|
+
| Layer | What it does |
|
|
151
|
+
|---|---|
|
|
152
|
+
| Content-type allowlist | Only declared types are accepted |
|
|
153
|
+
| Anti-spoofing | `strict_mime_check: true` inspects the actual bytes |
|
|
154
|
+
| Server-side limits | Size, type and count enforced on upload, again when temporary files are wired |
|
|
155
|
+
| Fail-closed authorization | No adapter → 403; `:none` must be explicit |
|
|
156
|
+
| Session isolation | Temporary files only visible to the session that uploaded them |
|
|
157
|
+
| Ownership | An attached blob can't be claimed by another record |
|
|
158
|
+
| Rate limiting | Per user + IP on the engine endpoints (`Rails.cache` backed) |
|
|
159
|
+
| Rejected blob purge | Background purge, never for attached blobs or unauthorized requests |
|
|
160
|
+
|
|
161
|
+
ActiveStorage's own `POST /rails/active_storage/direct_uploads` belongs to your app, not the gem: throttle it too, for example with [rack-attack](https://github.com/rack/rack-attack).
|
|
162
|
+
|
|
163
|
+
## Maintenance jobs
|
|
164
|
+
|
|
165
|
+
```yaml
|
|
166
|
+
# config/recurring.yml (solid_queue)
|
|
167
|
+
production:
|
|
168
|
+
cleanup_temporary_uploads:
|
|
169
|
+
class: UploadDND::CleanupTemporaryAttachmentsJob
|
|
170
|
+
schedule: every day at 2am
|
|
171
|
+
purge_orphan_blobs:
|
|
172
|
+
class: UploadDND::PurgeOrphanBlobsJob
|
|
173
|
+
args: [{ "apply": true }]
|
|
174
|
+
schedule: every sunday at 3am
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The orphan purge is opt-in and conservative: a blob is removed only if it has no attachment, is older than `orphan_blob_min_age`, and isn't referenced anywhere in the database's text and JSON columns (bucket key, `signed_id` or Action Text sgid). Dry-run first:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
bin/rails upload_d_n_d:purge_orphans # report only
|
|
181
|
+
APPLY=1 bin/rails upload_d_n_d:purge_orphans # delete
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
PostgreSQL is required for the reference scan; on other databases the job raises instead of deleting blindly.
|
|
185
|
+
|
|
186
|
+
## Configuration
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
# config/initializers/upload_d_n_d.rb
|
|
190
|
+
UploadDND.configure do |config|
|
|
191
|
+
config.max_size = 10.megabytes
|
|
192
|
+
config.accepted_types = ".pdf,.jpg,.jpeg,.png"
|
|
193
|
+
config.main_color = "#E97451"
|
|
194
|
+
config.temporary_files_retention = 7 # days
|
|
195
|
+
|
|
196
|
+
# :pundit / :cancancan are auto-detected; :none disables authorization
|
|
197
|
+
# config.authorization_adapter = :pundit
|
|
198
|
+
|
|
199
|
+
config.rate_limit_enabled = true
|
|
200
|
+
config.rate_limit_max_requests = 30 # one request per file
|
|
201
|
+
config.rate_limit_period = 60
|
|
202
|
+
|
|
203
|
+
config.on_upload_success = ->(blob, user) { AuditLog.record(:upload, blob, user) }
|
|
204
|
+
config.on_file_removed = ->(record, attribute_name, user) { AuditLog.record(:removal, record, user) }
|
|
205
|
+
end
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
The French README covers every option in detail: nested and singular resources, custom delete routes, cancelling temporary uploads, styling.
|
|
209
|
+
|
|
210
|
+
## Development
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
bundle install
|
|
214
|
+
bundle exec rspec
|
|
215
|
+
bin/rubocop
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
The suite runs against a dummy app on PostgreSQL. CI runs RuboCop and the spec suite.
|
|
219
|
+
|
|
220
|
+
## Docs
|
|
221
|
+
|
|
222
|
+
- [Authorization](docs/AUTHORIZATION.md)
|
|
223
|
+
- [Generator examples](docs/GENERATOR_EXAMPLES.md)
|
|
224
|
+
- [Migrating to 0.5](docs/MIGRATION_0.5.md)
|
|
225
|
+
- [Changelog](docs/CHANGELOG.md)
|
|
226
|
+
|
|
227
|
+
## License
|
|
228
|
+
|
|
229
|
+
Released under the [MIT License](MIT-LICENSE).
|
data/Rakefile
ADDED