venus_media_library 0.1.0 → 0.2.0
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 +4 -4
- data/CHANGELOG.md +27 -0
- data/README.md +34 -22
- data/app/assets/javascripts/venus_media_library/venus_media_library.js +2 -0
- data/app/controllers/venus_media_library/application_controller.rb +36 -0
- data/app/controllers/venus_media_library/assets_controller.rb +20 -0
- data/app/controllers/venus_media_library/images_controller.rb +17 -13
- data/app/controllers/venus_media_library/legacy_assets_controller.rb +54 -0
- data/app/controllers/venus_media_library/pickers_controller.rb +3 -3
- data/app/helpers/venus_media_library/images_helper.rb +10 -21
- data/app/models/venus_media_library/asset.rb +8 -0
- data/app/views/venus_media_library/images/index.html.erb +3 -0
- data/app/views/venus_media_library/legacy_assets/index.html.erb +20 -0
- data/app/views/venus_media_library/pickers/_picker.html.erb +1 -0
- data/config/routes.rb +7 -0
- data/db/migrate/20260817190000_create_venus_media_library_assets.rb +10 -0
- data/lib/venus_media_library/version.rb +1 -1
- data/lib/venus_media_library.rb +9 -9
- metadata +12 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1499112ecf8687cfa0f34bd242c7443117c945d4800e4d74a5d163e33d406c42
|
|
4
|
+
data.tar.gz: ac7593b69b25cd85b6e1c029320ab776bd02e65284559f3814c69a9136ff5c95
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f4281c9f9c2b06fdab878293cf5607a181f3bc31e6db14ec5c83cf7f7e330ac5401e4e8a8855090f08367f2f29b6babf6440ed18a69e16427356b97cf66ba6a3
|
|
7
|
+
data.tar.gz: 10e093ac59f1622f0f952977aab386e92194b309caa3f669f7dc0e48b49b54b4b6bcb868d948f7a0856c16bf874a60cb250fc48cb9f9e8be81b1c1bc4ea19e60
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,33 @@ All notable changes to this project are documented here. The format is based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres
|
|
5
5
|
to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.2.0] - 2026-08-17
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Owner-scoped media records with private-by-default uploads and optional
|
|
11
|
+
community sharing.
|
|
12
|
+
- Host `current_user` and `current_user.admin?` conventions, configurable for
|
|
13
|
+
applications with a different authentication model.
|
|
14
|
+
- Authorization-aware original and thumbnail delivery routes.
|
|
15
|
+
- A tested Rails compatibility range through the 8.x series.
|
|
16
|
+
|
|
17
|
+
## [0.1.1] - 2026-08-17
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- A self-contained SQLite dummy app and test suite; contributors and CI no
|
|
21
|
+
longer need PostgreSQL.
|
|
22
|
+
- CI coverage for RuboCop, the full RSpec suite, gem building, and package
|
|
23
|
+
validation.
|
|
24
|
+
- SVG upload coverage. `image/svg+xml` remains part of the default upload
|
|
25
|
+
allowlist.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
- The dummy app and installation guide now mount the browsable library at
|
|
29
|
+
`/venus_media_library`; the engine root at that URL renders the image grid.
|
|
30
|
+
- `allowed_content_types` now strictly controls uploads instead of implicitly
|
|
31
|
+
permitting every `image/*` MIME type.
|
|
32
|
+
- JSON pagination is capped at 100 images per page.
|
|
33
|
+
|
|
7
34
|
## [0.1.0] - 2026-08-15
|
|
8
35
|
|
|
9
36
|
### Added
|
data/README.md
CHANGED
|
@@ -34,7 +34,7 @@ Active Storage must be installed in the host app (`bin/rails active_storage:inst
|
|
|
34
34
|
In the host app's `config/routes.rb`:
|
|
35
35
|
|
|
36
36
|
```ruby
|
|
37
|
-
mount VenusMediaLibrary::Engine, at: "/
|
|
37
|
+
mount VenusMediaLibrary::Engine, at: "/venus_media_library"
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
Include the picker JavaScript once in your layout (Propshaft/Sprockets):
|
|
@@ -92,14 +92,15 @@ permitting the attachment param (e.g. `params.permit(:cover)`).
|
|
|
92
92
|
|
|
93
93
|
### Endpoints
|
|
94
94
|
|
|
95
|
-
Mounted at your chosen path (examples assume `/
|
|
95
|
+
Mounted at your chosen path (examples assume `/venus_media_library`):
|
|
96
96
|
|
|
97
97
|
| Method | Path | Purpose |
|
|
98
98
|
| --- | --- | --- |
|
|
99
|
-
| `GET` | `/
|
|
100
|
-
| `GET` | `/
|
|
101
|
-
| `
|
|
102
|
-
| `
|
|
99
|
+
| `GET` | `/venus_media_library` | Browsable HTML thumbnail grid |
|
|
100
|
+
| `GET` | `/venus_media_library/images` | HTML thumbnail grid (also `.json`) |
|
|
101
|
+
| `GET` | `/venus_media_library/images.json` | `{ images: [...], page:, has_more:, total: }` |
|
|
102
|
+
| `POST` | `/venus_media_library/images` | Upload a file (param `file`); returns the image JSON |
|
|
103
|
+
| `GET` | `/venus_media_library/picker?target=<input_id>` | Turbo Frame body for the modal |
|
|
103
104
|
|
|
104
105
|
Each image payload includes `id`, `signed_id`, `filename`, `content_type`, `byte_size`, `url`, and `thumb_url`.
|
|
105
106
|
|
|
@@ -109,7 +110,7 @@ In an initializer (e.g. `config/initializers/venus_media_library.rb`):
|
|
|
109
110
|
|
|
110
111
|
```ruby
|
|
111
112
|
VenusMediaLibrary.configure do |config|
|
|
112
|
-
# Content types accepted by the uploader
|
|
113
|
+
# Content types accepted by the uploader.
|
|
113
114
|
config.allowed_content_types = %w[image/png image/jpeg image/webp image/gif image/svg+xml]
|
|
114
115
|
|
|
115
116
|
# [width, height] for the grid thumbnail variant.
|
|
@@ -122,36 +123,39 @@ VenusMediaLibrary.configure do |config|
|
|
|
122
123
|
# default service (Disk in dev, S3 in prod, etc.).
|
|
123
124
|
config.storage_service = nil
|
|
124
125
|
|
|
125
|
-
#
|
|
126
|
-
# uses rails_storage_proxy_url so images on a PRIVATE bucket in proxy mode are
|
|
127
|
-
# absolute and publicly fetchable by crawlers (e.g. for an og:image).
|
|
126
|
+
# Retained for compatibility. Library URLs are authorization-aware engine URLs.
|
|
128
127
|
config.url_type = :redirect
|
|
129
128
|
|
|
130
129
|
# Optional access gate. A proc run in the engine controller's context before
|
|
131
130
|
# every action, so it can use host helpers (current_user, redirect_to, head,
|
|
132
|
-
# main_app).
|
|
133
|
-
# upload endpoints to admins:
|
|
131
|
+
# main_app). Use it for a custom precondition before engine actions:
|
|
134
132
|
#
|
|
135
133
|
# config.authenticate_with = lambda do
|
|
136
134
|
# redirect_to main_app.root_path unless current_user&.admin?
|
|
137
135
|
# end
|
|
138
136
|
config.authenticate_with = nil
|
|
137
|
+
|
|
138
|
+
# Ownership defaults to the host application's authentication convention.
|
|
139
|
+
config.current_user = -> { current_user }
|
|
140
|
+
config.admin = ->(user) { user.admin? }
|
|
139
141
|
end
|
|
140
142
|
```
|
|
141
143
|
|
|
142
144
|
### Configuration Details
|
|
143
145
|
|
|
144
|
-
- **`allowed_content_types`** — Restricts uploads to these MIME types. The
|
|
146
|
+
- **`allowed_content_types`** — Restricts uploads to these exact MIME types. The default explicitly includes SVG (`image/svg+xml`).
|
|
145
147
|
|
|
146
148
|
- **`thumbnail_size`** — Array of `[width, height]` for grid thumbnails. Larger values give better preview quality at the cost of image processing overhead and bandwidth.
|
|
147
149
|
|
|
148
|
-
- **`per_page`** — Number of images to display per page. Smaller values suit mobile-friendly UIs; larger values reduce pagination clicks.
|
|
150
|
+
- **`per_page`** — Number of images to display per page. Smaller values suit mobile-friendly UIs; larger values reduce pagination clicks. JSON callers can request up to 100 images per page.
|
|
149
151
|
|
|
150
152
|
- **`storage_service`** — Active Storage service name (e.g. `:amazon`, `:google`). Leave `nil` to use the host app's default, making the engine truly storage-agnostic. Uploads automatically inherit the configured service.
|
|
151
153
|
|
|
152
|
-
- **`
|
|
153
|
-
|
|
154
|
-
|
|
154
|
+
- **`current_user`** — A controller-context callback that returns the signed-in host user. It defaults to `current_user`; every engine endpoint requires it to return a user.
|
|
155
|
+
|
|
156
|
+
- **`admin`** — A callback that determines whether that user can manage all media. It defaults to `->(user) { user.admin? }`.
|
|
157
|
+
|
|
158
|
+
- **`url_type`** — Retained for backward-compatible host configuration. Browsing and picker URLs are always protected engine routes, so private uploads are never exposed via an Active Storage signed URL.
|
|
155
159
|
|
|
156
160
|
- **`authenticate_with`** — A proc that gates access to the engine. Runs before every action in the engine's controller context, so you can call host helpers like `current_user`, `redirect_to`, and `head`. Return nothing to allow, or redirect/deny to block. Example:
|
|
157
161
|
```ruby
|
|
@@ -232,11 +236,11 @@ If you must support IE11, you'll need polyfills. No Turbo Drive requirement, but
|
|
|
232
236
|
|
|
233
237
|
### The Picker Flow
|
|
234
238
|
|
|
235
|
-
1. **User clicks "Choose from library"** — Opens a modal via a Turbo Frame (`GET /
|
|
239
|
+
1. **User clicks "Choose from library"** — Opens a modal via a Turbo Frame (`GET /venus_media_library/picker?target=<input_id>`).
|
|
236
240
|
2. **Modal loads the image grid** — The frame fetches the index view, listing images newest-first with pagination.
|
|
237
241
|
3. **User uploads or selects** —
|
|
238
242
|
- **Select:** Click an image tile; JavaScript writes the blob's `signed_id` and URL into the form inputs and closes the modal.
|
|
239
|
-
- **Upload:** Click the upload button; JavaScript posts the file to `POST /
|
|
243
|
+
- **Upload:** Click the upload button; JavaScript posts the file to `POST /venus_media_library/images`, attaches the new blob to the same inputs, and reloads the grid.
|
|
240
244
|
4. **Form submission** — The host app form submits with the image data, storing it as a URL column or Active Storage attachment.
|
|
241
245
|
|
|
242
246
|
### URL Signing & Storage Agnosticism
|
|
@@ -256,7 +260,7 @@ cd spec/dummy && bundle exec rspec ../
|
|
|
256
260
|
Or use the included Rakefile:
|
|
257
261
|
|
|
258
262
|
```bash
|
|
259
|
-
bundle exec rake spec
|
|
263
|
+
bundle exec rake app:spec
|
|
260
264
|
```
|
|
261
265
|
|
|
262
266
|
When testing a host app that uses Media Library, you can:
|
|
@@ -275,6 +279,14 @@ When testing a host app that uses Media Library, you can:
|
|
|
275
279
|
|
|
276
280
|
## Troubleshooting
|
|
277
281
|
|
|
282
|
+
### Ownership and community sharing
|
|
283
|
+
|
|
284
|
+
Each upload is owned by the user returned by `config.current_user` and starts private. A member can browse and download their own uploads; checking **Share with community** during upload makes that item visible to other signed-in members. Admins, as determined by `config.admin`, can browse and download every engine-managed upload.
|
|
285
|
+
|
|
286
|
+
The engine sends originals and thumbnails through its own authorization-aware routes. Do not use an Active Storage blob URL as a substitute for an engine media URL, because it bypasses the ownership check.
|
|
287
|
+
|
|
288
|
+
Blobs that existed before the engine was installed have no owner and are intentionally invisible to normal members. Admins can use **Import unowned legacy uploads** from the library to claim an image into their private library, then use the normal community-sharing control if appropriate. Do not expose legacy blobs by direct Active Storage URLs.
|
|
289
|
+
|
|
278
290
|
### Thumbnails aren't generating
|
|
279
291
|
|
|
280
292
|
**Symptom:** Gray placeholder squares instead of previews in the grid.
|
|
@@ -313,12 +325,12 @@ end
|
|
|
313
325
|
|
|
314
326
|
**Solution:** Check browser console for errors. Verify:
|
|
315
327
|
1. JavaScript is loaded: `<%= javascript_include_tag "venus_media_library/venus_media_library", defer: true %>`
|
|
316
|
-
2. The engine is mounted and accessible at your chosen path (
|
|
328
|
+
2. The engine is mounted and accessible at your chosen path (recommended: `/venus_media_library`).
|
|
317
329
|
3. No JavaScript errors in other assets are breaking the page.
|
|
318
330
|
|
|
319
331
|
## Development
|
|
320
332
|
|
|
321
|
-
The engine ships with a dummy app under `spec/dummy` (Active Storage configured with the Disk service, engine mounted at `/
|
|
333
|
+
The engine ships with a dummy app under `spec/dummy` (Active Storage configured with the Disk service, engine mounted at `/venus_media_library`).
|
|
322
334
|
|
|
323
335
|
```bash
|
|
324
336
|
bundle install
|
|
@@ -90,6 +90,8 @@
|
|
|
90
90
|
|
|
91
91
|
var form = new FormData();
|
|
92
92
|
form.append("file", file);
|
|
93
|
+
var share = document.querySelector("[data-ml-community-share]");
|
|
94
|
+
form.append("community_shared", share && share.checked ? "1" : "0");
|
|
93
95
|
|
|
94
96
|
var headers = { "Accept": "application/json" };
|
|
95
97
|
var token = csrfToken();
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
module VenusMediaLibrary
|
|
2
2
|
class ApplicationController < ActionController::Base
|
|
3
|
+
helper_method :venus_media_library_admin?
|
|
3
4
|
# Run the host-configured access gate (if any) before every engine action.
|
|
4
5
|
# The engine ships open; the host restricts the picker/upload endpoints by
|
|
5
6
|
# setting VenusMediaLibrary.configuration.authenticate_with to a proc.
|
|
6
7
|
before_action :authenticate_venus_media_library_access!
|
|
8
|
+
before_action :require_venus_media_library_user!
|
|
7
9
|
|
|
8
10
|
private
|
|
9
11
|
|
|
@@ -15,5 +17,39 @@ module VenusMediaLibrary
|
|
|
15
17
|
# host helpers (current_user, redirect_to, head, main_app, ...).
|
|
16
18
|
instance_exec(&gate)
|
|
17
19
|
end
|
|
20
|
+
|
|
21
|
+
def venus_media_library_user
|
|
22
|
+
return @venus_media_library_user if defined?(@venus_media_library_user)
|
|
23
|
+
|
|
24
|
+
@venus_media_library_user = instance_exec(&VenusMediaLibrary.configuration.current_user)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def venus_media_library_admin?
|
|
28
|
+
instance_exec(venus_media_library_user, &VenusMediaLibrary.configuration.admin)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def visible_media_assets
|
|
32
|
+
scope = VenusMediaLibrary::Asset.includes(:blob)
|
|
33
|
+
return scope if venus_media_library_admin?
|
|
34
|
+
|
|
35
|
+
scope.where(community_shared: true).or(scope.where(owner: venus_media_library_user))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def unowned_legacy_image_blobs
|
|
39
|
+
ActiveStorage::Blob.where("content_type LIKE ?", "image/%")
|
|
40
|
+
.where.not(id: VenusMediaLibrary::Asset.select(:blob_id))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def require_venus_media_library_admin!
|
|
44
|
+
return if venus_media_library_admin?
|
|
45
|
+
|
|
46
|
+
head :forbidden
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def require_venus_media_library_user!
|
|
50
|
+
return if venus_media_library_user
|
|
51
|
+
|
|
52
|
+
head :unauthorized
|
|
53
|
+
end
|
|
18
54
|
end
|
|
19
55
|
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module VenusMediaLibrary
|
|
2
|
+
class AssetsController < ApplicationController
|
|
3
|
+
def show
|
|
4
|
+
asset = visible_media_assets.find(params[:id])
|
|
5
|
+
send_data asset.blob.download, filename: asset.blob.filename.to_s,
|
|
6
|
+
type: asset.blob.content_type, disposition: "inline"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def thumbnail
|
|
10
|
+
asset = visible_media_assets.find(params[:id])
|
|
11
|
+
blob = asset.blob
|
|
12
|
+
return redirect_to asset_path(asset) unless blob.variable?
|
|
13
|
+
|
|
14
|
+
width, height = VenusMediaLibrary.configuration.thumbnail_size
|
|
15
|
+
representation = blob.variant(resize_to_limit: [ width, height ]).processed
|
|
16
|
+
send_data representation.download, filename: blob.filename.to_s,
|
|
17
|
+
type: representation.image.content_type, disposition: "inline"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -4,6 +4,8 @@ module VenusMediaLibrary
|
|
|
4
4
|
class ImagesController < ApplicationController
|
|
5
5
|
include VenusMediaLibrary::ImagesHelper
|
|
6
6
|
|
|
7
|
+
MAX_PER_PAGE = 100
|
|
8
|
+
|
|
7
9
|
# GET /images
|
|
8
10
|
# Lists image blobs, newest first, with simple offset pagination.
|
|
9
11
|
# Responds with an HTML grid or a JSON payload for the picker.
|
|
@@ -12,11 +14,11 @@ module VenusMediaLibrary
|
|
|
12
14
|
@per_page = per_page
|
|
13
15
|
offset = (@page - 1) * @per_page
|
|
14
16
|
|
|
15
|
-
scope =
|
|
17
|
+
scope = visible_media_assets
|
|
16
18
|
@total_count = scope.count
|
|
17
19
|
@blobs = scope.order(created_at: :desc).offset(offset).limit(@per_page).to_a
|
|
18
20
|
@has_more = offset + @blobs.size < @total_count
|
|
19
|
-
@images = @blobs.map { |
|
|
21
|
+
@images = @blobs.map { |asset| ml_image_payload(asset) }
|
|
20
22
|
|
|
21
23
|
respond_to do |format|
|
|
22
24
|
format.html # index.html.erb
|
|
@@ -45,31 +47,33 @@ module VenusMediaLibrary
|
|
|
45
47
|
content_type: uploaded.content_type,
|
|
46
48
|
service_name: VenusMediaLibrary.configuration.storage_service
|
|
47
49
|
)
|
|
50
|
+
asset = VenusMediaLibrary::Asset.create!(
|
|
51
|
+
blob: blob, owner: venus_media_library_user,
|
|
52
|
+
community_shared: ActiveModel::Type::Boolean.new.cast(params[:community_shared]) || false
|
|
53
|
+
)
|
|
48
54
|
|
|
49
55
|
respond_to do |format|
|
|
50
|
-
format.json { render json: ml_image_payload(
|
|
56
|
+
format.json { render json: ml_image_payload(asset), status: :created }
|
|
51
57
|
format.html { redirect_to images_path }
|
|
52
58
|
end
|
|
59
|
+
rescue ActiveRecord::RecordInvalid
|
|
60
|
+
blob&.purge
|
|
61
|
+
raise
|
|
53
62
|
end
|
|
54
63
|
|
|
55
64
|
private
|
|
56
65
|
|
|
57
|
-
def image_blobs
|
|
58
|
-
ActiveStorage::Blob.where("content_type LIKE ?", "image/%")
|
|
59
|
-
end
|
|
60
|
-
|
|
61
66
|
def per_page
|
|
62
|
-
|
|
63
|
-
value
|
|
67
|
+
requested = params[:per_page].presence&.to_i
|
|
68
|
+
value = requested&.positive? ? requested : VenusMediaLibrary.configuration.per_page.to_i
|
|
69
|
+
|
|
70
|
+
value.clamp(1, MAX_PER_PAGE)
|
|
64
71
|
end
|
|
65
72
|
|
|
66
73
|
def allowed_content_type?(content_type)
|
|
67
74
|
return false if content_type.blank?
|
|
68
75
|
|
|
69
|
-
|
|
70
|
-
return content_type.start_with?("image/") if allowed.blank?
|
|
71
|
-
|
|
72
|
-
allowed.include?(content_type) || content_type.start_with?("image/")
|
|
76
|
+
Array(VenusMediaLibrary.configuration.allowed_content_types).include?(content_type)
|
|
73
77
|
end
|
|
74
78
|
|
|
75
79
|
def respond_error(message, status)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module VenusMediaLibrary
|
|
2
|
+
class LegacyAssetsController < ApplicationController
|
|
3
|
+
include VenusMediaLibrary::ImagesHelper
|
|
4
|
+
before_action :require_venus_media_library_admin!
|
|
5
|
+
|
|
6
|
+
def index
|
|
7
|
+
@legacy_blobs = unowned_legacy_image_blobs.order(created_at: :desc)
|
|
8
|
+
|
|
9
|
+
respond_to do |format|
|
|
10
|
+
format.html
|
|
11
|
+
format.json { render json: { images: @legacy_blobs.map { |blob| legacy_image_payload(blob) } } }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def show
|
|
16
|
+
blob = unowned_legacy_image_blobs.find(params[:id])
|
|
17
|
+
send_data blob.download, filename: blob.filename.to_s, type: blob.content_type, disposition: "inline"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def thumbnail
|
|
21
|
+
blob = unowned_legacy_image_blobs.find(params[:id])
|
|
22
|
+
return redirect_to legacy_asset_path(blob) unless blob.variable?
|
|
23
|
+
|
|
24
|
+
width, height = VenusMediaLibrary.configuration.thumbnail_size
|
|
25
|
+
representation = blob.variant(resize_to_limit: [ width, height ]).processed
|
|
26
|
+
send_data representation.download, filename: blob.filename.to_s,
|
|
27
|
+
type: representation.image.content_type, disposition: "inline"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def import
|
|
31
|
+
blob = unowned_legacy_image_blobs.find(params[:id])
|
|
32
|
+
asset = VenusMediaLibrary::Asset.create!(blob: blob, owner: venus_media_library_user)
|
|
33
|
+
|
|
34
|
+
respond_to do |format|
|
|
35
|
+
format.html { redirect_to images_path, notice: "Legacy upload imported into your private library." }
|
|
36
|
+
format.json { render json: ml_image_payload(asset), status: :created }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def legacy_image_payload(blob)
|
|
43
|
+
{
|
|
44
|
+
id: blob.id,
|
|
45
|
+
filename: blob.filename.to_s,
|
|
46
|
+
content_type: blob.content_type,
|
|
47
|
+
byte_size: blob.byte_size,
|
|
48
|
+
created_at: blob.created_at,
|
|
49
|
+
url: legacy_asset_path(blob),
|
|
50
|
+
thumb_url: thumbnail_legacy_asset_path(blob)
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -7,12 +7,12 @@ module VenusMediaLibrary
|
|
|
7
7
|
def show
|
|
8
8
|
@target = params[:target].to_s
|
|
9
9
|
@page = [ params.fetch(:page, 1).to_i, 1 ].max
|
|
10
|
-
per = VenusMediaLibrary.configuration.per_page
|
|
10
|
+
per = [ VenusMediaLibrary.configuration.per_page.to_i, 1 ].max.clamp(1, ImagesController::MAX_PER_PAGE)
|
|
11
11
|
offset = (@page - 1) * per
|
|
12
12
|
|
|
13
|
-
scope =
|
|
13
|
+
scope = visible_media_assets
|
|
14
14
|
@has_more = offset + per < scope.count
|
|
15
|
-
@images = scope.order(created_at: :desc).offset(offset).limit(per).map { |
|
|
15
|
+
@images = scope.order(created_at: :desc).offset(offset).limit(per).map { |asset| ml_image_payload(asset) }
|
|
16
16
|
|
|
17
17
|
render partial: "venus_media_library/pickers/picker",
|
|
18
18
|
locals: { target: @target, images: @images, page: @page, has_more: @has_more },
|
|
@@ -3,16 +3,18 @@ module VenusMediaLibrary
|
|
|
3
3
|
# ImagesController so the HTML grid and the JSON response stay in sync.
|
|
4
4
|
module ImagesHelper
|
|
5
5
|
# A serializable description of a blob used by the JSON API and the tiles.
|
|
6
|
-
def ml_image_payload(
|
|
6
|
+
def ml_image_payload(asset)
|
|
7
|
+
blob = asset.blob
|
|
7
8
|
{
|
|
8
|
-
id:
|
|
9
|
+
id: asset.id,
|
|
9
10
|
signed_id: blob.signed_id,
|
|
10
11
|
filename: blob.filename.to_s,
|
|
11
12
|
content_type: blob.content_type,
|
|
12
13
|
byte_size: blob.byte_size,
|
|
13
14
|
created_at: blob.created_at,
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
community_shared: asset.community_shared,
|
|
16
|
+
url: ml_blob_url(asset),
|
|
17
|
+
thumb_url: ml_thumb_url(asset)
|
|
16
18
|
}
|
|
17
19
|
end
|
|
18
20
|
|
|
@@ -20,27 +22,14 @@ module VenusMediaLibrary
|
|
|
20
22
|
# host app, so they are reached through the `main_app` proxy. With
|
|
21
23
|
# url_type = :proxy the URL streams through Rails (works for private buckets
|
|
22
24
|
# and external crawlers); the default :redirect 302s to the storage URL.
|
|
23
|
-
def ml_blob_url(
|
|
24
|
-
|
|
25
|
-
main_app.rails_storage_proxy_url(blob)
|
|
26
|
-
else
|
|
27
|
-
main_app.rails_blob_url(blob)
|
|
28
|
-
end
|
|
29
|
-
rescue StandardError
|
|
30
|
-
main_app.rails_blob_path(blob)
|
|
25
|
+
def ml_blob_url(asset)
|
|
26
|
+
venus_media_library.asset_path(asset)
|
|
31
27
|
end
|
|
32
28
|
|
|
33
29
|
# Small variant used for the grid thumbnail. Falls back to the original URL
|
|
34
30
|
# when the blob can't be variated (e.g. SVG).
|
|
35
|
-
def ml_thumb_url(
|
|
36
|
-
|
|
37
|
-
w, h = VenusMediaLibrary.configuration.thumbnail_size
|
|
38
|
-
main_app.rails_representation_url(blob.variant(resize_to_limit: [ w, h ]))
|
|
39
|
-
else
|
|
40
|
-
ml_blob_url(blob)
|
|
41
|
-
end
|
|
42
|
-
rescue StandardError
|
|
43
|
-
ml_blob_url(blob)
|
|
31
|
+
def ml_thumb_url(asset)
|
|
32
|
+
venus_media_library.thumbnail_asset_path(asset)
|
|
44
33
|
end
|
|
45
34
|
end
|
|
46
35
|
end
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
<header class="ml-library__header">
|
|
3
3
|
<h1>Media Library</h1>
|
|
4
4
|
<p class="ml-library__count"><%= @total_count %> image<%= "s" unless @total_count == 1 %></p>
|
|
5
|
+
<% if venus_media_library_admin? %>
|
|
6
|
+
<p><%= link_to "Import unowned legacy uploads", legacy_assets_path, class: "ml-btn" %></p>
|
|
7
|
+
<% end %>
|
|
5
8
|
</header>
|
|
6
9
|
|
|
7
10
|
<% if @images.empty? %>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<div class="ml-library">
|
|
2
|
+
<header class="ml-library__header">
|
|
3
|
+
<h1>Unowned legacy uploads</h1>
|
|
4
|
+
<p class="ml-library__count"><%= @legacy_blobs.size %> image<%= "s" unless @legacy_blobs.size == 1 %> awaiting import</p>
|
|
5
|
+
</header>
|
|
6
|
+
|
|
7
|
+
<% if @legacy_blobs.empty? %>
|
|
8
|
+
<p class="ml-empty">No unowned legacy uploads found.</p>
|
|
9
|
+
<% else %>
|
|
10
|
+
<div class="ml-grid">
|
|
11
|
+
<% @legacy_blobs.each do |blob| %>
|
|
12
|
+
<article class="ml-tile">
|
|
13
|
+
<img src="<%= thumbnail_legacy_asset_path(blob) %>" alt="<%= blob.filename %>" loading="lazy">
|
|
14
|
+
<span class="ml-tile__name"><%= blob.filename %></span>
|
|
15
|
+
<%= button_to "Import into my private library", import_legacy_asset_path(blob), method: :post, class: "ml-btn" %>
|
|
16
|
+
</article>
|
|
17
|
+
<% end %>
|
|
18
|
+
</div>
|
|
19
|
+
<% end %>
|
|
20
|
+
</div>
|
data/config/routes.rb
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
VenusMediaLibrary::Engine.routes.draw do
|
|
2
2
|
# Image library: index lists images (HTML grid + JSON), create handles uploads.
|
|
3
3
|
resources :images, only: [ :index, :create ]
|
|
4
|
+
resources :assets, only: [ :show ] do
|
|
5
|
+
get :thumbnail, on: :member
|
|
6
|
+
end
|
|
7
|
+
resources :legacy_assets, only: [ :index, :show ] do
|
|
8
|
+
get :thumbnail, on: :member
|
|
9
|
+
post :import, on: :member
|
|
10
|
+
end
|
|
4
11
|
|
|
5
12
|
# Turbo Frame that renders the picker modal body for a given target field.
|
|
6
13
|
get "picker", to: "pickers#show", as: :picker
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
class CreateVenusMediaLibraryAssets < ActiveRecord::Migration[7.1]
|
|
2
|
+
def change
|
|
3
|
+
create_table :venus_media_library_assets do |t|
|
|
4
|
+
t.references :blob, null: false, foreign_key: { to_table: :active_storage_blobs }, index: { unique: true }
|
|
5
|
+
t.references :owner, null: false, polymorphic: true
|
|
6
|
+
t.boolean :community_shared, null: false, default: false
|
|
7
|
+
t.timestamps
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
data/lib/venus_media_library.rb
CHANGED
|
@@ -11,9 +11,7 @@ module VenusMediaLibrary
|
|
|
11
11
|
# config.storage_service = :amazon
|
|
12
12
|
# end
|
|
13
13
|
class Configuration
|
|
14
|
-
# Content types accepted by the uploader
|
|
15
|
-
# Every entry is matched literally; the index additionally shows any blob
|
|
16
|
-
# whose content_type starts with "image/".
|
|
14
|
+
# Content types accepted by the uploader. Every entry is matched literally.
|
|
17
15
|
attr_accessor :allowed_content_types
|
|
18
16
|
|
|
19
17
|
# [width, height] used for the grid thumbnail variant.
|
|
@@ -27,12 +25,8 @@ module VenusMediaLibrary
|
|
|
27
25
|
# the engine stays storage-agnostic (Disk in dev, S3 in prod, etc.).
|
|
28
26
|
attr_accessor :storage_service
|
|
29
27
|
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
# :proxy -> rails_storage_proxy_url (Rails streams the bytes)
|
|
33
|
-
# Use :proxy when the bucket is private and images must be publicly fetchable
|
|
34
|
-
# by external crawlers (e.g. an og:image on a private S3 bucket in proxy mode).
|
|
35
|
-
# Still storage-agnostic: both are Active Storage route helpers.
|
|
28
|
+
# Retained for compatibility with older host configuration. Library URLs are
|
|
29
|
+
# always served through authorization-aware engine routes.
|
|
36
30
|
attr_accessor :url_type
|
|
37
31
|
|
|
38
32
|
# Optional access gate. A proc run in the engine controller's context before
|
|
@@ -44,6 +38,10 @@ module VenusMediaLibrary
|
|
|
44
38
|
# c.authenticate_with = -> { redirect_to main_app.root_path unless current_user&.admin? }
|
|
45
39
|
# end
|
|
46
40
|
attr_accessor :authenticate_with
|
|
41
|
+
# The current signed-in host user and the role predicate. Both callbacks run
|
|
42
|
+
# in the engine controller context. By default this uses the host's
|
|
43
|
+
# `current_user` and `current_user.admin?` convention.
|
|
44
|
+
attr_accessor :current_user, :admin
|
|
47
45
|
|
|
48
46
|
def initialize
|
|
49
47
|
@allowed_content_types = %w[image/png image/jpeg image/jpg image/gif image/webp image/svg+xml]
|
|
@@ -52,6 +50,8 @@ module VenusMediaLibrary
|
|
|
52
50
|
@storage_service = nil
|
|
53
51
|
@url_type = :redirect
|
|
54
52
|
@authenticate_with = nil
|
|
53
|
+
@current_user = -> { current_user }
|
|
54
|
+
@admin = ->(user) { user.admin? }
|
|
55
55
|
end
|
|
56
56
|
end
|
|
57
57
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: venus_media_library
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Good Works On Earth
|
|
@@ -16,6 +16,9 @@ dependencies:
|
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
18
|
version: '7.1'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '9.0'
|
|
19
22
|
type: :runtime
|
|
20
23
|
prerelease: false
|
|
21
24
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -23,6 +26,9 @@ dependencies:
|
|
|
23
26
|
- - ">="
|
|
24
27
|
- !ruby/object:Gem::Version
|
|
25
28
|
version: '7.1'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '9.0'
|
|
26
32
|
- !ruby/object:Gem::Dependency
|
|
27
33
|
name: image_processing
|
|
28
34
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -57,7 +63,9 @@ files:
|
|
|
57
63
|
- app/assets/stylesheets/venus_media_library/application.css
|
|
58
64
|
- app/assets/stylesheets/venus_media_library/picker.css
|
|
59
65
|
- app/controllers/venus_media_library/application_controller.rb
|
|
66
|
+
- app/controllers/venus_media_library/assets_controller.rb
|
|
60
67
|
- app/controllers/venus_media_library/images_controller.rb
|
|
68
|
+
- app/controllers/venus_media_library/legacy_assets_controller.rb
|
|
61
69
|
- app/controllers/venus_media_library/pickers_controller.rb
|
|
62
70
|
- app/helpers/venus_media_library/application_helper.rb
|
|
63
71
|
- app/helpers/venus_media_library/images_helper.rb
|
|
@@ -65,12 +73,15 @@ files:
|
|
|
65
73
|
- app/jobs/venus_media_library/application_job.rb
|
|
66
74
|
- app/mailers/venus_media_library/application_mailer.rb
|
|
67
75
|
- app/models/venus_media_library/application_record.rb
|
|
76
|
+
- app/models/venus_media_library/asset.rb
|
|
68
77
|
- app/views/layouts/venus_media_library/application.html.erb
|
|
69
78
|
- app/views/venus_media_library/images/_image.html.erb
|
|
70
79
|
- app/views/venus_media_library/images/index.html.erb
|
|
80
|
+
- app/views/venus_media_library/legacy_assets/index.html.erb
|
|
71
81
|
- app/views/venus_media_library/pickers/_modal.html.erb
|
|
72
82
|
- app/views/venus_media_library/pickers/_picker.html.erb
|
|
73
83
|
- config/routes.rb
|
|
84
|
+
- db/migrate/20260817190000_create_venus_media_library_assets.rb
|
|
74
85
|
- lib/tasks/venus_media_library_tasks.rake
|
|
75
86
|
- lib/venus_media_library.rb
|
|
76
87
|
- lib/venus_media_library/engine.rb
|