Sinatra - Setting Cache-Control Headers via config.ru - ruby

I'm currently running an Octopress (based on Jekyll) site on Heroku's Cedar stack—the code lives here: https://github.com/elithrar/octopress
I want to selectively apply a Cache-Control header based on the file type:
.html files get a value of public, max-age=3600
.css|.js|.png|.ico (etc) get a value of public, max-age=604800 - alternatively, I'd like to apply this rule to anything served from the /stylesheets', '/javascripts', '/imgs' directories.
Have used both set :static_cache_control , [:public, :max_age => 3600] and just the vanilla cache_control :public, :max_age => 3600 statements with no luck.
I have managed to set public, max-age=3600 on the articles themselves (e.g. /2012/lazy-sundays/), but have not been able to get the headers to apply to the CSS/JS (e.g. /stylesheets/screen.css)
My config.ru currently looks like this (updated):
require 'bundler/setup'
require 'sinatra/base'
# The project root directory
$root = ::File.dirname(__FILE__)
class SinatraStaticServer < Sinatra::Base
get(/.+/) do
cache_control :public, :max_age => 7200
send_sinatra_file(request.path) {404}
end
not_found do
send_sinatra_file('404.html') {"Sorry, I cannot find #{request.path}"}
cache_control :no_cache, :max_age => 0
end
def send_sinatra_file(path, &missing_file_block)
file_path = File.join(File.dirname(__FILE__), 'public', path)
file_path = File.join(file_path, 'index.html') unless file_path =~ /\.[a-z]+$/i
File.exist?(file_path) ? send_file(file_path) : missing_file_block.call
end
end
use Rack::Deflater
run SinatraStaticServer

Here's how to set long expiry headers for static assets, and an arbitrary expiry header for you main content on Heroku:
gemfile:
gem 'rack-contrib'
config.ru:
require 'rack/contrib'
get '*.html' do |page|
# whatever code you need to serve up your main pages
# goes here... use Rack::File I guess.
page
end
# Set content headers for that content...
before do
expires 5001, :public, :must_revalidate
end
# Assets in /static/stylesheets (domain.com/stylesheets)
# are served by Rack StaticCache, with a default 2 year expiry.
use Rack::StaticCache, :urls => ["/stylesheets"], :root => Dir.pwd + '/static'
run Sinatra::Application
By default that will give you a 2 year expiry for content listed in the array of urls (static/stylesheets, static/images etc.).
You have to move from /public to /static because otherwise you are unnecessarily fighting with Heroku's nginx config (the right place to apply these sorts of settings really...).
I know you said you're trying to not use Rack Contrib but that makes no sense. There's no harm in using a tiny 90 line library to do this https://github.com/rack/rack-contrib/blob/master/lib/rack/contrib/static_cache.rb.
The "right" way would be to host static content on an environment where you can configure nginx, and the second best way is renaming your static file path so heroku ignores it, and use rack static to serve static files with the headers you want.
--
Also to be clear, simply renaming your public folder to something else will allow you to do this via routes, and the normal Sinatra expires function. But I'd use StaticCache because it's less verbose. (The real issue is Heroku doesn't let nginx talk to your app for requests to public/, I believe.)

I have very little familiarity with Sinatra, but I think something like this would do the trick:
class SinatraStaticServer < Sinatra::Base
before '*.html' do
response.headers['Cache-Control'] = 'public, max-age=3600'
end
before %r{\.(css)|(js)|(png)|(ico)} do
response.headers['Cache-Control'] = 'public, max-age=604800'
end
# ...
end
Update: I looked into it further when you said that the above was not successfully getting the headers added. I determined that the issue was that Sinatra was automatically serving the files out of public/ rather than going through the app, and thus the headers weren't being added. My solution was to move the static files from public/ to public/public/ and adjust send_sinatra_file accordingly:
class SinatraStaticServer < Sinatra::Base
# ...
def send_sinatra_file(path, &missing_file_block)
file_path = File.join(File.dirname(__FILE__), 'public/public', path)
file_path = File.join(file_path, 'index.html') unless file_path =~ /\.[a-z]+$/i
File.exist?(file_path) ? send_file(file_path) : missing_file_block.call
end
# ...
end
I confirmed that this works on my machine. Note that I used response.headers['Cache-Control'] as in the first part of my answer, not set :static_cache_control which you tried, but I think is meant to only be run once, in a configure do block.
Also note that with this current set-up, a 404 that matches the above, e.g. nonexistant.png will serve a 404 status with the Cache-Control header still there. I can see several ways around that, but I figure you do to, so I'm just pointing it out and figure you'll deal with it however you like.

Related

Getting Rack mounted path in Sinatra application

Suppose I have the following config.ru file
require './status.rb'
map "/status" do
run Sinatra::Application
end
and the status.rb is a simple
require 'sinatra'
get '/' do
'Some status here...'
end
I'd like to know where the Sinatra application is mounted inside status.rb (for example to provide proper paths to resources). Is there a way of retrieving that information from Rack?
To get where the app is mounted you can use request.script_name.
get '/' do
p request.script_name # will print "/status"
'Some status here...'
end
If you’re generating urls for resources, you might want to look at the url method instead. That will take into account things like proxies as well as where the app is mounted:
get '/' do
p url('foo') # will print "http://localhost:9292/status/foo"
'Some status here...'
end

Explain to me a Sinatra setup for dummy's for a preview of html/css code

A word of warning up front: I do not know even the ruby basics, but I'm trying to learn more and more of the world of shell scripting this year.
I saw this Vimeo video of Ben Schwarz and immediately thought that I'd like to use such a tool to debug my sass and haml files.
So this is a call to help me to grasp the concept of Sinatra.
What I want is a simple way to output the code of my index.html to check if all the haml magic was applied correctly - so it should function as a source viewer that gives me live updates. I'd prefer it if Sinatra simply looked at the files that LiveReload already rendered (c.f. index.html) in my project folder.
Update: This is a screenshot of the Vimeo Video. It shows just the raw CSS output of a Sass file. This is what I'd like to have for my Haml and Sass code, or better for the output files that are already rendered by LiveReload as HTML and CSS.
I looked at the source file from #benschwarz at his github, but I wasn't even with his own example I'm only getting the standard: "Sinatra doesn’t know this ditty." So transferring this to work with html is still out of my reach.
What I did so far:
I setup my project as usual in ~/Sites/projectname
I setup RVM and install all the gems I need
Sass, Compass, Haml - the output gets compiled via LiveReload
Sinatra
I created myapp.rb in ~/Sites/projectname with the following content:
# myapp.rb
require 'sinatra'
set :public_folder, '/'
get '/' do
File.read(File.join('public', 'index.html'))
end
Whatever, I fired up Sinatra and checked http://localhost:4567/ – this didn't work because I do not know how to set the public_folder to ~/Sites/projectname.
Afterthoughts:
So I went on to search the net, but my limited knowledge of Ruby put my attempt of an successful research to an immediate halt.
Here are some sites I stumpled upon which are obvioulsy close to the solution I need, but… like I told you in the first sentence: if the solution was a book, I'd need the "For Dummies" version.
https://bitbucket.org/sulab/genelist_store/src/30fc0ba390b9/idea8/idea8.rb
Serving static files with Sinatra
http://www.sinatrarb.com/intro
Obvioulsy the reference documentation of Sinatra would help me, but I don't speak the language, hence, I don't get the lingo.
About public folder:
The public_folder is relative to your app file myapp.rb. If you have a public folder inside the projectname folder, this is your public folder. If you have your css, js and image files in another folder, say, includes under project_name, then you need to change the line:
# Actually, you need to remove the line above from myapp.rb as it is.
# The line means that the public folder which is used to have css, js and
# image files under '/' and that means that even myapp.rb is visible to everyone.
set :public_folder, '/'
# to:
set :public_folder, File.dirname(__FILE__) + '/includes'
And that will serve up css, js and/or image files from the project_name/includes folder instead of project_name/public folder.
Reading the html file:
Reading the html files does not depend on the public folder settings. These need not be inside the public folder.
get '/' do
File.read(File.dirname(__FILE__) + '/index.html')
# This says that the app should read the index.html
# Assuming that both myapp.rb and index.html are in the same folder.
# incase the html files are inside a different directory, say, html,
# change that line to:
# File.read(File.dirname(__FILE__) + '/html/index.html')
# Directory structure sample:
# project_name
# | - myapp.rb
# | - index.html (and not html/index.html etc.)
# | /public (or includes incase the css, js assets have a different location)
# | | /css
# | | /js
# | | /images
end
To get the html output inside the browser
After the file is read, typically, this will be your string: "<html><head></head><body></body></html>"
Without escaping the string, the browser renders the html string as html (no pun) and that's why you won't see any text. To escape the html, you can use the CGI class provided by Ruby (hat tip). So, in the end, this will be your snippet:
get '/' do
CGI::escapeHTML(File.read(File.dirname(__FILE__) + 'index.html'))
end
But that will spit out the html file in a single line. To clean it up,
# myapp.rb
get '/' do
#raw_html = CGI::escapeHTML(File.read(File.dirname(__FILE__) + 'index.html'))
end
# Using inline templates to keep things simple.
# get '/' do...end gets the index.erb file and hence, in the inline template,
# we need to use the ## index representation. If we say get '/home' do...end,
# then the inline template will come under ## home. All the html/erb between
# two "##"s will be rendered as one template (also called as view).
# The <%= #raw_html %>spews out the entire html string read inside the "get" block
__END__
## index
<html>
<head></head>
<body>
<pre>
<%= #raw_html %>
</pre>
</body>
</html>
If you're trying to render an index.html file, I would try storing it in the /views directory with an .erb extension. Or use an inline template. Here is a great resource

How do I config.ru properly in modular Sinatra application.?

I'm trying to use subclassing style in Sinatra application. So, I have a main app like this.
class MyApp < Sinatra::Base
get '/'
end
...
end
class AnotherRoute < MyApp
get '/another'
end
post '/another'
end
end
run Rack::URLMap.new \
"/" => MyApp.new,
"/another" => AnotherRoute.new
In config.ru I understand that it's only for "GET" how about other resources (e.g. "PUT", "POST")? I'm not sure if I'm missing someting obvious. And also if I have ten path (/path1, /path2, ...) do I have to config them all in config.ru even though they are in the same class?
app.rb
class MyApp < Sinatra::Base
get '/'
end
end
app2.rb If you want two separate files. Note this inherits from Sinatra::Base not MyApp.
class AnotherRoute < Sinatra::Base
get '/'
end
post '/'
end
end
The config.ru
require 'bundler/setup'
Bundler.require(:default)
require File.dirname(__FILE__) + "/lib/app.rb"
require File.dirname(__FILE__) + "/lib/app2.rb"
map "/" do
run MyApp
end
map "/another" do
run AnotherRoute
end
You could write this as
class MyApp < Sinatra::Base
get '/'
end
get '/another'
end
post '/another'
end
end
in config.ru
require './my_app'
run MyApp
Run:
rackup -p 1234
Refer to documentation at http://www.sinatrarb.com/intro#Serving%20a%20Modular%20Application
With URLMap you specify a base url where the app should be mounted. The path specified in the map is not used when determining which route to use within the app itself. In other words the app acts as if it's root is after the path used in URLMap.
For example, your code will respond to the following paths:
/: will be routed to the / route in MyApp
/another: will go to the / route in AnotherRoute. Since AnotherRoute extends MyApp this will be the same as / in MyApp (but in a different instance).
URLMap sees /another and uses it to map to AnotherRoute, stripping this part of the request from the path. AnotherRoute then only sees /.
/another/another: will be routed to the two /another routes in AnotherRoute. Again, the first another is used by the URLMap to route the request to AnotherRoute. AnotherRoute then only sees the second another as the path.
Note that this path will respond to both GET and POST requests, each being handled by the appropriate block.
It's not clear what you're trying to do, but I think you can achieve what you want by running an instance of AnotherRoute, with a config.ru that is just:
run AnotherRoute.new
Since AnotherRoute extends MyApp, the / route will be defined for it.
If you're looking for a way to add routes to an existing Sinatra application, you could create a module with an included method that adds the routes, rather than use inheritance.

Ruby/Sinatra send_file not working

I'm using send_file on a Sinatra app:
get '/update/dl/:upd' do
filename ="/uploads/#{params[:upd]}"
send_file(filename, :filename => "t.cer", :type => "application/octet-stream")
end
The folder /uploads/ it's not public, it's on the app dir. When I try to go to localhost:4567/update/dl/some_file in Chrome it returns me a 404, like with Firefox, when seeing the headers, it's a 404. But if I try with Safari it downloads the file. So I guess somthing's wrong with my code (and Safari's, but let's left that to Apple :P). What could be wrong? Thanks!
I get it to work fine in chrome if I remove the initial slash in filename so it's "filename instead of "/filename. The 404 comes from a file not found error in send_file
# foo.rb
require 'sinatra'
get '/update/dl/:upd' do
filename ="uploads/#{params[:upd]}"
# just send the file if it's an accepted file
if filename =~ /^[a-zA-Z0-9]*.cer$/
send_file(filename, :filename => "t.cer", :type => "application/octet-stream")
end
end
However, there's really a big security hole in this, a user can download anything that the sinatra process has access too, I named my sinatra app foo.rb and this request downloads the sinatra script:
http://localhost:4567/update/dl/..%2Ffoo.rb

Using Sinatra for larger projects via multiple files

It seems that in Sinatra all route handlers are being written into a single file, if I understand right it acts as a one large/small controller. Is there any way to split it into separate independent files, so when let's say somebody calls "/" - one action is executed, and if smth like "/posts/2" is received then another action - similar logic that is applied in PHP?
Here is a basic template for Sinatra apps that I use. (My larger apps have 200+ files broken out like this, not counting vendor'd gems, covering 75-100 explicit routes. Some of these routes are Regexp routes covering an additional 50+ route patterns.) When using Thin, you run an app like this using:
thin -R config.ru start
Edit: I'm now maintaining my own Monk skeleton based on the below called Riblits. To use it to copy my template as the basis for your own projects:
# Before creating your project
monk add riblits git://github.com/Phrogz/riblits.git
# Inside your empty project directory
monk init -s riblits
File Layout:
config.ru
app.rb
helpers/
init.rb
partials.rb
models/
init.rb
user.rb
routes/
init.rb
login.rb
main.rb
views/
layout.haml
login.haml
main.haml
config.ru
root = ::File.dirname(__FILE__)
require ::File.join( root, 'app' )
run MyApp.new
app.rb
# encoding: utf-8
require 'sinatra'
require 'haml'
class MyApp < Sinatra::Application
enable :sessions
configure :production do
set :haml, { :ugly=>true }
set :clean_trace, true
end
configure :development do
# ...
end
helpers do
include Rack::Utils
alias_method :h, :escape_html
end
end
require_relative 'models/init'
require_relative 'helpers/init'
require_relative 'routes/init'
helpers/init.rb
# encoding: utf-8
require_relative 'partials'
MyApp.helpers PartialPartials
require_relative 'nicebytes'
MyApp.helpers NiceBytes
helpers/partials.rb
# encoding: utf-8
module PartialPartials
def spoof_request(uri,env_modifications={})
call(env.merge("PATH_INFO" => uri).merge(env_modifications)).last.join
end
def partial( page, variables={} )
haml page, {layout:false}, variables
end
end
helpers/nicebytes.rb
# encoding: utf-8
module NiceBytes
K = 2.0**10
M = 2.0**20
G = 2.0**30
T = 2.0**40
def nice_bytes( bytes, max_digits=3 )
value, suffix, precision = case bytes
when 0...K
[ bytes, 'B', 0 ]
else
value, suffix = case bytes
when K...M then [ bytes / K, 'kiB' ]
when M...G then [ bytes / M, 'MiB' ]
when G...T then [ bytes / G, 'GiB' ]
else [ bytes / T, 'TiB' ]
end
used_digits = case value
when 0...10 then 1
when 10...100 then 2
when 100...1000 then 3
else 4
end
leftover_digits = max_digits - used_digits
[ value, suffix, leftover_digits > 0 ? leftover_digits : 0 ]
end
"%.#{precision}f#{suffix}" % value
end
module_function :nice_bytes # Allow NiceBytes.nice_bytes outside of Sinatra
end
models/init.rb
# encoding: utf-8
require 'sequel'
DB = Sequel.postgres 'dbname', user:'bduser', password:'dbpass', host:'localhost'
DB << "SET CLIENT_ENCODING TO 'UTF8';"
require_relative 'users'
models/user.rb
# encoding: utf-8
class User < Sequel::Model
# ...
end
routes/init.rb
# encoding: utf-8
require_relative 'login'
require_relative 'main'
routes/login.rb
# encoding: utf-8
class MyApp < Sinatra::Application
get "/login" do
#title = "Login"
haml :login
end
post "/login" do
# Define your own check_login
if user = check_login
session[ :user ] = user.pk
redirect '/'
else
redirect '/login'
end
end
get "/logout" do
session[:user] = session[:pass] = nil
redirect '/'
end
end
routes/main.rb
# encoding: utf-8
class MyApp < Sinatra::Application
get "/" do
#title = "Welcome to MyApp"
haml :main
end
end
views/layout.haml
!!! XML
!!! 1.1
%html(xmlns="http://www.w3.org/1999/xhtml")
%head
%title= #title
%link(rel="icon" type="image/png" href="/favicon.png")
%meta(http-equiv="X-UA-Compatible" content="IE=8")
%meta(http-equiv="Content-Script-Type" content="text/javascript" )
%meta(http-equiv="Content-Style-Type" content="text/css" )
%meta(http-equiv="Content-Type" content="text/html; charset=utf-8" )
%meta(http-equiv="expires" content="0" )
%meta(name="author" content="MeWho")
%body{id:#action}
%h1= #title
#content= yield
Absolutely. To see an example of this I recommend downloading the Monk gem, described here:
https://github.com/monkrb/monk
You can 'gem install' it via rubygems.org. Once you have the gem, generate a sample app using the instructions linked above.
Note that you don't have to use Monk for your actual development unless you want to (in fact I think it may not be current). The point is to see how you can easily structure your app in the MVC style (with separate controller-like route files) if you want to.
It's pretty simple if you look at how Monk handles it, mostly a matter of requiring files in separate directories, something like (you'll have to define root_path):
Dir[root_path("app/**/*.rb")].each do |file|
require file
end
Do a Google search for "Sinatra boilerplate" to get some ideas for how others are laying out their Sinatra applications. From that you can probably find one that suits your needs or simply make your own. It's not too hard to do. As you develop more Sinatra apps, you can add to your boilerplate.
Here's what I made and use for all of my projects:
https://github.com/rziehl/sinatra-boilerplate
I know this is an old query but I still can't believe no one mentioned Padrino You can use it as a framework on top of Sinatra, or piecemeal adding only the gems that interest you. It kicks ten buttloads of ass!
The key for modularity on Sinatra for larger projects is learning to use the underlying tools.
SitePoint has a very good tutorial from where you can see modular Sinatra apps and helpers. However you should pay special attention to one important detail. You keep multiple Sinatra apps and mount them with Rackup. Once you know how to write a basic app look at the config.ru file of that tutorial and observe how they mount independent Sinatra apps.
Once you learn to run Sinatra with Rack a whole new world of modularity strategies will open up. This obviously invites to try something really useful: now you can rely on having individual Gems for each sub application, what might enable you to easily version your modules.
Do not underestimate the power of using gem-modules for your app. You can easily test experimental changes in a well delimited environment and easily deploy them. Equally easy to revert back if something goes wrong.
There are a thousand ways to organize your code, so it would not hurt trying to get a layout similar to Rails. However there are also some great posts about how to customize your own structure. That post covers other frequent needs of most web developers.
If you have the time, I encourage you to learn more about Rack, the common ground for any Ruby based web application. It might have a far lesser impact in how you do your work, but there are always certain tasks that most people do on their apps that fits better as a Rack middleware.
My approach to host different projects on the same site is to use sinatra/namespace in such way:
server.rb
require "sinatra"
require "sinatra/namespace"
if [ENV["LOGNAME"], ENV["USER"]] == [nil, "naki"]
require "sinatra/reloader"
register Sinatra::Reloader
set :port, 8719
else
set :environment, :production
end
for server in Dir.glob "server_*.rb"
require_relative server
end
get "/" do
"this route is useless"
end
server_someproject.rb
module SomeProject
def self.foo bar
...
end
...
end
namespace "/someproject" do
set :views, settings.root
get "" do
redirect request.env["REQUEST_PATH"] + "/"
end
get "/" do
haml :view_someproject
end
post "/foo" do
...
SomeProject.foo ...
end
end
view_someproject.haml
!!!
%html
...
Another detail about subprojects I used was to add their names, description and routes to some kind of global variable, that is used by "/" to make a guide homepage, but I don't have a snippet right now.
Reading the docs here:
Sinatra Extensions
It appears that Sinatra allows you to decompose your application into Ruby Modules, which can be pulled in through the Sinatra "register" method or "helpers" methods, like so:
helpers.rb
require 'sinatra/base'
module Sinatra
module Sample
module Helpers
def require_logged_in()
redirect('/login') unless session[:authenticated]
end
end
end
end
routing/foos.rb
require 'sinatra/base'
module Sinatra
module Sample
module Routing
module Foos
def self.registered(app)
app.get '/foos/:id' do
# invoke a helper
require_logged_in
# load a foo, or whatever
erb :foos_view, :locals => { :foo => some_loaded_foo }
end
end
end
end
end
end
app.rb
#!/usr/bin/env ruby
require 'sinatra'
require_relative 'routing/foos'
class SampleApp < Sinatra::Base
helpers Sinatra::Sample::Helpers
register Sinatra::Sample::Routing::Foos
end
When Monk didn't work for me, I started working on templates myself.
If you think about it, there is nothing special about tying up a set of files. The monk philosophy was explained to me early in 2011 during RedDotRubyConf and they have specifically told me that it's really optional to use it especially now that it's hardly maintained.
This is a good start for those who want to use ActiveRecord:
Simple Sinatra MVC
https://github.com/katgironpe/simple-sinatra-mvc

Resources