Using Sinatra for larger projects via multiple files
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
Sinatra Routing - Separate Files
You'll need to restart the webserver to load routes that were added while it was running. Routes are loaded into memory when app.rb is invoked and Sinatra is launched. The route itself looks fine and it appears routes.rb is being imported successfully via Dir[File.join(File.dirname(__FILE__), "lib", "*.rb")].each { |lib| require lib }
.
If you're running the server directly through terminal Ctrl+X
, Ctrl+C
should shut it down, then restart it via rackup config.ru
* or ruby app.rb
. You may confirm the route is recognized by making a get request through your browser to: http://127.0.0.1:4567/test
.
For the rackup config.ru
command to work, you can change config.ru
to something like:
# config.ru
require './app'
run Sinatra::Application
This is just a deployment convenience.
Edit: @shaun, because Todo
extends Sinatra::Base
it's fine to use run Todo
in your case.
Organizing Sinatra routing blocks over multiple files
You can just re-open the class in different files.
# file_a.rb
require 'sinatra'
require_relative "./file_b.rb"
class App < Sinatra::Base
get("/a") { "route a" }
run!
end
# file_b.rb
class App < Sinatra::Base
get("/b") { "route b" }
end
If you really want different classes you can do something like this, but it's a little ugly:
# file_a.rb
require 'sinatra'
require_relative "./file_b.rb"
class App < Sinatra::Base
get("/a") { "route a" }
extend B
run!
end
# file_b.rb
module B
def self.extended(base)
base.class_exec do
get("/b") { "route b" }
end
end
end
I'm pretty sure these two are the easiest ways to do it. When you look inside the source code of how Sinatra actually adds routes from a method like get
, it's pretty hairy.
I guess you could also do something goofy like this, but I wouldn't exactly call it idiomatic:
# file_a.rb
require 'sinatra'
class App < Sinatra::Base
get("/a") { "route a" }
eval File.read("./file_b.rb")
run!
end
# file_b.rb
get("/b") { "route b" }
How to upload multiple files via Sinatra and avoid NoMethodError - undefined method `read' for #File:0x0000000xxxxxx0:String
When you run join()
on your tempfiles:
mz= params['images'].map{|f| f[:tempfile] }.join(";")
You're taking a a bunch of File
objects and forcing them into strings using to_s()
. But the default to_s
for a File
is to create this generally useless thing:
"#<File:0x0000000xxxxxx0>"
Which is why you're getting the error message that you are.
As for how to fix it, the solution is simply to not turn your files into strings. I don't entirely understand why you're taking an array, joining it into a string, then immediately splitting the string back into an array. I would just not do any of that:
post '/upload2' do
puts params
filename = params["images"][0][:filename]
puts filename
tempfile = params["images"][0][:tempfile]
puts tempfile
path = "/home/user/Descargas/sinatra_ajax-master/#{filename}"
File.open(path, 'wb') do |f|
f.write(tempfile.read)
end
erb :index
end
Sinatra with multiple environment config
I finally came up with a solution using self.included class method:
# config.rb
require 'sinatra/custom_logger'
module Config
def self.included(base_klass)
base_klass.extend(ClassMethods)
base_klass.helpers(Sinatra::CustomLogger)
base_klass.class_eval do
configure :development do
logger = MyCustomLogger.new(
param1,
param2,
param3,
paramx
)
set :logger, logger
end
configure :production do
# other stuff
end
end
end
module ClassMethods; end
end
then
require_relative 'config'
class MyApp < Sinatra::Base
include Config
Multiple file uploads in Sinatra
The only thing that puts me off here is that you don't use the POST method – maybe your issue has to do with that. Anyway, the following code works perfectly for me. I hope this will give you a hint how to fix your code.
require 'sinatra'
get '/' do
<<-HTML
<html>
<head><title>Multi file upload</title></head>
<body>
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="images[]" multiple />
<input type="submit" />
</form>
</body>
</html>
HTML
end
post '/upload' do
content_type :text
res = "I received the following files:\n"
res << params['images'].map{|f| f[:filename] }.join("\n")
res
end
Sinatra Multiple Models / Helpers with same name
Quick solution is to use load instead of require, because load reloads file each time you ask it.
But much better solution is to require all libraries and make namespaces. Google for module in ruby as namespace
Build MVC structure on top of Sinatra
Sinatra is already "VC" - you have views separated from your routes (controllers). You can choose to break it into multiple files if you like; for more on that, see this answer (mine):
Using Sinatra for larger projects via multiple files
To add an "M" (model), pick a database framework. Some people like ActiveRecord. Some people like DataMapper. There are many more from which you might choose. I personally love and highly recommend Sequel. My answer linked above also suggests a directory structure and shell for including the models. Once you distribute appropriate logic between your models and controllers, you have your "MVC".
Note that MVC is not about separate files, but separation of concerns. If you set up a Sinatra application as I suggest above, but have your views fetching data from your models, or you have your routes directly generating HTML (not through a "helper"), then you don't really have MVC. Conversely, you can do all of the above in a single file and still have an MVC application. Just put your data-integrity logic in your models (and more importantly, in the database itself), your presentation logic in your views and reusable helpers, and your mapping logic in your controllers.
Serving large static files with Sinatra
I think I can just use send_file
(see here) - but if anyone has any other suggestions I have open ears!
Related Topics
How to Remove Rvm (Ruby Version Manager) from My System
Rails 4: List of Available Datatypes
Avoiding Applescript Through Ruby: Rb-Appscript or Rubyosa
Usage of Attr_Accessor in Rails
Access Variables Programmatically by Name in Ruby
Installing Rubygems in Windows
I Don't Understand Ruby Local Scope
Weird Backslash Substitution in Ruby
How to Test For (Activerecord) Object Equality
Git, Heroku: Pre-Receive Hook Declined