shotwellfs 0.0.1RC0

Sign up to get free protection for your applications and to get access to all the features.
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: