shotwellfs 0.0.1RC0

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.
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ InstalledFiles
7
+ _yardoc
8
+ coverage
9
+ doc/
10
+ lib/bundler/man
11
+ pkg
12
+ rdoc
13
+ spec/reports
14
+ test/tmp
15
+ test/version_tmp
16
+ tmp
17
+ Gemfile.lock
18
+ .ruby-version
19
+ .ruby-gemset
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'rfuse', :path => "../rfuse", :git => "https://github.com/lwoggardner/rfuse.git"
4
+ gem 'rfusefs', :path => "../rfusefs", :git => "https://github.com/lwoggardner/rfusefs.git"
5
+ # Specify your gem's dependencies in shotwellfs.gemspec
6
+ gemspec
7
+
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Grant Gardner
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # Shotwellfs
2
+
3
+ Provides a FUSE filesystem over a Shotwell database of photos and videos (http://yorba.org/shotwell)
4
+
5
+ ## Installation
6
+
7
+ $ gem install shotwellfs
8
+
9
+ ## Usage
10
+
11
+ Start shotwellfs
12
+
13
+ $ shotwellfs <path/to/shotwell-dir> <mountpoint> [ -o mountoptions ]
14
+
15
+ _shotwell-dir_ is the directory containing shotwell's private data (ie data/photo.db)
16
+
17
+ Navigate to _mountpoint_ in your favourite file browser and see your photos layed out as events
18
+
19
+ * Crop and RedEye transformations are applied to JPG and TIF images (and cached in shotwell_dir)
20
+
21
+ * Shotwell event and photo information is available via extended attributes and thus easier to parse
22
+ regardless of filetype
23
+
24
+ For more advanced usage, including controlling how events are mapped to directories, see
25
+
26
+ $ shotwellfs -h
27
+
28
+ ## Contributing
29
+
30
+ 1. Fork it
31
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
32
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
33
+ 4. Push to the branch (`git push origin my-new-feature`)
34
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+ require 'yard'
3
+
4
+ YARD::Rake::YardocTask.new do |t|
5
+ # Need this because YardocTask does not read the gemspec
6
+ t.files = ['lib/**/*.rb', '-'] # optional
7
+ end
8
+
data/bin/shotwellfs ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'shotwellfs'
5
+
6
+ ShotwellFS.main(*ARGV)
7
+
@@ -0,0 +1,305 @@
1
+ require 'shotwellfs/transform'
2
+ require 'fusefs/sqlitemapper'
3
+ require 'date'
4
+ require 'set'
5
+ require 'fileutils'
6
+ require 'ffi-xattr'
7
+ require 'sys/filesystem'
8
+ require 'rb-inotify'
9
+
10
+ module ShotwellFS
11
+
12
+ # A Fuse filesystem over a shotwell picture/video library
13
+ class FileSystem < FuseFS::SqliteMapperFS
14
+
15
+ def self.source_type(value)
16
+ value.start_with?("video") ? "video" : "photo"
17
+ end
18
+
19
+ def self.source_id(value)
20
+ value[6..-1].hex
21
+ end
22
+
23
+
24
+ Event = Struct.new('Event', :id, :path, :time, :xattr)
25
+
26
+ SHOTWELL_SQL = <<-SQL
27
+ SELECT P.rating as 'rating', P.exposure_time as 'exposure_time',
28
+ P.title as 'title', P.comment as 'comment', P.filename as 'filename', P.id as 'id',
29
+ P.event_id as 'event_id', "photo" as 'type', P.transformations as 'transformations'
30
+ FROM phototable P
31
+ WHERE P.rating >= %1$d and P.event_id > 0
32
+ UNION
33
+ SELECT V.rating, V.exposure_time as 'exposure_time',
34
+ V.title, V.comment, V.filename as 'filename', V.id as 'id',
35
+ V.event_id as 'event_id', "video" as 'type', null
36
+ FROM videotable V
37
+ WHERE V.rating >= %1$d and V.event_id > 0
38
+ SQL
39
+
40
+ TAG_SQL = <<-SQL
41
+ select name,photo_id_list
42
+ FROM tagtable
43
+ SQL
44
+
45
+ EVENT_SQL = <<-SQL
46
+ SELECT E.id as 'id', E.name as 'name', E.comment as 'comment', P.exposure_time as 'exposure_time'
47
+ FROM eventtable E, phototable P
48
+ WHERE source_type(E.primary_source_id) = 'photo'
49
+ AND source_id(E.primary_source_id) = P.id
50
+ UNION
51
+ SELECT E.id, E.name, E.comment, V.exposure_time
52
+ FROM eventtable E, videotable V
53
+ WHERE source_type(E.primary_source_id) = 'video'
54
+ AND source_id(E.primary_source_id) = V.id
55
+ SQL
56
+
57
+ XATTR_TRANSFORM_ID = 'user.shotwell.transform_id'
58
+
59
+ def initialize(options)
60
+ @shotwell_dir = options[:device]
61
+ shotwell_db = "#{@shotwell_dir}/data/photo.db"
62
+
63
+ # Default event name if it is not set - date expression based on exposure time of primary photo
64
+ @event_name = options[:event_name] || "%d %a"
65
+
66
+ # Event to path conversion.
67
+ # id, name, comment
68
+ # via event.exposure_time.strftime() and sprintf(format,event)
69
+ @event_path = options[:event_path] || "%Y-%m %{name}"
70
+
71
+ # File names (without extension
72
+ @photo_path = options[:photo_path] || "%{id}"
73
+
74
+ @video_path = options[:video_path] || @photo_path
75
+
76
+ example_event = { id: 1, name: "<event name>", comment: "<event comment>", exposure_time: 0}
77
+
78
+ example_photo = { id: 1000, title: "<photo title>", comment: "<photo comment>",
79
+ rating:5, exposure_time: Time.now.to_i, filename: "photo.jpg", type: "photo" }
80
+ example_video = { id: 1000, title: "<photo title>", comment: "<photo comment>",
81
+ rating:5, exposure_time: Time.now.to_i, filename: "video.mp4", type: "video" }
82
+
83
+ event_path = event_path(example_event)
84
+
85
+ puts "Mapping paths as\n#{file_path(event_path,example_photo)}\n#{file_path(event_path,example_video)}"
86
+
87
+ min_rating = options[:rating] || 0
88
+
89
+ sql = sprintf(SHOTWELL_SQL,min_rating)
90
+
91
+ super(shotwell_db,sql,use_raw_file_access: true)
92
+ end
93
+
94
+ def transforms_dir
95
+ unless @transforms_dir
96
+ @transforms_dir = "#{@shotwell_dir}/fuse/transforms"
97
+ FileUtils.mkdir_p(@transforms_dir) unless File.directory?(@transforms_dir)
98
+ end
99
+ @transforms_dir
100
+ end
101
+
102
+ def transform_required?(filename,transform_id)
103
+ !(File.exists?(filename) && transform_id.eql?(Xattr.new(filename)[XATTR_TRANSFORM_ID]))
104
+ end
105
+
106
+ def transform(row)
107
+ if row[:transformations]
108
+
109
+ transformations = Transform.new(row[:transformations])
110
+
111
+ transform_id = transformations.generate_id(row[:id])
112
+ filename = "#{transforms_dir}/#{row[:id]}.jpg"
113
+
114
+ if transform_id
115
+
116
+ if transform_required?(filename,transform_id)
117
+
118
+ puts "Generating transform for #{row[:filename]}"
119
+ puts "Writing to #{filename} with id #{transform_id}"
120
+ puts transformations
121
+
122
+ transformations.apply(row[:filename],filename)
123
+
124
+ xattr = Xattr.new(filename)
125
+ xattr[XATTR_TRANSFORM_ID] = transform_id
126
+ end
127
+
128
+ return [ transform_id, filename ]
129
+ end
130
+ end
131
+ # Ho transforms
132
+ [ row[:id],row[:filename] ]
133
+ end
134
+
135
+ def map_row(row)
136
+ row = symbolize(row)
137
+ xattr = file_xattr(row)
138
+
139
+ transform_id, real_file = transform(row)
140
+ xattr[XATTR_TRANSFORM_ID] = transform_id.to_s
141
+
142
+ path = file_path(@events[row[:event_id]].path,row)
143
+
144
+ options = { :exposure_time => row[:exposure_time], :event_id => row[:event_id], :xattr => xattr }
145
+ [ real_file, path, options ]
146
+ end
147
+
148
+ def scan
149
+ db.create_function("source_type",1) do |func, value|
150
+ func.result = self.class.source_type(value)
151
+ end
152
+
153
+ db.create_function("source_id",1) do |func, value|
154
+ func.result = self.class.source_id(value)
155
+ end
156
+
157
+ load_keywords
158
+ load_events
159
+
160
+ puts "Scan ##{scan_id} Finding images and photos for #{@events.size} events"
161
+ super
162
+ @keywords= nil
163
+ @events = nil
164
+ end
165
+
166
+
167
+ # override default time handling for pathmapper
168
+ def times(path)
169
+ possible_file = node(path)
170
+ tm = possible_file ? possible_file[:exposure_time] : nil
171
+
172
+ #set mtime and ctime to the exposure time
173
+ return tm ? [0, tm, tm] : [0, 0, 0]
174
+ end
175
+
176
+ def statistics(path)
177
+ df_path = unmap(path) || @shotwell_dir
178
+ df = Sys::Filesystem.stat(df_path)
179
+ stats.to_statistics(df.blocks_available * df.block_size, df.files_free)
180
+ end
181
+
182
+ def mounted()
183
+ super
184
+ start_notifier()
185
+ end
186
+
187
+ def unmounted()
188
+ stop_notifier()
189
+ super
190
+ end
191
+
192
+
193
+ private
194
+
195
+ attr_reader :events,:keywords
196
+
197
+ def event_path(event)
198
+ event_time = Time.at(event[:exposure_time])
199
+ event[:name] ||= Time.at(event_time).strftime(@event_name)
200
+ return sprintf(event_time.strftime(@event_path),event)
201
+ end
202
+
203
+ def event_xattr(event)
204
+ xattr = {}
205
+ xattr['user.shotwell.event_id'] = event[:id].to_s
206
+ xattr['user.shotwell.event_name'] = event[:name] || ""
207
+ xattr['user.shotwell.event_comment'] = event[:comment] || ""
208
+ return xattr
209
+ end
210
+
211
+ def file_path(event_path,image)
212
+ ext = File.extname(image[:filename]).downcase
213
+
214
+ format = image['type'] == 'photo' ? @photo_path : @video_path
215
+
216
+ filename = sprintf(Time.at(image[:exposure_time]).strftime(format),image)
217
+
218
+ return "#{event_path}/#{filename}#{ext}"
219
+ end
220
+
221
+ def file_xattr(image)
222
+ xattr = { }
223
+ xattr['user.shotwell.title'] = image[:title] || ""
224
+ xattr['user.shotwell.comment'] = image[:comment] || ""
225
+ xattr['user.shotwell.id'] = image[:id].to_s
226
+
227
+ keywords = @keywords[image[:type]][image[:id]]
228
+ xattr['user.shotwell.keywords'] = keywords.to_a.join(",")
229
+ xattr['user.shotwell.rating'] = image[:rating].to_s
230
+ return xattr
231
+ end
232
+
233
+ def symbolize(row)
234
+ Hash[row.map{ |k, v| [(k.respond_to?(:to_sym) ? k.to_sym : k), v] }]
235
+ end
236
+
237
+ def load_events
238
+ @events = {}
239
+ db.execute(EVENT_SQL) do |row|
240
+ row = symbolize(row)
241
+ id = row[:id]
242
+ path = event_path(row)
243
+ time = row[:exposure_time]
244
+
245
+ xattr = event_xattr(row)
246
+
247
+ @events[id] = Event.new(id,path,time,xattr)
248
+ end
249
+ end
250
+
251
+ def load_keywords
252
+ @keywords = {}
253
+ @keywords['video'] = Hash.new() { |h,k| h[k] = Set.new() }
254
+ @keywords['photo'] = Hash.new() { |h,k| h[k] = Set.new() }
255
+
256
+ db.execute(TAG_SQL) do |row|
257
+ name = row['name']
258
+ photo_list = row['photo_id_list']
259
+ next unless photo_list
260
+ # just use the last entry on the tag path
261
+ slash = name.rindex('/')
262
+
263
+ tag = slash ? name[slash+1..-1] : name
264
+ photo_list.split(",").each do |item|
265
+ type = self.class.source_type(item)
266
+ id = self.class.source_id(item)
267
+ @keywords[type][id] << tag
268
+ end
269
+ end
270
+ nil
271
+ end
272
+
273
+ def map_file(*args)
274
+ node = super
275
+ parent = node.parent
276
+ unless parent[:sw_scan_id] == scan_id
277
+ event_id = node[:event_id]
278
+ event = @events[event_id]
279
+ parent[:xattr] = event.xattr
280
+ parent[:exposure_time] = event.time
281
+ parent[:sw_scan_id] = scan_id
282
+ end
283
+ node
284
+ end
285
+
286
+ def start_notifier
287
+ @notifier ||= INotify::Notifier.new()
288
+ modified = false
289
+ @notifier.watch(db_path,:modify,:close_write) do |event|
290
+ modified = modified || event.flags.include?(:modify)
291
+ if modified && event.flags.include?(:close_write)
292
+ puts "calling rescan"
293
+ rescan
294
+ puts "rescanned"
295
+ modified = false
296
+ end
297
+ end
298
+ Thread.new { @notifier.run }
299
+ end
300
+
301
+ def stop_notifier
302
+ @notifier.stop() if @notifier
303
+ end
304
+ end
305
+ end
@@ -0,0 +1,183 @@
1
+ require 'RMagick'
2
+ require 'digest/md5'
3
+ require 'iniparse'
4
+ module ShotwellFS
5
+
6
+ class Transform
7
+
8
+ # Bump this if the transformation code changes in a way
9
+ # that would change the images
10
+ VERSION = 1
11
+
12
+ attr_reader :crop,:redeye
13
+
14
+ class RedEye
15
+ attr_reader :eyes
16
+
17
+ Eye = Struct.new(:x,:y,:radius) do
18
+ def to_s()
19
+ "#{x},#{y}+#{radius}"
20
+ end
21
+ end
22
+
23
+
24
+ def initialize(section)
25
+ @eyes = []
26
+ num_points = section['num_points'].to_i
27
+ 0.upto(num_points - 1) do |point|
28
+ radius = section["radius#{point}"].to_i
29
+ center = section["center#{point}"]
30
+ match = /\((\d+),\s*(\d+)\)/.match(center)
31
+ @eyes << Eye.new(match[1].to_i,match[2].to_i,radius)
32
+ end
33
+ end
34
+
35
+ def to_s
36
+ "RedEye(#{@eyes.join(',')})"
37
+ end
38
+
39
+ #convert.exe before.jpg -region "230x140+60+130" ^
40
+ #-fill black ^
41
+ #-fuzz 25%% ^
42
+ #-opaque rgb("192","00","10")
43
+ def apply(image)
44
+ image.view(0,0,image.columns,image.rows) do |view|
45
+ eyes.each { |eye| do_redeye(eye,view) }
46
+ end
47
+ end
48
+
49
+ # This algorithm ported directly from shotwell.
50
+ def do_redeye(eye,pixbuf)
51
+
52
+ #
53
+ # we remove redeye within a circular region called the "effect
54
+ #extent." the effect extent is inscribed within its "bounding
55
+ #rectangle." */
56
+
57
+ # /* for each scanline in the top half-circle of the effect extent,
58
+ # compute the number of pixels by which the effect extent is inset
59
+ #from the edges of its bounding rectangle. note that we only have
60
+ #to do this for the first quadrant because the second quadrant's
61
+ #insets can be derived by symmetry */
62
+ r = eye.radius
63
+ x_insets_first_quadrant = Array.new(eye.radius + 1)
64
+
65
+ i = 0
66
+ r.step(0,-1) do |y|
67
+ theta = Math.asin(y.to_f / r)
68
+ x = (r.to_f * Math.cos(theta) + 0.5).to_i
69
+ x_insets_first_quadrant[i] = eye.radius - x
70
+ i = i + 1
71
+ end
72
+
73
+ x_bounds_min = eye.x - eye.radius
74
+ x_bounds_max = eye.x + eye.radius
75
+ ymin = eye.y - eye.radius
76
+ ymin = (ymin < 0) ? 0 : ymin
77
+ ymax = eye.y
78
+ ymax = (ymax > (pixbuf.height - 1)) ? (pixbuf.height - 1) : ymax
79
+
80
+ #/* iterate over all the pixels in the top half-circle of the effect
81
+ #extent from top to bottom */
82
+ inset_index = 0
83
+ ymin.upto(ymax) do |y_it|
84
+ xmin = x_bounds_min + x_insets_first_quadrant[inset_index]
85
+ xmin = (xmin < 0) ? 0 : xmin
86
+ xmax = x_bounds_max - x_insets_first_quadrant[inset_index]
87
+ xmax = (xmax > (pixbuf.width - 1)) ? (pixbuf.width - 1) : xmax
88
+
89
+ xmin.upto(xmax) { |x_it| red_reduce_pixel(pixbuf,x_it, y_it) }
90
+ inset_index += 1
91
+ end
92
+
93
+ #/* iterate over all the pixels in the bottom half-circle of the effect
94
+ #extent from top to bottom */
95
+ ymin = eye.y
96
+ ymax = eye.y + eye.radius
97
+ inset_index = x_insets_first_quadrant.length - 1
98
+ ymin.upto(ymax) do |y_it|
99
+ xmin = x_bounds_min + x_insets_first_quadrant[inset_index]
100
+ xmin = (xmin < 0) ? 0 : xmin
101
+ xmax = x_bounds_max - x_insets_first_quadrant[inset_index]
102
+ xmax = (xmax > (pixbuf.width - 1)) ? (pixbuf.width - 1) : xmax
103
+
104
+ xmin.upto(xmax) { |x_it| red_reduce_pixel(pixbuf,x_it, y_it) }
105
+ inset_index -= 1
106
+ end
107
+ end
108
+
109
+ def red_reduce_pixel(pixbuf,x,y)
110
+ #/* Due to inaccuracies in the scaler, we can occasionally
111
+ #* get passed a coordinate pair outside the image, causing
112
+ #* us to walk off the array and into segfault territory.
113
+ # * Check coords prior to drawing to prevent this... */
114
+ if ((x >= 0) && (y >= 0) && (x < pixbuf.width) && (y < pixbuf.height))
115
+
116
+ #/* The pupil of the human eye has no pigment, so we expect all
117
+ #color channels to be of about equal intensity. This means that at
118
+ #any point within the effects region, the value of the red channel
119
+ #should be about the same as the values of the green and blue
120
+ #channels. So set the value of the red channel to be the mean of the
121
+ #values of the red and blue channels. This preserves achromatic
122
+ #intensity across all channels while eliminating any extraneous flare
123
+ #affecting the red channel only (i.e. the red-eye effect). */
124
+ g = pixbuf[y][x].green
125
+ b = pixbuf[y][x].blue
126
+ pixbuf[y][x].red = (g + b) / 2
127
+ end
128
+ end
129
+ end
130
+
131
+ class Crop
132
+ attr_reader :x,:y,:width,:height
133
+ def initialize(section)
134
+ @x = section["left"].to_i
135
+ @y = section["top"].to_i
136
+ @width = section["right"].to_i - @x
137
+ @height = section["bottom"].to_i - @y
138
+ end
139
+
140
+ def to_s
141
+ "Crop(#{x},#{y},#{width},#{height})"
142
+ end
143
+
144
+ def apply(image)
145
+ image.crop!(x,y,width,height)
146
+ end
147
+ end
148
+
149
+ def initialize(transformations)
150
+ doc = IniParse.parse(transformations)
151
+ @crop = doc.has_section?("crop") ? Crop.new(doc["crop"]) : nil
152
+ @redeye = doc.has_section?("redeye") ? RedEye.new(doc["redeye"]) : nil
153
+ end
154
+
155
+ def has_transforms?
156
+ ( crop || redeye ) && true
157
+ end
158
+
159
+ def generate_id(photo_id)
160
+ if has_transforms?
161
+ Digest::MD5.hexdigest("#{photo_id}:#{self}")
162
+ else
163
+ nil
164
+ end
165
+ end
166
+
167
+ def apply(input,output)
168
+ image = Magick::Image.read(input)[0]
169
+ image.format = "JPG"
170
+
171
+ redeye.apply(image) if redeye
172
+ crop.apply(image) if crop
173
+
174
+ image.write(output)
175
+ image.destroy!
176
+ end
177
+
178
+ def to_s
179
+ xforms = [ redeye, crop ].reject { |x| x.nil? }.join("\n ")
180
+ "#{self.class.name}:#{VERSION}\n #{xforms}"
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,3 @@
1
+ module ShotwellFS
2
+ VERSION = "0.0.1RC0"
3
+ end
data/lib/shotwellfs.rb ADDED
@@ -0,0 +1,38 @@
1
+ require 'shotwellfs/version'
2
+ require 'rfusefs'
3
+ require 'shotwellfs/filesystem'
4
+
5
+ module ShotwellFS
6
+
7
+ OPTIONS = [ :rating, :event_name, :event_path, :photo_path, :video_path ]
8
+ OPTION_USAGE = <<-HELP
9
+
10
+ [shotwellfs: #{VERSION}]
11
+
12
+ -o rating=N only include photos and videos with this rating or greater (default 0)
13
+ -o event_name=FMT strftime format used to generate text for unnamed events (default "%d %a")
14
+ -o event_path=FMT strftime and sprintf format to generate path prefix for events.
15
+ Available event fields - id, name, comment
16
+ (default "%Y-%m %<name>s")
17
+ -o photo_path=FMT strftime and sprintf format to generate path for photo files
18
+ Available photo fields - id, title, comment, rating
19
+ (default "%<id>d")
20
+ -o video_path=FMT as above for video files. If not set, photo_path is used.
21
+
22
+ HELP
23
+
24
+ def self.main(*args)
25
+
26
+ FuseFS.main(args,OPTIONS,OPTION_USAGE,"path/to/shotwell_dir") do |options|
27
+ if options[:device] && File.exists?("#{options[:device]}/data/photo.db")
28
+ fs = FileSystem.new(options)
29
+ Signal.trap("HUP") { fs.rescan() }
30
+ fs
31
+ else
32
+ puts "shotwellfs: failed to access Shotwell photo database #{options[:device]}/data/photo.db"
33
+ nil
34
+ end
35
+ end
36
+ end
37
+
38
+ end
@@ -0,0 +1,30 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'shotwellfs/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "shotwellfs"
8
+ gem.version = ShotwellFS::VERSION
9
+ gem.authors = ["Grant Gardner"]
10
+ gem.email = ["grant@lastweekend.com.au"]
11
+ gem.description = %q{A Fuse filesystem to remap image files according to shotwell metadata}
12
+ gem.summary = %q{FUSE fileystem for Shotwell}
13
+ gem.homepage = "http://github.com/lwoggardner/shotwellfs"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_runtime_dependency("sqlite3","~>1.3")
21
+ gem.add_runtime_dependency("rfusefs",">=1.0.2")
22
+ gem.add_runtime_dependency("rb-inotify","~>0.9")
23
+ gem.add_runtime_dependency("rmagick","~>2.13")
24
+ gem.add_runtime_dependency("iniparse","~>1.1")
25
+ gem.add_runtime_dependency("ffi-xattr","~>0.0")
26
+ gem.add_runtime_dependency("sys-filesystem")
27
+ gem.add_development_dependency("yard")
28
+ gem.add_development_dependency("redcarpet")
29
+
30
+ end
metadata ADDED
@@ -0,0 +1,202 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: shotwellfs
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1RC0
5
+ prerelease: 5
6
+ platform: ruby
7
+ authors:
8
+ - Grant Gardner
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-06-20 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: sqlite3
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '1.3'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '1.3'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rfusefs
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: 1.0.2
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: 1.0.2
46
+ - !ruby/object:Gem::Dependency
47
+ name: rb-inotify
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: '0.9'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '0.9'
62
+ - !ruby/object:Gem::Dependency
63
+ name: rmagick
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: '2.13'
70
+ type: :runtime
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: '2.13'
78
+ - !ruby/object:Gem::Dependency
79
+ name: iniparse
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ~>
84
+ - !ruby/object:Gem::Version
85
+ version: '1.1'
86
+ type: :runtime
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ~>
92
+ - !ruby/object:Gem::Version
93
+ version: '1.1'
94
+ - !ruby/object:Gem::Dependency
95
+ name: ffi-xattr
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ~>
100
+ - !ruby/object:Gem::Version
101
+ version: '0.0'
102
+ type: :runtime
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ~>
108
+ - !ruby/object:Gem::Version
109
+ version: '0.0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: sys-filesystem
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ - !ruby/object:Gem::Dependency
127
+ name: yard
128
+ requirement: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ! '>='
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ type: :development
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ! '>='
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ - !ruby/object:Gem::Dependency
143
+ name: redcarpet
144
+ requirement: !ruby/object:Gem::Requirement
145
+ none: false
146
+ requirements:
147
+ - - ! '>='
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ type: :development
151
+ prerelease: false
152
+ version_requirements: !ruby/object:Gem::Requirement
153
+ none: false
154
+ requirements:
155
+ - - ! '>='
156
+ - !ruby/object:Gem::Version
157
+ version: '0'
158
+ description: A Fuse filesystem to remap image files according to shotwell metadata
159
+ email:
160
+ - grant@lastweekend.com.au
161
+ executables:
162
+ - shotwellfs
163
+ extensions: []
164
+ extra_rdoc_files: []
165
+ files:
166
+ - .gitignore
167
+ - Gemfile
168
+ - LICENSE.txt
169
+ - README.md
170
+ - Rakefile
171
+ - bin/shotwellfs
172
+ - lib/shotwellfs.rb
173
+ - lib/shotwellfs/filesystem.rb
174
+ - lib/shotwellfs/transform.rb
175
+ - lib/shotwellfs/version.rb
176
+ - shotwellfs.gemspec
177
+ homepage: http://github.com/lwoggardner/shotwellfs
178
+ licenses: []
179
+ post_install_message:
180
+ rdoc_options: []
181
+ require_paths:
182
+ - lib
183
+ required_ruby_version: !ruby/object:Gem::Requirement
184
+ none: false
185
+ requirements:
186
+ - - ! '>='
187
+ - !ruby/object:Gem::Version
188
+ version: '0'
189
+ required_rubygems_version: !ruby/object:Gem::Requirement
190
+ none: false
191
+ requirements:
192
+ - - ! '>'
193
+ - !ruby/object:Gem::Version
194
+ version: 1.3.1
195
+ requirements: []
196
+ rubyforge_project:
197
+ rubygems_version: 1.8.25
198
+ signing_key:
199
+ specification_version: 3
200
+ summary: FUSE fileystem for Shotwell
201
+ test_files: []
202
+ has_rdoc: