gallery_sync 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .env
3
+ .bundle
4
+ Gemfile.lock
5
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in gallery_sync.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1 @@
1
+ not usable yet
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ require "bundler/gem_tasks"
2
+ require "gallery_sync/tasks"
3
+ GallerySync::Tasks.install
@@ -0,0 +1,28 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "gallery_sync/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "gallery_sync"
7
+ s.version = GallerySync::VERSION
8
+ s.authors = ["Iwan Birrer"]
9
+ s.email = ["iwanbirrer@bluewin.ch"]
10
+ s.homepage = "https://github.com/ibirrer/gallery_sync"
11
+ s.summary = %q{Photo gallery syncing for Amazon S3, Dropbox}
12
+ s.description = %q{Synchronizes directory based photo galleries between Amazon S3, Dropbox, local file system. Built for the purpose of managing a photo gallery solely through Dropbox and publishing it trhough Amazon S3. No Database involved, all metdata (e.g album descriptions is kept in the filesystem.}
13
+
14
+
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ # runtime dependencies
22
+ s.add_runtime_dependency "dropbox-sdk"
23
+ s.add_runtime_dependency "rmagick", ">= 2.12.0"
24
+ s.add_runtime_dependency "aws-sdk"
25
+
26
+ # development dependencies
27
+ s.add_development_dependency "rspec"
28
+ end
@@ -0,0 +1,4 @@
1
+ require "gallery_sync/version"
2
+
3
+ module GallerySync
4
+ end
@@ -0,0 +1,483 @@
1
+ require 'dropbox_sdk'
2
+ require 'aws/s3'
3
+ require 'RMagick'
4
+ require 'benchmark'
5
+
6
+ module GallerySync
7
+ class Gallery
8
+ # options, :reload, to reload albums, returns a cached version otherwise.
9
+ def albums(options = {})
10
+ if @albums.nil? or options[:reload]
11
+ @albums = load_albums
12
+ end
13
+ @albums.sort! { |a, b|
14
+ rank_a = a.metadata.fetch('rank', 0)
15
+ rank_b = b.metadata.fetch('rank', 0)
16
+ date_a = a.metadata.fetch('date_from', Date.civil())
17
+ date_b = b.metadata.fetch('date_from', Date.civil())
18
+ if(rank_a == rank_b)
19
+ date_a <=> date_b
20
+ else
21
+ rank_a <=> rank_b
22
+ end
23
+ }.reverse!
24
+ @albums
25
+ end
26
+
27
+ def serialized_albums
28
+ ser_albums = []
29
+ albums.each { |album|
30
+ ser_album = Album.new(album.name)
31
+ ser_album.metadata = album.metadata
32
+ ser_album.album_photo = SerPhoto::value_of(album.album_photo)
33
+ ser_album.photos = []
34
+ album.photos.each { |photo|
35
+ ser_album.photos << SerPhoto::value_of(photo)
36
+ }
37
+ ser_albums << ser_album
38
+ }
39
+ ser_albums
40
+ end
41
+
42
+ def sync_to(dest_gallery)
43
+ src = self
44
+
45
+ src_albums = src.albums
46
+ dest_albums = dest_gallery.albums
47
+
48
+ to_be_deleted = dest_albums.map(&:name) - src_albums.map(&:name)
49
+ to_be_deleted.each { |album| dest_gallery.rm_album(album) }
50
+ src_albums.each { |album|
51
+ if(dest_gallery[album.name])
52
+ album.merge_to(dest_gallery[album.name], dest_gallery) if dest_gallery[album.name]
53
+ else
54
+ dest_gallery.upload_album(album)
55
+ # sync metadata
56
+ dest_gallery.update_album_metadata(album,album.metadata)
57
+ end
58
+ }
59
+ end
60
+
61
+ def [](album_name)
62
+ r = albums.select { |a| a.name == album_name }
63
+ r.empty? ? nil : r.first
64
+ end
65
+ end
66
+
67
+ class CacheableGallery < Gallery
68
+ def initialize(gallery)
69
+ @albums = gallery.serialized_albums()
70
+ end
71
+
72
+ def load_albums
73
+ @albums
74
+ end
75
+ end
76
+
77
+ class FileGallery < Gallery
78
+ def initialize(name, root_path)
79
+ @root_path = root_path
80
+ @name = name
81
+ end
82
+
83
+ def load_albums
84
+ albums = []
85
+ album_dirs = Dir.glob(File.join(@root_path,'*',''))
86
+ album_dirs.each { |album_dir|
87
+ album = Album.new(File.basename(album_dir))
88
+ metadata_file = File.join(album_dir, "album.yml")
89
+ album.load_metadata_from_yaml(File.open(metadata_file)) if File.exist?(metadata_file)
90
+ photos = Dir.glob(File.join(album_dir, "*.{png,jpg}")).select { |f| File.file?(f)}.map { |f|
91
+ FilePhoto.new(album,File.basename(f),f)
92
+ }
93
+ if !photos.empty?
94
+ albums << album
95
+ album.photos = photos
96
+ album.album_photo = photos[0]
97
+ end
98
+ }
99
+ albums
100
+ end
101
+
102
+ def rm_photo photo
103
+ photo.rm
104
+ end
105
+
106
+ def rm_album album_name
107
+ album = self[album_name]
108
+ album.photos.each { |p|
109
+ p.rm
110
+ }
111
+ end
112
+
113
+
114
+
115
+ def upload_album album
116
+ Dir.mkdir File.join(@root_path, album.name)
117
+ album.photos.each { |photo|
118
+ upload_photo(photo)
119
+ }
120
+ end
121
+
122
+ def upload_photo(photo)
123
+ path = File.join(@root_path, photo.album.name, photo.name)
124
+ File.open(path,'wb') {|io|io.write(photo.file)}
125
+ end
126
+
127
+ def update_album_metadata(album,metadata)
128
+ file = File.join(@root_path, album.name, "album.yml")
129
+ File.open(file, 'w') do |out|
130
+ YAML::dump(metadata, out)
131
+ end
132
+ end
133
+
134
+ def to_s
135
+ @root_path
136
+ end
137
+ end
138
+
139
+ class Album
140
+ attr_accessor :photos, :album_photo, :metadata
141
+ attr_reader :name
142
+
143
+ def initialize(name)
144
+ @name = name
145
+ end
146
+
147
+ def metadata()
148
+ if(@metadata.nil?)
149
+ @metadata = {'name' => name()}
150
+ end
151
+ @metadata
152
+ end
153
+
154
+ # loads metadata from a yaml string
155
+ # title, date_from, date_to
156
+ def load_metadata_from_yaml(file)
157
+ @metadata = {}
158
+ @metadata = YAML::load(file)
159
+ end
160
+
161
+ def get_merge_actions(dest_album)
162
+ r = {}
163
+ src_album = self
164
+ src_names = @photos.collect(&:name)
165
+ dest_names = dest_album.photos.collect(&:name)
166
+
167
+ to_add = src_names - dest_names
168
+ to_delete = dest_names - src_names
169
+
170
+ r[:add] = to_add.map { |name| src_album[name] }
171
+ r[:delete] = to_delete.map { |name| dest_album[name] }
172
+ return r
173
+ end
174
+
175
+ def merge_to(dest_album,dest_gallery)
176
+ merge_actions = get_merge_actions(dest_album)
177
+
178
+ # perform delete
179
+ merge_actions[:delete].each { |p|
180
+ dest_gallery.rm_photo p
181
+ }
182
+
183
+ # perform add
184
+ merge_actions[:add].each { |p|
185
+ dest_gallery.upload_photo(p)
186
+ }
187
+
188
+ # sync metadata
189
+ if(self.metadata != dest_album.metadata)
190
+ dest_gallery.update_album_metadata(dest_album,self.metadata)
191
+ end
192
+ end
193
+
194
+ def [](photo_name)
195
+ r = photos.select { |p| p.name == photo_name }
196
+ r.empty? ? nil : r.first
197
+ end
198
+
199
+ def to_s
200
+ @name
201
+ end
202
+ end
203
+
204
+ class SerPhoto < Struct.new(:name, :path, :thumb_url, :medium_url, :full_url)
205
+ def self.value_of(photo)
206
+ SerPhoto.new(photo.name,photo.path,photo.thumb_url,photo.medium_url,photo.full_url)
207
+ end
208
+ end
209
+
210
+ class Photo
211
+ attr_reader :album
212
+ def initialize(album, name)
213
+ @album = album
214
+ @name = name
215
+ end
216
+
217
+ def name
218
+ @name
219
+ end
220
+
221
+ def path
222
+ "#{@album.name}/#{@name}"
223
+ end
224
+
225
+ def ==(another_photo)
226
+ @name == another_photo.name
227
+ end
228
+
229
+ def to_s
230
+ @name
231
+ end
232
+ end
233
+
234
+ class FilePhoto < Photo
235
+ def initialize(album,name,filepath)
236
+ super(album,name)
237
+ @filepath = filepath
238
+ end
239
+
240
+ def rm
241
+ File.delete @filepath
242
+ end
243
+
244
+ def thumb_url
245
+ @filepath
246
+ end
247
+
248
+ def medium_url
249
+ @filepath
250
+ end
251
+
252
+ def full_url
253
+ @filepath
254
+ end
255
+
256
+ def to_s
257
+ "#{name} (#{@filepath})"
258
+ end
259
+
260
+ def file
261
+ File.open(@filepath,"rb") {|io| io.read}
262
+ end
263
+ end
264
+
265
+ class DropboxPhoto < Photo
266
+ def initialize(album,name,dropbox_path,gallery)
267
+ super(album,name)
268
+ @dropbox_path = dropbox_path
269
+ @gallery = gallery
270
+ end
271
+
272
+ def db_path
273
+ @dropbox_path
274
+ end
275
+
276
+ def file
277
+ file = @gallery.client.get_file(@dropbox_path)
278
+ return file
279
+ end
280
+ end
281
+
282
+ class S3Photo < Photo
283
+ def initialize(album,name,s3_path, gallery)
284
+ super(album,name)
285
+ @s3_path = s3_path
286
+ @gallery = gallery
287
+ end
288
+
289
+ def s3_path
290
+ @s3_path
291
+ end
292
+
293
+ def thumb_url
294
+ key = album.name + "/thumb/" + name
295
+ @gallery.bucket.objects[key].public_url(:secure => false).to_s
296
+ end
297
+
298
+ def medium_url
299
+ key = album.name + "/medium/" + name
300
+ @gallery.bucket.objects[key].public_url(:secure => false).to_s
301
+ end
302
+
303
+ def full_url
304
+ key = album.name + "/full/" + name
305
+ @gallery.bucket.objects[key].public_url(:secure => false).to_s
306
+ end
307
+ end
308
+
309
+ class DropboxSource < Gallery
310
+
311
+ # Creates a dropbox source.
312
+ # @param [String] root the dropbox root directory of albums,
313
+ # given as an absolute directory relative to the dropbox
314
+ # root directry. Must start with a slash ('/'), e.g.
315
+ # /Photos/mygallery Directories are handled as albums.
316
+ def initialize(root, app_key, app_secret, user_key, user_secret)
317
+ @root = root
318
+ session = DropboxSession.new(app_key,app_secret)
319
+ session.set_access_token(user_key,user_secret)
320
+ session.assert_authorized
321
+ @client = DropboxClient.new(session,:dropbox)
322
+ end
323
+
324
+ def client
325
+ @client
326
+ end
327
+
328
+ def getFile photo
329
+ @client.get_file(@root + photo.db_path)
330
+ end
331
+
332
+ def load_albums
333
+ albums = []
334
+ # retrieve all albums
335
+ @client.metadata(@root)['contents'].map { |c| c['path'] if c['is_dir'] }.each { |dir|
336
+ # retrieve all photos
337
+ photos = []
338
+ album = Album.new(File.basename(dir))
339
+ @client.metadata(dir)['contents'].map { |c| c['path'] unless c['is_dir'] }.each { |file|
340
+ if(file =~ /(\.png|\.jpg)\z/i)
341
+ photos << DropboxPhoto.new(album, File.basename(file), file, self)
342
+ end
343
+ if(file =~ /album\.yml\z/)
344
+ album.load_metadata_from_yaml(@client.get_file(file))
345
+ end
346
+ }
347
+
348
+ # only add album if it contains at least one photo
349
+ if( !photos.empty? )
350
+ album.photos = photos
351
+ album.album_photo = photos[0]
352
+ albums << album
353
+ end
354
+ }
355
+ albums
356
+ end
357
+ end
358
+
359
+ class S3Destination < Gallery
360
+ def initialize(bucket_name,s3_key,s3_secret)
361
+ @con = AWS::S3::new(
362
+ :access_key_id => s3_key,
363
+ :secret_access_key => s3_secret
364
+ )
365
+ @bucket = @con.buckets.create bucket_name
366
+ end
367
+
368
+ def bucket
369
+ @bucket
370
+ end
371
+
372
+ def load_albums
373
+ albums = []
374
+
375
+ dirs = @bucket.as_tree.children.select(&:branch?)
376
+ dirs.each { |dir|
377
+ photos = []
378
+ album = Album.new(File.basename(dir.prefix))
379
+ metadata = @bucket.objects[File.join(dir.prefix,'album.yml')]
380
+ if(metadata.exists?)
381
+ album.load_metadata_from_yaml(metadata.read)
382
+ end
383
+
384
+ dir = dir.children.select(&:branch?)[0]
385
+ if (dir)
386
+ dir.children.select(&:leaf?).each { |leaf|
387
+ if( !leaf.key.end_with?("/") )
388
+ photos << S3Photo.new(album, File.basename(leaf.key), leaf.key, self)
389
+ end
390
+ }
391
+ end
392
+
393
+ # only add album if it contains at least one photo
394
+ if( !photos.empty? )
395
+ album.photos = photos
396
+ album.album_photo = photos[0]
397
+ albums << album
398
+ end
399
+ }
400
+ albums
401
+ end
402
+
403
+ def rm_album album_name
404
+ album = self[album_name]
405
+ album.photos.each { |p|
406
+ rm_photo p
407
+ }
408
+ end
409
+
410
+ def rm_photo photo
411
+ key_thumb = photo.album.name + "/thumb/" + photo.name
412
+ key_medium = photo.album.name + "/medium/" + photo.name
413
+ key_full = photo.album.name + "/full/" + photo.name
414
+ @bucket.objects[key_thumb].delete
415
+ @bucket.objects[key_medium].delete
416
+ @bucket.objects[key_full].delete
417
+ end
418
+
419
+ def upload_album album
420
+ album.photos.each { |photo|
421
+ upload_photo(photo)
422
+ }
423
+ end
424
+
425
+ def upload_photo(photo)
426
+ key_thumb = photo.album.name + "/thumb/" + photo.name
427
+ key_medium = photo.album.name + "/medium/" + photo.name
428
+ key_full = photo.album.name + "/full/" + photo.name
429
+
430
+ photos = ImageScaler.scale_image(photo.file)
431
+
432
+ @bucket.objects.create(key_thumb, :data => photos[:thumb], :acl => :public_read)
433
+ @bucket.objects.create(key_medium, :data => photos[:medium], :acl => :public_read)
434
+ #@bucket.objects.create(key_full, :data => photos[:full], :acl => :public_read)
435
+ end
436
+
437
+ def update_album_metadata(album,metadata)
438
+ metadata_key = album.name + "/album.yml"
439
+ @bucket.objects.create(metadata_key, :data => YAML::dump(metadata), :acl => :public_read)
440
+ end
441
+
442
+ def to_s
443
+ @bucket.name
444
+ end
445
+ end
446
+
447
+
448
+ class ImageScaler
449
+ # @param image an in-memory image
450
+ # @returns thre different versions of the
451
+ # given photo, thumb, medium and full.
452
+ def self.scale_image(image)
453
+ r = {}
454
+
455
+ magick_image = Magick::Image.from_blob(image).first
456
+ magick_image.auto_orient!
457
+
458
+ # thumb (strip exif metadata to decrease size)
459
+ # ^ means minimum size
460
+ r[:thumb] = magick_image.change_geometry('100x100^') { |w,h,img|
461
+ resized = img.resize(w,h)
462
+ if(w > h)
463
+ # landscape
464
+ resized.crop((w-h)/2,0,h,h)
465
+ else
466
+ # portrait
467
+ resized.crop(0,0,w,w)
468
+ end
469
+ }.strip!.to_blob {
470
+ self.quality = 80
471
+ }
472
+
473
+ # medium
474
+ r[:medium] = magick_image.change_geometry('460x460') { |cols, rows, img|
475
+ img.resize(cols, rows)
476
+ }.to_blob { self.quality = 85 }
477
+
478
+ # full
479
+ r[:full] = image
480
+ return r
481
+ end
482
+ end
483
+ end
@@ -0,0 +1,36 @@
1
+ require 'dropbox_sdk'
2
+ module GallerySync
3
+ class Tasks
4
+ extend Rake::DSL if defined? Rake::DSL
5
+ def self.install
6
+ namespace :gallery do
7
+ desc "Authorize Dropbpox"
8
+ task :authorize_dropbox do
9
+ # prompt app key and secret
10
+ print "Enter dropbox app key: "
11
+ app_key = $stdin.gets.chomp
12
+ print "Enter dropbox app secret: "
13
+ app_secret = $stdin.gets.chomp
14
+
15
+ # authorize user
16
+ session = DropboxSession.new(app_key,app_secret)
17
+ session.get_request_token
18
+ authorize_url = session.get_authorize_url
19
+ puts "AUTHORIZING"
20
+ puts authorize_url
21
+ print "Please visit that website and hit 'Allow', then hit Enter here."
22
+ $stdin.gets.chomp
23
+
24
+ # print configuration
25
+ access_token = session.get_access_token
26
+ puts "\nAdd the following configuration to your .env file:\n\n"
27
+ puts "GALLERY_SYNC_DROPBOX_APP_KEY=#{app_key}"
28
+ puts "GALLERY_SYNC_DROPBOX_APP_SECRET=#{app_secret}"
29
+ puts "GALLERY_SYNC_DROPBOX_USER_KEY=#{access_token.key}"
30
+ puts "GALLERY_SYNC_DROPBOX_USER_SECRET=#{access_token.secret}"
31
+ puts "\n"
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,3 @@
1
+ module GallerySync
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,374 @@
1
+ require 'gallery_sync/gallery_sync'
2
+ require 'tmpdir'
3
+
4
+ module GallerySync
5
+
6
+ describe GallerySync do
7
+
8
+ CONFIG_KEYS = [
9
+ 'GALLERY_SYNC_S3_KEY',
10
+ 'GALLERY_SYNC_S3_SECRET',
11
+ 'GALLERY_SYNC_DROPBOX_APP_KEY',
12
+ 'GALLERY_SYNC_DROPBOX_APP_KEY',
13
+ 'GALLERY_SYNC_DROPBOX_APP_SECRET',
14
+ 'GALLERY_SYNC_DROPBOX_USER_KEY',
15
+ 'GALLERY_SYNC_DROPBOX_USER_SECRET']
16
+
17
+ before (:all) do
18
+ env_file = File.expand_path('../../.env', __FILE__)
19
+ if File.exists? env_file
20
+ env_values = Hash[IO.read(env_file).split.collect {|line| line.split('=')}]
21
+ (CONFIG_KEYS).each { | key |
22
+ ENV[key] = env_values[key]
23
+ }
24
+ end
25
+
26
+ missing_config_keys = CONFIG_KEYS - ENV.keys
27
+ raise "missing config key(s): #{missing_config_keys}. Check #{env_file}" unless missing_config_keys.empty?
28
+
29
+ @dropbox_empty = DropboxSource.new("/Photos/rubytest/empty",
30
+ ENV['GALLERY_SYNC_DROPBOX_APP_KEY'],
31
+ ENV['GALLERY_SYNC_DROPBOX_APP_SECRET'],
32
+ ENV['GALLERY_SYNC_DROPBOX_USER_KEY'],
33
+ ENV['GALLERY_SYNC_DROPBOX_USER_SECRET'] )
34
+
35
+ @src = DropboxSource.new("/Photos/rubytest/test1",
36
+ ENV['GALLERY_SYNC_DROPBOX_APP_KEY'],
37
+ ENV['GALLERY_SYNC_DROPBOX_APP_SECRET'],
38
+ ENV['GALLERY_SYNC_DROPBOX_USER_KEY'],
39
+ ENV['GALLERY_SYNC_DROPBOX_USER_SECRET'] )
40
+ end
41
+
42
+ describe DropboxSource, "empty dropbox directory" do
43
+ it "should return no albums" do
44
+ @dropbox_empty.albums.should be_empty
45
+ end
46
+ end
47
+
48
+ describe DropboxSource, "for each non-empty dropbox directory" do
49
+ before(:all) do
50
+ @albums = @src.albums
51
+ end
52
+
53
+ it "an album should be created" do
54
+ @albums.should have(2).items
55
+ end
56
+
57
+ it "the albums should have the correct name" do
58
+ @albums[0].name.should == "one"
59
+ end
60
+
61
+ it "each file in an album should be depicted as one photo" do
62
+ @albums[0].photos.should have(3).photos
63
+ end
64
+
65
+ it "each photo should contain the correct path" do
66
+ @albums[0].photos[0].db_path.should eq "/Photos/rubytest/test1/one/IMG_0225.jpg"
67
+ end
68
+
69
+ it "each photo should contain the correct path" do
70
+ @albums[0].photos[0].path.should eq "one/IMG_0225.jpg"
71
+ end
72
+
73
+ it "the first photo is the album photo" do
74
+ @albums[0].album_photo.should == @albums[0].photos[0]
75
+ end
76
+
77
+ it "should read metadata correctly" do
78
+ @src['one'].metadata['name'].should == "Album 1"
79
+ @src['one'].metadata['date_from'].should == Date.parse('2011-09-09')
80
+ @src['one'].metadata['date_to'].should == Date.parse('2011-09-10')
81
+ end
82
+ end
83
+
84
+ describe Album do
85
+ before(:all) do
86
+ @album = Album.new("album")
87
+ photos = [Photo.new(@album, "p1"), Photo.new(@album,"p2")]
88
+ @album.photos = photos
89
+ @album.album_photo = photos[0]
90
+ end
91
+ it "path of photo must macht <gallery_name>/<photo_name>" do
92
+ @album.photos[0].path.should == "album/p1"
93
+ end
94
+ end
95
+
96
+ describe "DropboxToS3" do
97
+ before(:all) do
98
+ @albums = @src.albums
99
+ @dest = S3Destination.new("therech_rubytest_dropbox", ENV['GALLERY_SYNC_S3_KEY'], ENV['GALLERY_SYNC_S3_SECRET'])
100
+ end
101
+
102
+ it "should sync the gallery from dropbox to s3" do
103
+ @src.sync_to(@dest)
104
+ @dest.albums.size.should == @src.albums.size
105
+ @dest['one'].metadata.should == @src['one'].metadata
106
+ end
107
+ end
108
+
109
+ describe "FileToS3" do
110
+ before(:all) do
111
+ @gal1_dir = File.join(File.expand_path(File.dirname(__FILE__)),"resources/gal1")
112
+ @gal1 = FileGallery.new "foo", @gal1_dir
113
+ @dest = S3Destination.new("therech_rubytest_fromfile",ENV['GALLERY_SYNC_S3_KEY'], ENV['GALLERY_SYNC_S3_SECRET'])
114
+ end
115
+
116
+ it "should upload the complete album" do
117
+ @gal1.sync_to(@dest)
118
+ albums = @gal1.albums
119
+ albums.size.should == 2
120
+ album_names = @dest.albums.map(&:name)
121
+ album_names.size.should == 2
122
+ album_names.should include 'album1'
123
+ album_names.should include 'album2'
124
+ album_names.should_not include 'album3'
125
+ album_names.should_not include 'album4'
126
+ @dest["album2"].photos.size.should == 2
127
+ @dest["album1"].photos.size.should == 3
128
+ @dest["album1"].metadata.should == @gal1['album1'].metadata
129
+ end
130
+ end
131
+
132
+ describe S3Destination, "each directory on s3" do
133
+ before(:all) do
134
+ @dest = S3Destination.new("therech_rubytest",ENV['GALLERY_SYNC_S3_KEY'], ENV['GALLERY_SYNC_S3_SECRET'])
135
+ @albums = @dest.albums
136
+ end
137
+
138
+ it "accessor for album" do
139
+ @dest['one'].should_not be nil
140
+ @dest['two'].should_not be nil
141
+ @dest['four'].should be nil
142
+ end
143
+
144
+ it "should serialize albums correctly" do
145
+ ser_albums = @dest.serialized_albums()
146
+ albums = Marshal.load(Marshal.dump(ser_albums))
147
+ albums.size.should == 2
148
+ end
149
+
150
+ it "should represent an album" do
151
+ @albums.should have(2).items
152
+ end
153
+
154
+ it "should return albums in correct order" do
155
+ @albums[0].name.should == "one"
156
+ end
157
+
158
+ it "each file in an album should be depicted as one photo" do
159
+ @albums[0].photos.should have(3).photos
160
+ end
161
+
162
+ it "each photo should contain the correct path" do
163
+ @albums[0].photos[0].s3_path.should eq "one/full/IMG_0225.jpg"
164
+ end
165
+
166
+ it "each photo should contain the correct path" do
167
+ @albums[0].photos[0].path.should eq "one/IMG_0225.jpg"
168
+ end
169
+
170
+ it "the first photo is the album photo" do
171
+ @albums[0].album_photo.should == @albums[0].photos[0]
172
+ end
173
+
174
+ it "should have three versions of photos" do
175
+ @albums[0].photos[0].full_url.should match "one/full/IMG_0225.jpg"
176
+ @albums[0].photos[0].medium_url.should match "one/medium/IMG_0225.jpg"
177
+ @albums[0].photos[0].thumb_url.should match "one/thumb/IMG_0225.jpg"
178
+ end
179
+
180
+ it "should read metadata correctly" do
181
+ @dest['one'].metadata['name'].should == "Album 1"
182
+ @dest['one'].metadata['date_from'].should == Date.parse('2011-09-09')
183
+ @dest['one'].metadata['date_to'].should == Date.parse('2011-09-10')
184
+ end
185
+ end
186
+
187
+ # test gallery looks as follows:
188
+ # -gal1
189
+ # -album1
190
+ # -p1.png
191
+ # -p2.png
192
+ # -p3.png
193
+ # -album2
194
+ # -empty_dir
195
+ # -p1.png
196
+ # -p4.png
197
+ # -album3
198
+ describe FileGallery do
199
+ before(:all) do
200
+ @gal1_dir = File.join(File.expand_path(File.dirname(__FILE__)),"resources/gal1")
201
+ @gal1 = FileGallery.new "foo", @gal1_dir
202
+ end
203
+
204
+ it "should serialize albums correctly" do
205
+ ser_albums = @gal1.serialized_albums()
206
+ albums = Marshal.load(Marshal.dump(ser_albums))
207
+ albums.size.should == 2
208
+ end
209
+
210
+ it "empty albums must not be included" do
211
+ @gal1.albums.size.should == 2
212
+ end
213
+
214
+ it "albums should have the name of the directory" do
215
+ album_names = @gal1.albums.map(&:name)
216
+ album_names.should include 'album1'
217
+ album_names.should include 'album2'
218
+ album_names.should_not include 'album3'
219
+ end
220
+
221
+ it "directories in albums must be ignored" do
222
+ @gal1["album2"].photos.size.should == 2
223
+ @gal1["album1"].photos.size.should == 3
224
+ end
225
+
226
+ it "shuld read metadata correctly" do
227
+ @album1 = @gal1['album1']
228
+ metadata = @album1.metadata()
229
+ @album1.name.should == "album1"
230
+ metadata['name'].should == "Album 1"
231
+ metadata['date_from'].should == Date.parse('2011-09-09')
232
+ metadata['date_to'].should == Date.parse('2011-09-10')
233
+ end
234
+
235
+ # provides src and dest gallery through a block
236
+ def sync()
237
+ #dir = Dir.mktmpdir
238
+
239
+ Dir.mktmpdir{ |dir|
240
+ FileUtils.cp_r @gal1_dir, dir
241
+ dest = File.join(dir, "gal1")
242
+ File.delete File.join(dest, 'album1/p1.png')
243
+ File.delete File.join(dest, 'album1/album.yml')
244
+ FileUtils.cp File.join(@gal1_dir, 'album1/p1.png'), File.join(dest, 'album3')
245
+ FileUtils.cp File.join(@gal1_dir, 'album1/p2.png'), File.join(dest, 'album2')
246
+ Dir.mkdir File.join(dest, 'album4')
247
+ FileUtils.cp_r Dir.glob(File.join(@gal1_dir, 'album1', '/*')), File.join(dest, 'album4')
248
+
249
+ @gal2 = FileGallery.new "gal2", dest
250
+ @gal1.sync_to(@gal2)
251
+ @gal2 = FileGallery.new "gal2", dest
252
+ yield(@gal1,@gal2)
253
+ }
254
+ end
255
+
256
+ # destination album is created on the fly in temp directory
257
+ # content is as follows
258
+ # -gal1
259
+ # -album1
260
+ # -p2.png
261
+ # -p3.png
262
+ # -album2
263
+ # -empty_dir
264
+ # -p1.png
265
+ # -p2.png
266
+ # -p4.png
267
+ # -album3
268
+ # -p1.png
269
+ # -album4
270
+ # -p1.png
271
+ # -p2.png
272
+ # -p3.png
273
+ it "sync_to" do
274
+ sync() { |src,dst|
275
+ album_names = dst.albums.map(&:name)
276
+ album_names.size.should == 2
277
+ album_names.should include 'album1'
278
+ album_names.should include 'album2'
279
+ album_names.should_not include 'album3'
280
+ album_names.should_not include 'album4'
281
+ dst["album2"].photos.size.should == 2
282
+ dst["album1"].photos.size.should == 3
283
+ }
284
+ end
285
+
286
+ it "should sync metadata" do
287
+ sync() { |src,dst|
288
+ src_album = src['album1']
289
+ dst_album = dst['album1']
290
+ dst_album.metadata.should == src_album.metadata
291
+ }
292
+ end
293
+
294
+ it "should sync full album when dest is empty" do
295
+ Dir.mktmpdir {|dir|
296
+ gal_dir = File.join(dir, "gal")
297
+ Dir.mkdir gal_dir
298
+ gal = FileGallery.new "gal", gal_dir
299
+
300
+ @gal1.sync_to(gal)
301
+ gal = FileGallery.new "gal", gal_dir
302
+ album_names = gal.albums.map(&:name)
303
+ album_names.size.should == 2
304
+ album_names.should include 'album1'
305
+ album_names.should include 'album2'
306
+ album_names.should_not include 'album3'
307
+ album_names.should_not include 'album4'
308
+ gal["album2"].photos.size.should == 2
309
+ gal["album1"].photos.size.should == 3
310
+ }
311
+ end
312
+ end
313
+
314
+ describe Album, "get_merge_actions" do
315
+ before(:all) do
316
+ @src = Album.new "album"
317
+ @dest = Album.new "album"
318
+
319
+ @src.photos = [
320
+ Photo.new(@src,"f1"),
321
+ Photo.new(@src, "f2"),
322
+ Photo.new(@src, "f3")]
323
+
324
+ @dest.photos = [
325
+ Photo.new(@dest,"f2"),
326
+ Photo.new(@dest, "f5")]
327
+ @diff = @src.get_merge_actions(@dest)
328
+ end
329
+
330
+ it "returns the difference between two path" do
331
+ @diff[:add].should == [Photo.new(@src,"f1"),Photo.new(@src,"f3")]
332
+ @diff[:delete].should == [Photo.new(@dest,"f5")]
333
+ end
334
+ end
335
+
336
+ describe Photo do
337
+ before(:all) do
338
+ @a1 = Album.new("a1")
339
+ @a2 = Album.new("a2")
340
+ end
341
+
342
+ it { Photo.new(@a1,"p1").should == Photo.new(@a2, "p1") }
343
+ it { Photo.new(@a1,"p1").should == Photo.new(@a1, "p1") }
344
+ it { Photo.new(@a1,"p1").should_not == Photo.new(@a1, "p2") }
345
+ end
346
+
347
+ describe ImageScaler do
348
+ describe "scale_image" do
349
+ it "should return 3 differently scaled versions" do
350
+ portrait = File.join(File.expand_path(File.dirname(__FILE__)),
351
+ "resources/images/portrait.jpg")
352
+ landscape = File.join(File.expand_path(File.dirname(__FILE__)),
353
+ "resources/images/landscape.jpg")
354
+
355
+ file_portrait = open(portrait, "rb") {|io| io.read }
356
+ file_landscape = open(landscape, "rb") {|io| io.read }
357
+ scaled_images_portrait = ImageScaler.scale_image(file_portrait)
358
+ scaled_images_landscape = ImageScaler.scale_image(file_landscape)
359
+ #p scaled_images[:thumb].size
360
+ #p scaled_images[:medium].size
361
+ #p scaled_images[:full].size
362
+
363
+ med_file = open("/tmp/portrait_thumb.jpg", "wb").write(scaled_images_portrait[:thumb])
364
+ med_file = open("/tmp/portrait_med.jpg", "wb").write(scaled_images_portrait[:medium])
365
+ med_file = open("/tmp/portrait_full.jpg", "wb").write(scaled_images_portrait[:full])
366
+
367
+ med_file = open("/tmp/landscape_thumb.jpg", "wb").write(scaled_images_landscape[:thumb])
368
+ med_file = open("/tmp/landscape_med.jpg", "wb").write(scaled_images_landscape[:medium])
369
+ med_file = open("/tmp/landscape_full.jpg", "wb").write(scaled_images_landscape[:full])
370
+ end
371
+ end
372
+ end
373
+ end
374
+ end
@@ -0,0 +1,4 @@
1
+ name: "Album 1"
2
+ date_from: 2011-09-09
3
+ date_to: 2011-09-10
4
+
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gallery_sync
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Iwan Birrer
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-03-11 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: dropbox-sdk
16
+ requirement: &15524860 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *15524860
25
+ - !ruby/object:Gem::Dependency
26
+ name: rmagick
27
+ requirement: &15524120 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: 2.12.0
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *15524120
36
+ - !ruby/object:Gem::Dependency
37
+ name: aws-sdk
38
+ requirement: &15523180 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *15523180
47
+ - !ruby/object:Gem::Dependency
48
+ name: rspec
49
+ requirement: &15521600 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *15521600
58
+ description: Synchronizes directory based photo galleries between Amazon S3, Dropbox,
59
+ local file system. Built for the purpose of managing a photo gallery solely through
60
+ Dropbox and publishing it trhough Amazon S3. No Database involved, all metdata (e.g
61
+ album descriptions is kept in the filesystem.
62
+ email:
63
+ - iwanbirrer@bluewin.ch
64
+ executables: []
65
+ extensions: []
66
+ extra_rdoc_files: []
67
+ files:
68
+ - .gitignore
69
+ - Gemfile
70
+ - README.md
71
+ - Rakefile
72
+ - gallery_sync.gemspec
73
+ - lib/gallery_sync.rb
74
+ - lib/gallery_sync/gallery_sync.rb
75
+ - lib/gallery_sync/tasks.rb
76
+ - lib/gallery_sync/version.rb
77
+ - spec/gallery_sync_spec.rb
78
+ - spec/resources/gal1/album1/album.yml
79
+ - spec/resources/gal1/album1/p1.png
80
+ - spec/resources/gal1/album1/p2.png
81
+ - spec/resources/gal1/album1/p3.png
82
+ - spec/resources/gal1/album2/p1.png
83
+ - spec/resources/gal1/album2/p4.png
84
+ - spec/resources/images/landscape.jpg
85
+ - spec/resources/images/portrait.jpg
86
+ homepage: https://github.com/ibirrer/gallery_sync
87
+ licenses: []
88
+ post_install_message:
89
+ rdoc_options: []
90
+ require_paths:
91
+ - lib
92
+ required_ruby_version: !ruby/object:Gem::Requirement
93
+ none: false
94
+ requirements:
95
+ - - ! '>='
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ required_rubygems_version: !ruby/object:Gem::Requirement
99
+ none: false
100
+ requirements:
101
+ - - ! '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ requirements: []
105
+ rubyforge_project:
106
+ rubygems_version: 1.8.17
107
+ signing_key:
108
+ specification_version: 3
109
+ summary: Photo gallery syncing for Amazon S3, Dropbox
110
+ test_files: []