Advice about static files organization

91 views
Skip to first unread message

Jesús Gabriel y Galán

unread,
Jan 14, 2012, 5:37:46 AM1/14/12
to sina...@googlegroups.com
Hi all,

I'm building a site to play games online. I'm working on a little
framework to build turn based games, and now I'm trying to build a
sinatra controller for it.
The purpose of this controller is first to present a list of games
that are present on the system (the framework has an inherited hook to
keep track of which games are available).
These first screens' resources belong in a general folder. When a game
is chosen, though, the resources should be taken from that game's own
folders.
The games could be gems, for example, and are developed independently
of the framework. Each game class contains information about where are
its resources. So, for example I have this file structure:

sintra_app.rb
views/game_list.erb
public/js/main.js
public/css/main.css

In another path:

game1/game1.rb
game1/views/turn_start.erb
game1/public/js/events.js
game1/public/img/card1.jpg
...

So, for a specific request I would check in the session or database
which is the game that is being played, and ask its class for the
public path, but I don't know how to set it for the current request
only. I don't know if it's possible and I'm doing something wrong but
I get NoMethodErrors if I try to use set :public_folder inside a
route definition, so I'm guessing set can only be used in the app or
in configure blocks. Am I right?

So, what would be the best strategy to achieve that?

Thanks,

Jesus.

Jonathan Stott

unread,
Jan 14, 2012, 8:54:48 AM1/14/12
to sina...@googlegroups.com
You can do this quite easily if each game (and the base app) inherits
from Sinatra::Base. They can then have entirely independent settings
for things like the view directory and the public directory. You can
also set up a Rack::URLMap to route between them all.

Regards
Jonathan

2012/1/14 Jesús Gabriel y Galán <jgabrie...@gmail.com>:

> --
> You received this message because you are subscribed to the Google Groups "sinatrarb" group.
> To post to this group, send email to sina...@googlegroups.com.
> To unsubscribe from this group, send email to sinatrarb+...@googlegroups.com.
> For more options, visit this group at http://groups.google.com/group/sinatrarb?hl=en.
>

Jesús Gabriel y Galán

unread,
Jan 14, 2012, 9:07:37 AM1/14/12
to sina...@googlegroups.com
On Sat, Jan 14, 2012 at 2:54 PM, Jonathan Stott
<jonatha...@gmail.com> wrote:
> You can do this quite easily if each game (and the base app)  inherits
> from Sinatra::Base.  They can then have entirely independent settings
> for things like the view directory and the public directory.  You can
> also set up a Rack::URLMap to route between them all.

Thanks for your answer, I get what you mean. The way I have it now,
each game inherits from a framework class, and this "backend" could be
reused between different controllers:

- A CLI controller
- A GUI controler (i.e.: Shoes)
- A Sinatra controller

just providing views for that type of application.
If I could, I would rather not tie the implementation of the game
logic to Sinatra. Take a look for example to TicTacToe:

require 'gameframework/game/game'
require 'gameframework/game/event'

module TicTacToe
class Game < GameFramework::Game
view_path File.expand_path(File.join(File.dirname(__FILE__), "views"))
initial_view :map
initial_accepted_events :move
[... snip ...]
end

Each type of controller would use the view path and the current view
returned by the game to show the user. Each game could have a set of
views for each type of controller:

tictactoe/cli/map.erb
tictactoe/sinatra/map.erb
tictactoe/sinatra/public/cross.jpg
...

I guess I could just provide a class that inherits from Base, and each
game will provide a subclass, along with the views and resources.

Thanks,

Jesus.

Jonathan Stott

unread,
Jan 14, 2012, 10:08:41 AM1/14/12
to sina...@googlegroups.com
2012/1/14 Jesús Gabriel y Galán <jgabrie...@gmail.com>:
>
> Thanks for your answer, I get what you mean. The way I have it now,
> each game inherits from a framework class, and this "backend" could be
> reused between different controllers:
>
> - A CLI controller
> - A GUI controler (i.e.: Shoes)
> - A Sinatra controller
>
> just providing views for that type of application.
> If I could, I would rather not tie the implementation of the game
> logic to Sinatra. Take a look for example to TicTacToe:
>
> *SNIP*

>
> I guess I could just provide a class that inherits from Base, and each
> game will provide a subclass, along with the views and resources.

That's how I would do it. A relatively thin layer around the actual
game logic in another class which exposes that over HTTP. As you
develop more games, it might be possible to generate some or all of
your game applications automatically.

Alternatively, if you can determine the game you're in before you
reach sinatra (i.e. via a middleware) you could instead

set :public_folder, proc { request.env['game.public'] }

where the 'game.public' is a path set in the middleware. It needs to
be a middleware, as sinatra's static serving is done before before
filters are invoked. The other alternative, without a middleware,
would be to have the public folder proc determine the game in sinatra.

I'm not sure which is cleaner.

Regards
Jonathan

Jesús Gabriel y Galán

unread,
Jan 14, 2012, 10:23:15 AM1/14/12
to sina...@googlegroups.com

I tried this but it didn't work:

def view_path
@game ? @game.class.view_path : settings.public_folder
end

configure do
enable :sessions
set :public_folder, Proc.new {view_path}
end

@game is set in a before filter for paths that are in-game.

Jesus.

Jonathan Stott

unread,
Jan 14, 2012, 10:46:28 AM1/14/12
to sina...@googlegroups.com
2012/1/14 Jesús Gabriel y Galán <jgabrie...@gmail.com>:
> On Sat, Jan 14, 2012 at 4:08 PM, Jonathan Stott
> *SNIP*

>
> I tried this but it didn't work:
>
>                def view_path
>                        @game ? @game.class.view_path : settings.public_folder
>                end
>
>                configure do
>                        enable :sessions
>                        set :public_folder, Proc.new {view_path}
>                end
>
> @game is set in a before filter for paths that are in-game.

Yeah, as I said, the static files are served BEFORE the before filter
is processed. So you need to determine the game before you serve
static files to do that. This could be in a middleware, or in the
proc, but you can't rely on the before filters.

Regards
Jonathan

Jesús Gabriel y Galán

unread,
Jan 14, 2012, 11:16:54 AM1/14/12
to sina...@googlegroups.com

Oh, thanks. I missed to fully understand the implications of what you said.
I'll look into this and see the most convenient way. Could be a good
excuse to learn a bit more about Rack middleware.

Thanks a lot for you help.

Jesus.

Jesús Gabriel y Galán

unread,
Jan 14, 2012, 1:54:05 PM1/14/12
to sina...@googlegroups.com

Hi again,

I've tried the middleware root. I have a middleware that correctly
detects if there's a game in the session, but the problem I have now
is that I don't know how to access the request from the body of the
proc. I've tried the above but there's no request object.
Also, I would need to have a default value in case the middleware
doesn't set a value there. Using settings.public_folder won't work
since it will recursively call the proc again. Ideas?

Thanks,

Jesus.

Jonathan Stott

unread,
Jan 15, 2012, 6:48:37 AM1/15/12
to sina...@googlegroups.com
2012/1/14 Jesús Gabriel y Galán <jgabrie...@gmail.com>:
> *SNIP*

>
> Hi again,
>
> I've tried the middleware root. I have a middleware that correctly
> detects if there's a game in the session, but the problem I have now
> is that I don't know how to access the request from the body of the
> proc. I've tried the above but there's no request object.
> Also, I would need to have a default value in case the middleware
> doesn't set a value there. Using settings.public_folder won't work
> since it will recursively call the proc again. Ideas?

Ah, yes, that was me not thinking straight. Since the settings are
class methods, of course they don't have access to a request.

You have a few of options:

1) you could manage the serving of the assets through the same
middleware. Look at how Rack::Static (one of the middlewares bundled
with Rack) does it. It might be possible to modify that for your
needs.

2) If you mount each sinatra application at a different url via rack's
URLMap, you could just have independent public directories for each
game. so:

map "/" do
run RootApp
end

map "/tictactoe" do
run TicTacToeApp
end

etc.

Or via something like

class BaseGameApp < Sinatra::Base
def self.applications
@applications ||= {}
end

def self.namespace(name)
applications[name] = self
end
end

and have each app do

class TicTacToeApp < BaseGameApp
namespace "/tictactoe"
end

then in your config.ru do

run BaseGameApp.applications

Note that the URLMap middleware also supports mapping by hostname, so
you could map to 'tictactoe.yourdomain.com' assuming you have the
necessary CNAMEs or whatever.

Changing the initial portion of the URL or the hostname does have some
benefits, in that you could then send static assets with longer cache
expiry times and still be sure the client would see the right ones.
If you're serving different assets on the same path, it will mess up
caching unless you completely disable it, which makes your application
slower.

3) Implement static file serving yourself. override the static!
method in Sinatra::Base with one that checks for your middleware
defined value for the public folder, or defaults to the settings.

Regards
Jonathan

Jesús Gabriel y Galán

unread,
Jan 15, 2012, 7:33:28 AM1/15/12
to sina...@googlegroups.com
On Sun, Jan 15, 2012 at 12:48 PM, Jonathan Stott
<jonatha...@gmail.com> wrote:
> 2012/1/14 Jesús Gabriel y Galán <jgabrie...@gmail.com>:
>> *SNIP*
>>
>> Hi again,
>>
>> I've tried the middleware root. I have a middleware that correctly
>> detects if there's a game in the session, but the problem I have now
>> is that I don't know how to access the request from the body of the
>> proc. I've tried the above but there's no request object.
>> Also, I would need to have a default value in case the middleware
>> doesn't set a value there. Using settings.public_folder won't work
>> since it will recursively call the proc again. Ideas?
>
> Ah, yes, that was me not thinking straight.  Since the settings are
> class methods, of course they don't have access to a request.

When I realized this I understood why it was not possible to do what I
wanted with public_folder if I was hosting all games in the same
sinatra app. Even if I made it work for my test, it wouldn't work for
two different users accessing the site.

> You have a few of options:
>
> 1) you could manage the serving of the assets through the same
> middleware.  Look at how Rack::Static (one of the middlewares bundled
> with Rack) does it.  It might be possible to modify that for your
> needs.

After looking to the middleware options, this is what I ended up doing
yesterday. For the record, this is what is working for me:

require 'gameframework'

class GameStaticResource
def initialize app
@app = app
@file_servers = {}
end

def get_file_server_for_key key
root = GameFramework::Game.available_games[key].resource_path
Rack::File.new(root)
end

def file_server key
@file_servers[key] ||= get_file_server_for_key(key)
end

def call env
path = env["PATH_INFO"]
match = path.match(%r{/resources/(.*?)(/.*)})
if match
key = match.captures[0]
env["PATH_INFO"] = match.captures[1]
file_server(key).call(env)
else
@app.call(env)
end
end
end

Now I just tell the games' views to prefix the resources with
'/resources/#{game_key}' and voila, everything works as expected. I
think it's a good compromise that the sinatra views of the games have
to adhere to this contract for the static resources. I'm now trying to
do this prefixing via a helper, and I'm not managing to make it work,
but I want to dig a little bit more before asking (maybe I'll give up
at some point and ask anyway :) ).

> 2) If you mount each sinatra application at a different url via rack's
> URLMap, you could just have independent public directories for each
> game.

I thought a little bit, as we have already discussed it, about having
each game provide its own sinatra app, but for now I like the above
better.

> 3) Implement static file serving yourself. override the static!
> method in Sinatra::Base with one that checks for your middleware
> defined value for the public folder, or defaults to the settings.

I'd rather have Rack handle the static file serving, they probably do
it better than I ever could ! :D.

Thanks a lot for your help. I love Sinatra and I love this community.
You are all great !

Jesus.

Reply all
Reply to author
Forward
0 new messages