gallerist 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/CONTRIBUTING.md +53 -0
  4. data/Gemfile +8 -0
  5. data/LICENSE +25 -0
  6. data/README.md +57 -0
  7. data/Rakefile +16 -0
  8. data/assets/stylesheets/main.scss +17 -0
  9. data/bin/gallerist +59 -0
  10. data/config.ru +21 -0
  11. data/gallerist.gemspec +32 -0
  12. data/lib/gallerist.rb +46 -0
  13. data/lib/gallerist/app.rb +230 -0
  14. data/lib/gallerist/errors.rb +12 -0
  15. data/lib/gallerist/helpers.rb +72 -0
  16. data/lib/gallerist/library.rb +100 -0
  17. data/lib/gallerist/middleware/raise_warmup_exceptions.rb +24 -0
  18. data/lib/gallerist/middleware/show_exceptions.rb +40 -0
  19. data/lib/gallerist/models/admin_data.rb +10 -0
  20. data/lib/gallerist/models/album.rb +30 -0
  21. data/lib/gallerist/models/album_photo.rb +20 -0
  22. data/lib/gallerist/models/base_model.rb +32 -0
  23. data/lib/gallerist/models/image_proxies_model.rb +11 -0
  24. data/lib/gallerist/models/image_proxy_state.rb +22 -0
  25. data/lib/gallerist/models/master.rb +25 -0
  26. data/lib/gallerist/models/model_resource.rb +22 -0
  27. data/lib/gallerist/models/person.rb +46 -0
  28. data/lib/gallerist/models/person_model.rb +10 -0
  29. data/lib/gallerist/models/person_photo.rb +39 -0
  30. data/lib/gallerist/models/photo.rb +100 -0
  31. data/lib/gallerist/models/tag.rb +31 -0
  32. data/lib/gallerist/models/tag_photo.rb +20 -0
  33. data/lib/rack/handler/unicorn.rb +34 -0
  34. data/views/500.erb +5 -0
  35. data/views/album.erb +9 -0
  36. data/views/index.erb +24 -0
  37. data/views/layout.erb +34 -0
  38. data/views/partials/thumbnail.erb +13 -0
  39. data/views/person.erb +9 -0
  40. data/views/photos.erb +9 -0
  41. data/views/tag.erb +9 -0
  42. metadata +183 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ff2b65d33304727eae500b7fc80f7de3b925c9e9
4
+ data.tar.gz: 4725ef924d8903081662a61279c01fec06343499
5
+ SHA512:
6
+ metadata.gz: 16b0d2f8928d41365189e1159e59eae1a37b2256bfbfbd4c3aa28a0d87bc2a7405ba49d5d3c2c0d26876e7d5c7567b8de21d1349e16d043d9d0d350d6a2880f7
7
+ data.tar.gz: 8f3a100f666ffe5c626c9ca231b1b36cdbaaa402a52c83e0a105af6c3f676227bc07c159810923524b5e6ea84be02f2d0cd91c1dbbe4d9e68900511eed671352
@@ -0,0 +1,2 @@
1
+ pkg/
2
+ Gemfile.lock
@@ -0,0 +1,53 @@
1
+ Contribution Guidelines
2
+ =======================
3
+
4
+ First of all, each single contribution is appreciated, whether a typo fix,
5
+ improved documentation, a fixed bug or a whole new feature.
6
+
7
+ ## Making your changes
8
+
9
+ 1. Fork the repository on GitHub
10
+ 2. Create a topic branch with a descriptive name, e.g. `fix-issue-123` or
11
+ `feature-x`
12
+ 3. Make your modifications, complying with the
13
+ [code conventions](#code-conventions)
14
+ 4. Commit small logical changes, each with a descriptive commit message.
15
+ Please don˚t mix unrelated changes in a single commit.
16
+ 5. Don˚t commit things that are unrelated to the whole change.
17
+
18
+ ## Commit messages
19
+
20
+ Please format your commit messages as follows:
21
+
22
+ Short summary of the change (up to 50 characters)
23
+
24
+ Optionally add a more extensive description of your change after a
25
+ blank line. Wrap the lines in this and the following paragraphs after
26
+ 72 characters.
27
+
28
+ ## Submitting your changes
29
+
30
+ 1. Push your changes to a topic branch in your fork of the repository.
31
+ 2. [Submit a pull request][pr] to the original repository.
32
+ Describe your changes as short as possible, but as detailed as needed for
33
+ others to get an overview of your modifications.
34
+
35
+ ## Code conventions
36
+
37
+ * White spaces:
38
+ * Indent using 2 spaces
39
+ * Line endings must be line feeds (\n)
40
+ * Add a newline at the end of a file
41
+ * Name conventions:
42
+ * `UpperCamelCase` for classes and modules
43
+ * `lower_case` for variables and methods
44
+ * `UPPER_CASE` for constants
45
+
46
+ ## Further information
47
+
48
+ * [General GitHub documentation][gh-help]
49
+ * [GitHub pull request documentation][gh-pr]
50
+
51
+ [gh-help]: https://help.github.com
52
+ [gh-pr]: https://help.github.com/send-pull-requests
53
+ [pr]: https://github.com/koraktor/gallerist/pull/new
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ # This code is free software; you can redistribute it and/or modify it under
2
+ # the terms of the new BSD License.
3
+ #
4
+ # Copyright (c) 2015, Sebastian Staudt
5
+
6
+ source 'https://rubygems.org'
7
+
8
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,25 @@
1
+ Copyright (c) 2015, Sebastian Staudt
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification,
5
+ are permitted provided that the following conditions are met:
6
+
7
+ * Redistributions of source code must retain the above copyright notice,
8
+ this list of conditions and the following disclaimer.
9
+ * Redistributions in binary form must reproduce the above copyright notice,
10
+ this list of conditions and the following disclaimer in the documentation
11
+ and/or other materials provided with the distribution.
12
+ * Neither the name of the author nor the names of its contributors
13
+ may be used to endorse or promote products derived from this software
14
+ without specific prior written permission.
15
+
16
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
20
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
23
+ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,57 @@
1
+ Gallerist
2
+ =========
3
+
4
+ Gallerist is a web application to browse libraries of Apple Photos and iPhoto.
5
+ It is built on top of [Sinatra][sinatra].
6
+
7
+ ## Requirements
8
+
9
+ * One or more Photos or iPhoto libraries (`.photoslibrary` or `.photolibrary`
10
+ directories)
11
+ * Ruby 2.x with Bundler
12
+
13
+ OS X 10.9 and above ship with Ruby 2.0. You will only have to install Bundler
14
+ (`gem install bundler`) to get started.
15
+
16
+ ## Installation
17
+
18
+ Gallerist can be simply installed as a Ruby gem.
19
+
20
+ ```shell
21
+ $ gem install gallerist
22
+ ```
23
+
24
+ *Note*: You might need to use `sudo` if you’re installing into your system
25
+ Ruby, e.g. when not using rbenv or RVM.
26
+
27
+ If you want to run the current development code please use Git to clone the
28
+ repository.
29
+
30
+ ## Usage
31
+
32
+ ```shell
33
+ $ bin/gallerist ~/Pictures/Photos\ Library.photoslibrary
34
+ ```
35
+
36
+ After that the application is served on port 9292 by default. You can open it
37
+ by simply browsing to `http://localhost:9292`.
38
+
39
+ Further command-line arguments are available, see `gallerist --help` for more
40
+ information.
41
+
42
+ ## Caveats
43
+
44
+ * iPhoto libraries work to a certain degree as iPhoto’s events are not
45
+ listed, only albums.
46
+ * Gallerist works on a copy of the library databases, i.e. changes to the
47
+ original library will not be reflected instantly. You will have to restart
48
+ the web app first.
49
+
50
+ ## Future plans
51
+
52
+ * Support for internal categories like photo stream and videos
53
+ * Support for moments and places
54
+ * Performance improvements
55
+
56
+ [brew]: http://brew.sh
57
+ [sinatra]: http://www.sinatrarb.com
@@ -0,0 +1,16 @@
1
+ # This code is free software; you can redistribute it and/or modify it under
2
+ # the terms of the new BSD License.
3
+ #
4
+ # Copyright (c) 2015, Sebastian Staudt
5
+
6
+ require 'rubygems/package_task'
7
+
8
+ spec = Gem::Specification.load 'gallerist.gemspec'
9
+ Gem::PackageTask.new spec do |pkg|
10
+ end
11
+
12
+ # Task to clean the package directory
13
+ desc 'Clean package directory'
14
+ task :clean do
15
+ FileUtils.rm_rf 'pkg'
16
+ end
@@ -0,0 +1,17 @@
1
+ /**
2
+ * This code is free software; you can redistribute it and/or modify it under
3
+ * the terms of the new BSD License.
4
+ *
5
+ * Copyright (c) 2015, Sebastian Staudt
6
+ */
7
+
8
+ @import 'bootstrap';
9
+
10
+ .navbar-inverse .navbar-brand:hover {
11
+ color: $navbar-inverse-brand-color;
12
+ }
13
+
14
+ .tag {
15
+ display: inline-block;
16
+ margin-right: 1pt;
17
+ }
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # This code is free software; you can redistribute it and/or modify it under
4
+ # the terms of the new BSD License.
5
+ #
6
+ # Copyright (c) 2015, Sebastian Staudt
7
+
8
+ require 'optparse'
9
+ require 'rack'
10
+
11
+ ENV['RACK_ENV'] ||= 'production'
12
+
13
+ server_options = { port: 9292 }
14
+ opts = OptionParser.new do |opts|
15
+ opts.banner = 'Usage: gallerist [options] <library>'
16
+
17
+ opts.separator ''
18
+ opts.separator 'Options:'
19
+ opts.on('-p', '--port PORT', 'Specify on which port to listen') do |port|
20
+ server_options[:port] = port
21
+ end
22
+ opts.on('-d', '--debug', 'Enable debug mode') do
23
+ ENV['RACK_ENV'] = 'development'
24
+ end
25
+
26
+ opts.separator ''
27
+ opts.separator 'Arguments:'
28
+ opts.separator opts.summary_indent + '<library> The path to the photo library to open.'
29
+
30
+ opts.separator ''
31
+ opts.separator 'Advanced options:'
32
+ opts.on('-n', '--no-copy', 'Disable temporary copies of the library databases') do
33
+ ENV['GALLERIST_NOCOPY'] = 'true'
34
+ end
35
+ end
36
+ opts.parse!
37
+
38
+ $LOAD_PATH << File.join(__dir__, '..', 'lib')
39
+
40
+ require 'gallerist'
41
+
42
+ config_path = File.expand_path File.join(__dir__, '..', 'config.ru')
43
+
44
+ ENV['GALLERIST_LIBRARY'] = ARGV[0]
45
+
46
+ if ENV['GALLERIST_LIBRARY'].nil?
47
+ puts opts.help
48
+ exit 1
49
+ end
50
+
51
+ begin
52
+ Rack::Server.start config: config_path,
53
+ server: :unicorn,
54
+ Port: server_options[:port],
55
+ worker_processes: 4
56
+ rescue Gallerist::LibraryInUseError
57
+ $stderr.puts 'The library is currently in use.' <<
58
+ ' Is it currently opened in Photos?'
59
+ end
@@ -0,0 +1,21 @@
1
+ # This code is free software; you can redistribute it and/or modify it under
2
+ # the terms of the new BSD License.
3
+ #
4
+ # Copyright (c) 2015, Sebastian Staudt
5
+
6
+ unless defined? Gallerist
7
+ $LOAD_PATH << File.join(__dir__, 'lib')
8
+ require 'gallerist'
9
+ end
10
+
11
+ warmup do |app|
12
+ Rack::MockRequest.new(app).get '/',
13
+ 'rack.errors' => $stderr,
14
+ 'rack.warmup' => true
15
+ end
16
+
17
+ map '/assets' do
18
+ run Gallerist::App.sprockets
19
+ end
20
+
21
+ run Gallerist::App
@@ -0,0 +1,32 @@
1
+ # This code is free software; you can redistribute it and/or modify it under
2
+ # the terms of the new BSD License.
3
+ #
4
+ # Copyright (c) 2015, Sebastian Staudt
5
+
6
+ $LOAD_PATH << File.join(__dir__, 'lib')
7
+
8
+ require 'gallerist'
9
+
10
+ Gem::Specification.new do |s|
11
+ s.name = 'gallerist'
12
+ s.version = Gallerist::VERSION
13
+ s.platform = Gem::Platform::RUBY
14
+ s.authors = [ 'Sebastian Staudt' ]
15
+ s.email = [ 'koraktor@gmail.com' ]
16
+ s.homepage = 'https://github.com/koraktor/gallerist'
17
+ s.license = 'BSD'
18
+ s.summary = 'A web application to browse Apple Photos and iPhoto libraries'
19
+ s.description = 'View Photos and iPhoto libraries in your browser'
20
+
21
+ s.add_dependency 'activerecord', '~> 4.2'
22
+ s.add_dependency 'bootstrap-sass', '~> 3.3'
23
+ s.add_dependency 'rack', '~> 1.6'
24
+ s.add_dependency 'sinatra', '~> 1.4'
25
+ s.add_dependency 'sprockets-helpers', '~> 1.1'
26
+ s.add_dependency 'sqlite3', '~> 1.3'
27
+ s.add_dependency 'unicorn', '~> 4.8'
28
+
29
+ s.executables = [ 'gallerist' ]
30
+ s.files = `git ls-files`.split
31
+ s.require_paths = 'lib'
32
+ end
@@ -0,0 +1,46 @@
1
+ # This code is free software; you can redistribute it and/or modify it under
2
+ # the terms of the new BSD License.
3
+ #
4
+ # Copyright (c) 2015, Sebastian Staudt
5
+
6
+ module Gallerist
7
+
8
+ VERSION = '0.1.0'
9
+
10
+ autoload :App, 'gallerist/app'
11
+ autoload :Helpers, 'gallerist/helpers'
12
+ autoload :Library, 'gallerist/library'
13
+ autoload :LibraryInUseError, 'gallerist/errors'
14
+ autoload :RaiseWarmupExceptions, 'gallerist/middleware/raise_warmup_exceptions'
15
+ autoload :ShowExceptions, 'gallerist/middleware/show_exceptions'
16
+
17
+ # Models
18
+
19
+ MODELS = {}
20
+
21
+ def self.model(name, file)
22
+ autoload name, file
23
+
24
+ MODELS[name] = file
25
+ end
26
+
27
+ def self.load_models
28
+ MODELS.values.each { |file| require file }
29
+ end
30
+
31
+ model :AdminData, 'gallerist/models/admin_data'
32
+ model :Album, 'gallerist/models/album'
33
+ model :AlbumPhoto, 'gallerist/models/album_photo'
34
+ model :BaseModel, 'gallerist/models/base_model'
35
+ model :ImageProxiesModel, 'gallerist/models/image_proxies_model'
36
+ model :ImageProxyState, 'gallerist/models/image_proxy_state'
37
+ model :Master, 'gallerist/models/master'
38
+ model :ModelResource, 'gallerist/models/model_resource'
39
+ model :Person, 'gallerist/models/person'
40
+ model :PersonModel, 'gallerist/models/person_model'
41
+ model :PersonPhoto, 'gallerist/models/person_photo'
42
+ model :Photo, 'gallerist/models/photo'
43
+ model :Tag, 'gallerist/models/tag'
44
+ model :TagPhoto, 'gallerist/models/tag_photo'
45
+
46
+ end
@@ -0,0 +1,230 @@
1
+ # This code is free software; you can redistribute it and/or modify it under
2
+ # the terms of the new BSD License.
3
+ #
4
+ # Copyright (c) 2015, Sebastian Staudt
5
+
6
+ require 'active_record'
7
+ require 'bootstrap-sass'
8
+ require 'logger'
9
+ require 'sinatra/sprockets-helpers'
10
+
11
+ class Gallerist::App < Sinatra::Base
12
+
13
+ register Sinatra::Sprockets::Helpers
14
+
15
+ configure do
16
+ enable :logging
17
+
18
+ set :root, File.join(root, '..', '..')
19
+
20
+ set :library_path, ENV['GALLERIST_LIBRARY']
21
+ set :copy_dbs, !ENV['GALLERIST_NOCOPY']
22
+ set :views, File.join(root, 'views')
23
+
24
+ set :sprockets, Sprockets::Environment.new(root)
25
+
26
+ sprockets.append_path File.join(root, 'assets', 'stylesheets')
27
+ sprockets.cache = Sprockets::Cache::FileStore.new Dir.tmpdir
28
+ sprockets.css_compressor = :scss
29
+
30
+ configure_sprockets_helpers do |helpers|
31
+ helpers.debug = development?
32
+ end
33
+
34
+ Bootstrap.load!
35
+ end
36
+
37
+ configure :development do
38
+ set :logging, ::Logger::DEBUG
39
+
40
+ debug_logger = ::Logger.new($stdout, ::Logger::DEBUG)
41
+
42
+ sprockets.logger = debug_logger
43
+ ActiveRecord::Base.logger = debug_logger
44
+ end
45
+
46
+ error 500 do |error|
47
+ @title = 'Error'
48
+
49
+ raise error if env['rack.errors'].is_a? StringIO
50
+
51
+ erb :'500'
52
+ end
53
+
54
+ helpers Gallerist::Helpers
55
+
56
+ def send_library_file(file, options = {})
57
+ logger.debug "Serving file '%s' from library..." % [ file ]
58
+
59
+ file_path = File.join(library.path, file)
60
+ response = catch(:halt) { send_file file_path, options }
61
+ if response == 404
62
+ logger.error "File '%s' could not be served, because it does not exist." % file
63
+ end
64
+
65
+ halt response
66
+ end
67
+
68
+ def library
69
+ unless settings.respond_to? :library
70
+ settings.set :library, Gallerist::Library.new(settings.library_path)
71
+
72
+ logger.info "Loading library from \"#{library.path}\""
73
+
74
+ if settings.copy_dbs
75
+ logger.debug 'Creating temporary copy of the main library database...'
76
+ library.copy_base_db
77
+ logger.debug ' Completed.'
78
+ end
79
+
80
+ ActiveRecord::Base.establish_connection({
81
+ adapter: 'sqlite3',
82
+ database: library.library_db
83
+ })
84
+ ActiveRecord::Base.connection.exec_query 'PRAGMA journal_mode="MEMORY";'
85
+
86
+ if settings.copy_dbs
87
+ logger.debug 'Creating temporary copies of additional library databases...'
88
+ library.copy_extra_dbs
89
+ logger.debug ' Completed.'
90
+ end
91
+
92
+ logger.info " Found library with type '%s'." % [ library.app_id ]
93
+
94
+ Gallerist.load_models
95
+ Gallerist::BaseModel.descendants.each do |model|
96
+ logger.debug "Setting up %s for library type '%s'" % [ model, library.type ]
97
+
98
+ model.setup_for library.type
99
+ end
100
+
101
+ Gallerist::ImageProxiesModel.establish_connection({
102
+ adapter: 'sqlite3',
103
+ database: library.image_proxies_db
104
+ })
105
+ Gallerist::ImageProxiesModel.connection.exec_query 'PRAGMA journal_mode="MEMORY";'
106
+
107
+ Gallerist::PersonModel.establish_connection({
108
+ adapter: 'sqlite3',
109
+ database: library.person_db
110
+ })
111
+ Gallerist::PersonModel.connection.exec_query 'PRAGMA journal_mode="MEMORY";'
112
+ end
113
+ settings.library
114
+ end
115
+
116
+ def navbar_for(obj)
117
+ @navbar = [[ '/', library.name ]]
118
+
119
+ case obj
120
+ when :all_photos
121
+ @navbar << [ '/photos', 'All photos' ]
122
+ when :favorites
123
+ @navbar << [ '/favorites', 'Favorites' ]
124
+ when Gallerist::Album
125
+ @navbar << [ '/albums', 'Albums' ]
126
+ @navbar << [ '/albums/%s' % [ obj.id ], obj.name ]
127
+ when Gallerist::Person
128
+ @navbar << [ '/persons', 'Persons' ]
129
+ @navbar << [ '/persons/%s' % [ obj.id ], obj.name ]
130
+ when Gallerist::Tag
131
+ @navbar << [ '/tags', 'Tags' ]
132
+ @navbar << [ '/tags/%s' % [ obj.simple_name ], obj.name ]
133
+ end
134
+ end
135
+
136
+ def photo(id)
137
+ Gallerist::Photo.find id
138
+ rescue ActiveRecord::RecordNotFound
139
+ logger.error 'Could not find the photo with ID #%s.' % [ id ]
140
+ not_found
141
+ end
142
+
143
+ def self.setup_default_middleware(builder)
144
+ builder.use Sinatra::ExtendedRack
145
+ builder.use Gallerist::ShowExceptions if show_exceptions?
146
+ builder.use Gallerist::RaiseWarmupExceptions
147
+
148
+ setup_logging builder
149
+ end
150
+
151
+ get '/' do
152
+ @title = library.name
153
+
154
+ @albums = library.albums.visible.nonempty.order :date
155
+ @persons = library.persons
156
+ @tags = library.tags.nonempty.order 'photos_count desc'
157
+
158
+ navbar_for :root
159
+
160
+ erb :index
161
+ end
162
+
163
+ get '/albums/:id' do
164
+ @album = library.albums.find params[:id]
165
+ @title = @album.name
166
+
167
+ navbar_for @album
168
+
169
+ erb :album
170
+ end
171
+
172
+ get '/favorites' do
173
+ @photos = library.photos.favorites
174
+ @title = 'Favorites'
175
+
176
+ navbar_for :favorites
177
+
178
+ erb :photos
179
+ end
180
+
181
+ get '/persons/:id' do
182
+ @person = library.persons.find params[:id]
183
+ @title = @person.name
184
+
185
+ navbar_for @person
186
+
187
+ erb :person
188
+ end
189
+
190
+ get '/photos' do
191
+ @photos = library.photos
192
+ @title = 'All photos'
193
+
194
+ navbar_for :all_photos
195
+
196
+ erb :photos
197
+ end
198
+
199
+ get '/photos/:id' do
200
+ photo = photo params[:id]
201
+
202
+ send_library_file photo.image_path,
203
+ disposition: :inline,
204
+ filename: photo.file_name
205
+ end
206
+
207
+ get '/tags/:name' do
208
+ @tag = library.tags.find_by_simple_name params[:name]
209
+ @title = @tag.name
210
+
211
+ navbar_for @tag
212
+
213
+ erb :tag
214
+ end
215
+
216
+ get '/thumbs/:id' do
217
+ photo = photo params[:id]
218
+
219
+ if photo.thumbnail_available?
220
+ thumbnail_path = photo.small_thumbnail_path
221
+ else
222
+ thumbnail_path = photo.preview_path
223
+ end
224
+
225
+ send_library_file thumbnail_path,
226
+ disposition: :inline,
227
+ filename: 'thumb_%s' % [ photo.file_name ]
228
+ end
229
+
230
+ end