Is there another way, besides pretty_location, to utterly specify the file path

I am trying to replace Dragonfly 0.9 with Shrine 3.2.1 in an existing application. My goal is for this to be completely invisible to users and the application, such that existing URLs map directly over to the existing files in S3, even though I have ripped out the innards and replaced them. I’m also upgrading Rails from 4.0 to 6.0.3, just to keep things interesting.

I want this URL to work:

https://bucket_name.s3.us-east-2.amazonaws.com/titles/1839/1340.01_TP.jpg

pretty_location gets so close, but I can’t even see in the code how to do changes like tableize the attachment model, or use the original filename. Is there a way to get all the way exactly there, or is there another plugin or configuration option that I’m overlooking here?

Thanks in advance,

Walter

The pretty_location plugin is just a specific override of the Shrine#generate_location method, go to this section and scroll down for an example of what information is available to you in this method.

Thanks very much for the explanation of where the pieces live. Does io have a reference to the original filename in its world? Or is that in context?

Thanks,

Walter

Follow up: I am so close, but I’m sure I must be doing something wrong here. As a test, I duplicated one of my uploaders, and changed it to specify all the differences between the rest of my app and the new settings:

class MediaUploader < Shrine
  
  require 'shrine/storage/s3'
  
  s3_props = { public: true,
    REDACTED
  }
 
  storages = {
    cache: Shrine::Storage::S3.new( **s3_props), # temporary
    store: Shrine::Storage::S3.new( **s3_props), # permanent
  }
  
  def generate_location(io, record: nil, derivative: nil, **)
    return super unless record
 
    table  = record.class.table_name
    id     = record.id
    prefix = derivative || "original"
 
    "uploads/#{table}/#{id}/#{prefix}-#{record.file_name}"
  end
  
  plugin :derivatives
  plugin :remove_attachment
  plugin :metadata_attributes, filename: :name
  plugin :determine_mime_type
end

This is loaded into my model thusly:

class Source < ApplicationRecord
  belongs_to :title
  include MediaUploader::Attachment.new(:file)

In Rails console, I get one of my test records and start poking at it:

s = Source.first
  Source Load (0.3ms)  SELECT `sources`.* FROM `sources` ORDER BY `sources`.`id` ASC LIMIT 1
 => #<Source id: 228, name: nil, title_id: 1, file_name: "0687-02_LFeBk.pdf", file_uid: nil, content_format: "LF Printer PDF", file_size: nil, file_data: "{\"id\":\"48cd4fc9e0b661a19f2c00a5021c213e.pdf\",\"stor...", created_at: "2020-04-19 22:33:47", updated_at: "2020-04-19 22:33:47"> 
2.6.6 :005 > s.file.url
 => "/system/uploads/store/48cd4fc9e0b661a19f2c00a5021c213e.pdf" 

So that’s not using the output of generate_location at all. Not sure what, in fact, it is using. By poking around some more in s.file.methods, I worked out that I could call that method directly, if a little round-about:

s.file.uploader.generate_location s.file, record: s
 => "uploads/sources/228/original-0687-02_LFeBk.pdf" 
2.6.6 :015 > s.file.url
 => "/system/uploads/store/48cd4fc9e0b661a19f2c00a5021c213e.pdf" 

But I was expecting the generate_location to be invoked from within url, too.

What have I got wrong here?

Thanks again,

Walter

PS: I’m also curious why S3 isn’t coming into play here in the URL. I would imagine that would be a link to the actual file in storage, not just a path in the local filesystem.

Further hacking around here: I moved the storages definition out of the Shrine initializer, and put a copy of it in each of the uploader subclasses, so they each know about their own storage protocol, in the hopes that I would suddenly start seeing the S3 URLs in the call to .url. I also restarted Spring, just to see whether that made any difference. Here’s what I got after that:

 => #<Source id: 228, name: nil, title_id: 1, file_name: "0687-02_LFeBk.pdf", file_uid: nil, content_format: "LF Printer PDF", file_size: nil, file_data: "{\"id\":\"48cd4fc9e0b661a19f2c00a5021c213e.pdf\",\"stor...", created_at: "2020-04-19 22:33:47", updated_at: "2020-04-19 22:33:47"> 
2.6.6 :002 > s.file.url
 => "/system/48cd4fc9e0b661a19f2c00a5021c213e.pdf" 
2.6.6 :003 > s.file.uploader.generate_location s.file, record: s
 => "uploads/sources/228/0687-02_LFeBk.pdf" 
2.6.6 :004 > 

So again, it appears as though generate_location isn’t being consulted in generating the URL, nor is the storage protocol. I would expect that the generated URL would be identical whether .url or .generate_location was invoked.

I also expect that the resulting URL would be relative (like these examples) only in the case of a filesystem storage method, and an absolute S3 URL, concatenating all of the parts of the storage configuration together when the storage is S3.
:
Perhaps a basic question, then: is the .url method different at the model layer than at the view helper level? Will <%= image_tag @source.file.url %> present a complete S3 URL, even if console > s.file.url returns what looks like a relative (local filesystem) path?

Thoughts? Suggestions?

Thanks again,

Walter

It should not be different at the view helper level vs console.

What makes it different is how you’ve configured your shrine storage though. I’d expect s.file.url to return an S3 url if you have configured S3 storage; but maybe to return what looks like a local file path if you’ve configured a FileSystem storage.

So if you’re seeing a difference, I think it’s not that it’s view helper vs console, it’s that in your two different cases you’ve configured shrine storage differently, perhaps not intentionally? Or maybe you have cache and store storage configured differently, and in one case you were looking at a file in cache storage, and in another at a file in store storage?

What I have found, empirically, is that if I set the storage in the initializer, then whichever method I set there carries for the entire application (for any subclasses of that initializer). If I try to set storage in the subclass, I get filesystem, regardless.

My particular application is being somewhat polluted by my use of the ckeditor gem, though. That has its own Shrine initializer, and until I located it and rooted out its hard-coded storage path expectations (as well as its declaration of FileSystem storage), I was confused by what I was finding in my S3 bucket.

What I need to do next is make a new “stunt app” with no other gems, and see if I can work out experimentally whether it is possible to use S3 for some subclasses, and FileSystem for others. Eventually, I want everything on S3, but for the near-term, I need to avoid overwriting the files already in the bucket, and work out a way to read those files from S3 that were put there by a much older legacy application.

Thanks for your help, and for any other insights you may be able to pass along.

Walter