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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8bc0532620557b83e5d9441d9d7d3c8b6908e07de77b902387156212c5f34dbb
4
- data.tar.gz: e55f2d5bd5698921d1725718c5fa3c744a3ee365c78fa17a2fcde0886e195bb8
3
+ metadata.gz: 1499112ecf8687cfa0f34bd242c7443117c945d4800e4d74a5d163e33d406c42
4
+ data.tar.gz: ac7593b69b25cd85b6e1c029320ab776bd02e65284559f3814c69a9136ff5c95
5
5
  SHA512:
6
- metadata.gz: 8a5b72c1b7b0ba5efbaacfc3e6f2e3fc98e2bfbf1248d2c102c171231b9967e2ce37bf39696a5fcdf596129fc0623c19bbf240292f5c6f5f682956218b99edcc
7
- data.tar.gz: bd469adfdf85aec05d9dd5dd6a69cb5188b5c1354ded20a85e040133c6f3bad3cb2aeaed0722c55ac75f4e5b5f29454b52269c05684ab6fd46dd40d7f3141e9c
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: "/media"
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 `/media`):
95
+ Mounted at your chosen path (examples assume `/venus_media_library`):
96
96
 
97
97
  | Method | Path | Purpose |
98
98
  | --- | --- | --- |
99
- | `GET` | `/media/images` | HTML thumbnail grid (also `.json`) |
100
- | `GET` | `/media/images.json` | `{ images: [...], page:, has_more:, total: }` |
101
- | `POST` | `/media/images` | Upload a file (param `file`); returns the image JSON |
102
- | `GET` | `/media/picker?target=<input_id>` | Turbo Frame body for the modal |
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 (any image/* is always allowed in the grid).
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
- # How image URLs are built. :redirect (default) uses rails_blob_url; :proxy
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). Left nil the engine is open set it to restrict the picker and
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 grid always shows any blob with `image/*` content type regardless. Defaults to common image formats; customize only if you need to block certain types.
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
- - **`url_type`** — Controls how image URLs are built:
153
- - `:redirect` (default) — `rails_blob_url` returns a temporary redirect URL that points to your storage backend (S3, GCS, etc.). Works for public URLs but blocks crawlers if your bucket is private.
154
- - `:proxy``rails_storage_proxy_url` returns a proxy URL through Rails, which streams bytes from storage. Useful for private buckets where external crawlers (e.g., social media bots fetching `og:image`) need public, absolute URLs.
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 /media/picker?target=<input_id>`).
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 /media/images`, attaches the new blob to the same inputs, and reloads the grid.
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 (default: `/media`).
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 `/media`).
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 = image_blobs
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 { |blob| ml_image_payload(blob) }
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(blob), status: :created }
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
- value = params[:per_page].presence&.to_i
63
- value && value.positive? ? value : VenusMediaLibrary.configuration.per_page
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
- allowed = VenusMediaLibrary.configuration.allowed_content_types
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 = ActiveStorage::Blob.where("content_type LIKE ?", "image/%")
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 { |blob| ml_image_payload(blob) }
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(blob)
6
+ def ml_image_payload(asset)
7
+ blob = asset.blob
7
8
  {
8
- id: blob.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
- url: ml_blob_url(blob),
15
- thumb_url: ml_thumb_url(blob)
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(blob)
24
- if VenusMediaLibrary.configuration.url_type == :proxy
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(blob)
36
- if blob.variable?
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
@@ -0,0 +1,8 @@
1
+ module VenusMediaLibrary
2
+ class Asset < ApplicationRecord
3
+ belongs_to :blob, class_name: "ActiveStorage::Blob"
4
+ belongs_to :owner, polymorphic: true
5
+
6
+ validates :blob, uniqueness: true
7
+ end
8
+ 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>
@@ -6,6 +6,7 @@
6
6
  Upload image
7
7
  <input type="file" accept="image/*" hidden data-ml-upload>
8
8
  </label>
9
+ <label class="ml-picker__share"><input type="checkbox" data-ml-community-share> Share with community</label>
9
10
  <span class="ml-picker__hint" data-ml-status></span>
10
11
  </div>
11
12
 
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
@@ -1,3 +1,3 @@
1
1
  module VenusMediaLibrary
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -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 and shown in the library grid.
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
- # How image URLs in the payload/picker are built:
31
- # :redirect (default) -> rails_blob_url (302s to the storage URL)
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.1.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