gallery_sync 0.0.1

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,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: []