This post goes through how to set up your Rails application to upload files to Google Cloud Storage using Google's gCloud gem and fog gem. <!–more–>
Unless you really need to use the gCloud gem, I highly recommend you follow this tutorial instead. It walks you through how to achieve uploads to Google Cloud Storage using the Carrierwave gem which, in my opinion, is a much better solution than gCloud.
The ability to upload PDF files was a major requirement of the Drumiverse. Storing files in the Rails /public
directory does not work if you are deploying to Heroku. The reason for this is because Heroku recreates your app from scratch every time your dyno is restarted after some period of inactivity.
The solution to this is to store persistent data on an external storage provider like Google Cloud Storage. They charge a few cents per GB storage, which is very reasonable. Unless you are storing large files that take up hundreds of gigabytes, or terabytes, then your monthly bill will be negligible.
Setting up a Project and Storage Bucket
- On your dashboard click on "Create a Cloud Storage Bucket". Enter a name for your project and select a location.
- Go to your new Project and navigate to the storage section.
- Create a new Bucket by entering a unique name. The cheapest storage class is Durable Reduced Availability (DRA), which sounds much worse than it is. From my understanding, it takes a few more microseconds before files are served following a file request from your rails app. That is perfectly ok. You can select the other storage class which costs only a few fractions of a cent more.
-
Once your bucket is created click on the three vertical dots to configure your bucket. Click on "Edit object default permissions".
-
Add a new item to the table. Select User from the dropdown.
-
enter "allUsers" in the text field
-
select "Reader" from the second drop down
-
Click Save
-
Do the same under "Edit Bucket Permissions"
-
Next, click on the "Settings" on the left sidebar, and then click on the "Interoperability" tab
-
Enable interoperability and create a new key. Keep this web page open because we need these keys in a few minutes.
Setting up your Rails application
Add some Gems
Add the following gems and run bundle install
#Google Cloud Storage Integration
gem 'gcloud'
gem 'fog' # they recommend using fog-google, I had issues doing that.
Create a Scaffold
If you haven't already created a scaffold, do so now. Your scaffold must include the attribute file:string
which we will use later to store the public URL of the uploaded file.
To add this to an existing model, create a migration.
rails g migration AddFileToMODELNAME file:string
Replace MODELNAME with the name of your existing model. Don't forget to rake db:migrate
before moving on to the next step.
Add Configuration Lines to Application.rb
Add the following lines to your config/application.rb
file. Replace BUCKETNAME with the name of the bucket you created earlier.
If your rails server fails to start then it is probably because you skipped this step.
config.x.settings = Rails.application.config_for :settings
config.x.fog_dir = 'BUCKETNAME'
Adding an Initializer
We have to create an initializer for the fog
gem. In config/initializers
create a new file called fog_cloud_storage.rb
and add the following code to it. (Just copy and paste, you don't have to customize anything here)
if Rails.env.test?
Fog.mock!
FogStorage = Fog::Storage.new(
provider: "Google",
google_storage_access_key_id: "mock",
google_storage_secret_access_key: "mock"
)
FogStorage.directories.create key: "testbucket", acl: "public-read"
StorageBucket = FogStorage.directories.get "testbucket"
else
config = Rails.application.config.x.settings["cloud_storage"]
FogStorage = Fog::Storage.new(
provider: "Google",
google_storage_access_key_id: config["access_key_id"],
google_storage_secret_access_key: config["secret_access_key"]
)
StorageBucket = FogStorage.directories.new key: config["bucket"]
end
Settings File
Next, we have to tell our application the interoperability keys we generated before. Create the following file:
default: &default
cloud_storage:
bucket: BUCKET_NAME
access_key_id: YOUR_ACCESS_KEY
secret_access_key: SECRET_ACCESS_KEY
development:
<<: *default
production:
cloud_storage:
bucket: BUCKET_NAME
access_key_id: YOUR_ACCESS_KEY
secret_access_key: SECRET_ACCESS_KEY
Make sure you use different keys for development and production and that you do not commit these keys into your repository, but instead inject them as environment variables. {: .notice–danger}
Add uploading logic to the model
Open your model file that contains the file:string
attribute you added before. Add the following lines. Do not refactor anything until you know uploading works. (I wasted a lot of time refactoring and breaking things along the way. This code is harder than it looks.)
This code was taken from Google's example project demonstrating how to upload to Google Cloud Storage. (I refactored it a bit.)
# used to temporarily store the file url from the form in the model
attr_accessor :file_url
private
after_create :upload_file, if: :file_url
def upload_file
if not file_url.blank?
pdf_file = StorageBucket.files.new(
key: "uploads/#{id}/#{file_url.original_filename.gsub(/[^0-9A-z.\-]/, '_')}",
body: file_url.read,
public: true
)
pdf_file.save
update_columns file: pdf_file.public_url
end
end
before_destroy :delete_file, if: :file #checks whether model has a file column set
def delete_file
bucket_name = StorageBucket.key
file_uri = URI.parse file #must be file, the database column
if file_uri.host == "#{bucket_name}.storage.googleapis.com"
# Remove leading forward slash from image path
# The result will be the image key, eg. "uploads/:id/:filename"
file_key = file_uri.path.sub("/", "")
file = StorageBucket.files.new key: file_key
file.destroy
end
end
before_update :update_file, if: :file_url #must be :file, checks whether there is a file to delete.
def update_file
delete_file if file?
upload_file
end
Prepare your View
If you used the scaffold generator to create your view, the you can easily add a file upload button to the form partial. (I am using simple_form. If you are using form_for, just google how to add a file field to a form.)
<%= f.file_field :file_url %>
Note that we modified the type of input as well as the attribute that the input value is stored in.
Allowing :file_url in params
The attribute :file_url
is a temporary variable declared in our model. We use this to store the :file_url
from the form. The actual file
we want to store in the database is assigned automatically in the upload_file
method in our model. In your controller, scroll down to the bottom and change the :file
attribute to :file_url
Note that the line of code should not contain the :file
attribute. Like I said, the model will assign that after a file is uploaded.
Conclusion
That should be all you have to do to upload files to Google Cloud Storage.
You can display the uploaded images in your view using an image tag. In app/views/MODELNAME/show.html.erb
, add this line:
<%= image_tag @post.file %>
Depending on the name of your model, you might have to change the @post
to @modelname
.
This code uploads the selected file to Google Cloud Storage, saving it in a subdirectory denoted by the record's ID number. Deleting or updating a record in rails will delete and update the file on your storage server as well.
If there are any mistakes or inconsistencies in this post, please point them out in the comments.