gallery_sync 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/Gemfile +4 -0
- data/README.md +1 -0
- data/Rakefile +3 -0
- data/gallery_sync.gemspec +28 -0
- data/lib/gallery_sync.rb +4 -0
- data/lib/gallery_sync/gallery_sync.rb +483 -0
- data/lib/gallery_sync/tasks.rb +36 -0
- data/lib/gallery_sync/version.rb +3 -0
- data/spec/gallery_sync_spec.rb +374 -0
- data/spec/resources/gal1/album1/album.yml +4 -0
- data/spec/resources/gal1/album1/p1.png +0 -0
- data/spec/resources/gal1/album1/p2.png +0 -0
- data/spec/resources/gal1/album1/p3.png +0 -0
- data/spec/resources/gal1/album2/p1.png +0 -0
- data/spec/resources/gal1/album2/p4.png +0 -0
- data/spec/resources/images/landscape.jpg +0 -0
- data/spec/resources/images/portrait.jpg +0 -0
- metadata +110 -0
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
not usable yet
|
data/Rakefile
ADDED
@@ -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
|
data/lib/gallery_sync.rb
ADDED
@@ -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,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
|
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: []
|