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