desoto-photoapp 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.travis.yml +3 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +48 -0
- data/Rakefile +1 -0
- data/assets/SourceSansPro-Semibold.ttf +0 -0
- data/assets/photos-action-installer.pkg +0 -0
- data/assets/watermark.png +0 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/exe/photoapp +39 -0
- data/lib/adjust-image.workflow/Contents/Info.plist +8 -0
- data/lib/adjust-image.workflow/Contents/QuickLook/Preview.png +0 -0
- data/lib/adjust-image.workflow/Contents/document.wflow +345 -0
- data/lib/desoto-photoapp.rb +106 -0
- data/lib/desoto-photoapp/photo.rb +99 -0
- data/lib/desoto-photoapp/s3.rb +275 -0
- data/lib/desoto-photoapp/version.rb +3 -0
- data/lib/import-photos.workflow/Contents/Info.plist +8 -0
- data/lib/import-photos.workflow/Contents/QuickLook/Thumbnail.png +0 -0
- data/lib/import-photos.workflow/Contents/document.wflow +346 -0
- data/photoapp.gemspec +27 -0
- metadata +153 -0
@@ -0,0 +1,106 @@
|
|
1
|
+
require "desoto-photoapp/version"
|
2
|
+
require "desoto-photoapp/photo"
|
3
|
+
require "desoto-photoapp/s3"
|
4
|
+
require 'yaml'
|
5
|
+
require 'colorator'
|
6
|
+
|
7
|
+
module Photoapp
|
8
|
+
class Session
|
9
|
+
attr_accessor :photos, :print, :upload
|
10
|
+
|
11
|
+
ROOT = File.expand_path('~/cave.pics') # where photos are stored
|
12
|
+
|
13
|
+
# relative to root
|
14
|
+
CONFIG_FILE = 'photoapp.yml'
|
15
|
+
UPLOAD = 'upload'
|
16
|
+
PRINT = 'print'
|
17
|
+
|
18
|
+
|
19
|
+
def initialize(options={})
|
20
|
+
@photos = []
|
21
|
+
@config = config(options)
|
22
|
+
end
|
23
|
+
|
24
|
+
def config(options={})
|
25
|
+
@config || begin
|
26
|
+
|
27
|
+
config = {
|
28
|
+
'source' => Dir.pwd, # where photos are located
|
29
|
+
'url_base' => 'www.cave.pics',
|
30
|
+
'watermark' => gem_dir('assets', 'watermark.png'),
|
31
|
+
'font' => gem_dir('assets', "SourceSansPro-Semibold.ttf"),
|
32
|
+
'font_size' => 30,
|
33
|
+
'config' => 'photoapp.yml',
|
34
|
+
'upload' => 'upload',
|
35
|
+
'print' => 'print'
|
36
|
+
}
|
37
|
+
|
38
|
+
config_file = root(options['config'] || config['config'])
|
39
|
+
|
40
|
+
config['source'] = options['source'] || config['source']
|
41
|
+
|
42
|
+
if File.exist?(config_file)
|
43
|
+
config.merge!(YAML.load(File.read(config_file)) || {})
|
44
|
+
end
|
45
|
+
|
46
|
+
config['upload'] = root(config['upload'])
|
47
|
+
config['print'] = root(config['print'])
|
48
|
+
|
49
|
+
config
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
def gem_dir(*paths)
|
55
|
+
File.expand_path(File.join(File.dirname(__FILE__), '..', *paths))
|
56
|
+
end
|
57
|
+
|
58
|
+
def root(path='')
|
59
|
+
File.expand_path(File.join(ROOT, path))
|
60
|
+
end
|
61
|
+
|
62
|
+
def process
|
63
|
+
logo = Magick::Image.read(config['watermark']).first
|
64
|
+
photos = []
|
65
|
+
tmp = root('.tmp')
|
66
|
+
FileUtils.mkdir_p(tmp)
|
67
|
+
|
68
|
+
if empty_print_queue?
|
69
|
+
FileUtils.rm_rf(config['print'])
|
70
|
+
end
|
71
|
+
|
72
|
+
load_photos.each do |f|
|
73
|
+
FileUtils.mv f, tmp
|
74
|
+
path = File.join(tmp, File.basename(f))
|
75
|
+
`automator -i #{path} #{gem_dir("lib/adjust-image.workflow")}`
|
76
|
+
photos << Photo.new(path, logo, self)
|
77
|
+
end
|
78
|
+
|
79
|
+
photos.each do |p|
|
80
|
+
p.write
|
81
|
+
p.add_to_photos
|
82
|
+
p.print
|
83
|
+
end
|
84
|
+
|
85
|
+
FileUtils.rm_rf tmp
|
86
|
+
end
|
87
|
+
|
88
|
+
def load_photos
|
89
|
+
files = ['*.jpg', '*.JPG', '*.JPEG', '*.jpeg'].map! { |f| File.join(config['source'], f) }
|
90
|
+
|
91
|
+
Dir[*files]
|
92
|
+
end
|
93
|
+
|
94
|
+
def empty_print_queue?
|
95
|
+
if printer = `lpstat -d`
|
96
|
+
printer = printer.scan(/:\s*(.+)/).flatten.first.strip
|
97
|
+
`lpstat -o -P #{printer}`.strip == ''
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def upload
|
102
|
+
S3.new(@config).push
|
103
|
+
FileUtils.rm_rf config['upload']
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'RMagick'
|
2
|
+
|
3
|
+
module Photoapp
|
4
|
+
class Photo
|
5
|
+
include Magick
|
6
|
+
attr_accessor :file, :logo, :image, :config, :session
|
7
|
+
|
8
|
+
def initialize(file, logo, session)
|
9
|
+
@file = file
|
10
|
+
@logo = logo
|
11
|
+
@session = session
|
12
|
+
@config = session.config
|
13
|
+
end
|
14
|
+
|
15
|
+
def config
|
16
|
+
@config
|
17
|
+
end
|
18
|
+
|
19
|
+
def image
|
20
|
+
@image ||= Image.read(file).first.resize_to_fill(2100, 1500, NorthGravity)
|
21
|
+
end
|
22
|
+
|
23
|
+
def watermark
|
24
|
+
@watermarked ||= image.composite(logo, SouthWestGravity, OverCompositeOp)
|
25
|
+
end
|
26
|
+
|
27
|
+
def with_url
|
28
|
+
@printable ||= begin
|
29
|
+
light_url = add_url("#fff")
|
30
|
+
dark_url = add_url("#000", true).blur_image(radius=6.0, sigma=2.0)
|
31
|
+
watermark.dup
|
32
|
+
.composite(dark_url, SouthEastGravity, OverCompositeOp)
|
33
|
+
.composite(light_url, SouthEastGravity, OverCompositeOp)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def add_url(color, stroke=false)
|
38
|
+
setting = config
|
39
|
+
image = Image.new(800,100) { self.background_color = "rgba(255, 255, 255, 0)" }
|
40
|
+
text = Draw.new
|
41
|
+
text.annotate(image, 0, 0, 60, 50, "#{setting['url_base']}/#{short}.jpg") do
|
42
|
+
text.gravity = SouthEastGravity
|
43
|
+
text.pointsize = setting['font_size']
|
44
|
+
text.fill = color
|
45
|
+
text.font = setting['font']
|
46
|
+
if stroke
|
47
|
+
text.stroke = color
|
48
|
+
end
|
49
|
+
end
|
50
|
+
image
|
51
|
+
end
|
52
|
+
|
53
|
+
def write
|
54
|
+
puts "writing #{upload_dest}"
|
55
|
+
puts "writing #{print_dest}"
|
56
|
+
FileUtils.mkdir_p(File.dirname(upload_dest))
|
57
|
+
FileUtils.mkdir_p(File.dirname(print_dest))
|
58
|
+
watermark.write upload_dest
|
59
|
+
with_url.write print_dest
|
60
|
+
cleanup
|
61
|
+
end
|
62
|
+
|
63
|
+
# Handle printing
|
64
|
+
def print
|
65
|
+
system "lpr #{print_dest}"
|
66
|
+
end
|
67
|
+
|
68
|
+
def add_to_photos
|
69
|
+
`automator -i #{config['print']} #{@session.gem_dir("lib/import-photos.workflow")}`
|
70
|
+
end
|
71
|
+
|
72
|
+
def cleanup
|
73
|
+
watermark.destroy!
|
74
|
+
with_url.destroy!
|
75
|
+
end
|
76
|
+
|
77
|
+
def upload_dest
|
78
|
+
File.join(config['upload'], short + '.jpg')
|
79
|
+
end
|
80
|
+
|
81
|
+
def print_dest
|
82
|
+
File.join(config['print'], short + '.jpg')
|
83
|
+
end
|
84
|
+
|
85
|
+
def short
|
86
|
+
@short ||= begin
|
87
|
+
now = Time.now
|
88
|
+
date = "#{now.strftime('%y')}#{now.strftime('%d')}#{now.month}"
|
89
|
+
source = [*?a..?z] - ['o', 'l'] + [*2..9]
|
90
|
+
short = ''
|
91
|
+
5.times { short << source.sample.to_s }
|
92
|
+
short = "#{short}#{date}"
|
93
|
+
session.photos << short + '.jpg'
|
94
|
+
short
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,275 @@
|
|
1
|
+
require 'find'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
module Photoapp
|
5
|
+
class S3
|
6
|
+
|
7
|
+
def initialize(options)
|
8
|
+
begin
|
9
|
+
require 'aws-sdk-v1'
|
10
|
+
rescue LoadError
|
11
|
+
abort "Deploying to S3 requires the aws-sdk-v1 gem. Install with `gem install aws-sdk-v1`."
|
12
|
+
end
|
13
|
+
@options = options
|
14
|
+
@local = File.expand_path(options['upload'] || 'upload')
|
15
|
+
@bucket_name = options['bucket_name']
|
16
|
+
@access_key = options['access_key_id'] || ENV['AWS_ACCESS_KEY_ID']
|
17
|
+
@secret_key = options['secret_access_key'] || ENV['AWS_SECRET_ACCESS_KEY']
|
18
|
+
@region = options['region'] || ENV['AWS_DEFAULT_REGION'] || 'us-east-1'
|
19
|
+
@distro_id = options['distribution_id'] || ENV['AWS_DISTRIBUTION_ID']
|
20
|
+
@remote_path = (options['remote_path'] || '/').sub(/^\//,'')
|
21
|
+
@verbose = options['verbose']
|
22
|
+
@incremental = options['incremental']
|
23
|
+
@delete = options['delete']
|
24
|
+
@headers = options['headers'] || []
|
25
|
+
@remote_path = @remote_path.sub(/^\//,'') # remove leading slash
|
26
|
+
@pull_dir = options['dir']
|
27
|
+
connect
|
28
|
+
end
|
29
|
+
|
30
|
+
def push
|
31
|
+
#abort "Seriously, you should. Quitting..." unless Deploy.check_gitignore
|
32
|
+
@bucket = @s3.buckets[@bucket_name]
|
33
|
+
if !@bucket.exists?
|
34
|
+
abort "Bucket not found: '#{@bucket_name}'. Check your configuration or create a bucket using: `octopress deploy add-bucket`"
|
35
|
+
else
|
36
|
+
puts "Syncing #{@local} files to #{@bucket_name} on S3."
|
37
|
+
write_files
|
38
|
+
delete_files if delete_files?
|
39
|
+
status_message
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def pull
|
44
|
+
@bucket = @s3.buckets[@bucket_name]
|
45
|
+
if !@bucket.exists?
|
46
|
+
abort "Bucket not found: '#{@bucket_name}'. Check your configuration or create a bucket using: `octopress deploy add-bucket`"
|
47
|
+
else
|
48
|
+
puts "Syncing from S3 bucket: '#{@bucket_name}' to #{@pull_dir}."
|
49
|
+
@bucket.objects.each do |object|
|
50
|
+
path = File.join(@pull_dir, object.key)
|
51
|
+
|
52
|
+
# Path is a directory, not a file
|
53
|
+
if path =~ /\/$/
|
54
|
+
FileUtils.mkdir_p(path) unless File.directory?(path)
|
55
|
+
else
|
56
|
+
dir = File.dirname(path)
|
57
|
+
FileUtils.mkdir_p(dir) unless File.directory?(dir)
|
58
|
+
File.open(path, 'w') { |f| f.write(object.read) }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Connect to S3 using the AWS SDK
|
65
|
+
# Retuns an aws bucket
|
66
|
+
#
|
67
|
+
def connect
|
68
|
+
AWS.config(access_key_id: @access_key, secret_access_key: @secret_key, region: @region)
|
69
|
+
@s3 = AWS.s3
|
70
|
+
@cloudfront = AWS.cloud_front.client
|
71
|
+
end
|
72
|
+
|
73
|
+
# Write site files to the selected bucket
|
74
|
+
#
|
75
|
+
def write_files
|
76
|
+
puts "Writing #{pluralize('file', site_files.size)}:" if @verbose
|
77
|
+
files_to_invalidate = []
|
78
|
+
site_files.each do |file|
|
79
|
+
s3_filename = remote_path(file)
|
80
|
+
o = @bucket.objects[s3_filename]
|
81
|
+
file_with_options = get_file_with_metadata(file, s3_filename);
|
82
|
+
|
83
|
+
begin
|
84
|
+
s3sum = o.etag.tr('"','') if o.exists?
|
85
|
+
rescue AWS::S3::Errors::NoSuchKey
|
86
|
+
s3sum = ""
|
87
|
+
end
|
88
|
+
|
89
|
+
if @incremental && (s3sum == Digest::MD5.file(file).hexdigest)
|
90
|
+
if @verbose
|
91
|
+
puts "= #{remote_path(file)}"
|
92
|
+
else
|
93
|
+
progress('=')
|
94
|
+
end
|
95
|
+
else
|
96
|
+
o.write(file_with_options)
|
97
|
+
files_to_invalidate.push(file)
|
98
|
+
if @verbose
|
99
|
+
puts "+ #{remote_path(file)}"
|
100
|
+
else
|
101
|
+
progress('+')
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
invalidate_cache(files_to_invalidate) unless @distro_id.nil?
|
107
|
+
end
|
108
|
+
|
109
|
+
def invalidate_cache(files)
|
110
|
+
puts "Invalidating cache for #{pluralize('file', site_files.size)}" if @verbose
|
111
|
+
@cloudfront.create_invalidation(
|
112
|
+
distribution_id: @distro_id,
|
113
|
+
invalidation_batch:{
|
114
|
+
paths:{
|
115
|
+
quantity: files.size,
|
116
|
+
items: files.map{|file| "/" + remote_path(file)}
|
117
|
+
},
|
118
|
+
# String of 8 random chars to uniquely id this invalidation
|
119
|
+
caller_reference: (0...8).map { ('a'..'z').to_a[rand(26)] }.join
|
120
|
+
}
|
121
|
+
) unless files.empty?
|
122
|
+
end
|
123
|
+
|
124
|
+
def get_file_with_metadata(file, s3_filename)
|
125
|
+
file_with_options = {
|
126
|
+
:file => file,
|
127
|
+
:acl => :public_read
|
128
|
+
}
|
129
|
+
|
130
|
+
@headers.each do |conf|
|
131
|
+
if conf.has_key? 'filename' and s3_filename.match(conf['filename'])
|
132
|
+
if @verbose
|
133
|
+
puts "+ #{remote_path(file)} matched pattern #{conf['filename']}"
|
134
|
+
end
|
135
|
+
|
136
|
+
if conf.has_key? 'expires'
|
137
|
+
expireDate = conf['expires']
|
138
|
+
|
139
|
+
relative_years = /^\+(\d+) year(s)?$/.match(conf['expires'])
|
140
|
+
if relative_years
|
141
|
+
expireDate = (Time.now + (60 * 60 * 24 * 365 * relative_years[1].to_i)).httpdate
|
142
|
+
end
|
143
|
+
|
144
|
+
relative_days = /^\+(\d+) day(s)?$/.match(conf['expires'])
|
145
|
+
if relative_days
|
146
|
+
expireDate = (Time.now + (60 * 60 * 24 * relative_days[1].to_i)).httpdate
|
147
|
+
end
|
148
|
+
|
149
|
+
file_with_options[:expires] = expireDate
|
150
|
+
end
|
151
|
+
|
152
|
+
if conf.has_key? 'content_type'
|
153
|
+
file_with_options[:content_type] = conf['content_type']
|
154
|
+
end
|
155
|
+
|
156
|
+
if conf.has_key? 'cache_control'
|
157
|
+
file_with_options[:cache_control] = conf['cache_control']
|
158
|
+
end
|
159
|
+
|
160
|
+
if conf.has_key? 'content_encoding'
|
161
|
+
file_with_options[:content_encoding] = conf['content_encoding']
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
return file_with_options
|
167
|
+
end
|
168
|
+
|
169
|
+
# Delete files from the bucket, to ensure a 1:1 match with site files
|
170
|
+
#
|
171
|
+
def delete_files
|
172
|
+
if deletable_files.size > 0
|
173
|
+
puts "Deleting #{pluralize('file', deletable_files.size)}:" if @verbose
|
174
|
+
deletable_files.each do |file|
|
175
|
+
@bucket.objects.delete(file)
|
176
|
+
if @verbose
|
177
|
+
puts "- #{file}"
|
178
|
+
else
|
179
|
+
progress('-')
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
# Create a new S3 bucket
|
186
|
+
#
|
187
|
+
def add_bucket
|
188
|
+
puts @bucket_name
|
189
|
+
@bucket = @s3.buckets.create(@bucket_name)
|
190
|
+
puts "Created new bucket '#{@bucket_name}' in region '#{@region}'."
|
191
|
+
configure_bucket
|
192
|
+
end
|
193
|
+
|
194
|
+
def configure_bucket
|
195
|
+
error_page = @options['error_page'] || remote_path('404.html')
|
196
|
+
index_page = @options['index_page'] || remote_path('index.html')
|
197
|
+
|
198
|
+
config = @bucket.configure_website do |cfg|
|
199
|
+
cfg.index_document_suffix = index_page
|
200
|
+
cfg.error_document_key = error_page
|
201
|
+
end
|
202
|
+
puts "Bucket configured with index_document: #{index_page} and error_document: #{error_page}."
|
203
|
+
end
|
204
|
+
|
205
|
+
def delete_files?
|
206
|
+
!!@delete
|
207
|
+
end
|
208
|
+
|
209
|
+
# local site files
|
210
|
+
def site_files
|
211
|
+
@site_files ||= Find.find(@local).to_a.reject do |f|
|
212
|
+
File.directory?(f)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
# Destination paths for local site files.
|
217
|
+
def site_files_dest
|
218
|
+
@site_files_dest ||= site_files.map{|f| remote_path(f) }
|
219
|
+
end
|
220
|
+
|
221
|
+
# Replace local path with remote path
|
222
|
+
def remote_path(file)
|
223
|
+
File.join(@remote_path, file.sub(@local, '')).sub(/^\//, '')
|
224
|
+
end
|
225
|
+
|
226
|
+
# Files from the bucket which are deletable
|
227
|
+
# Only deletes files beneath the remote_path if specified
|
228
|
+
def deletable_files
|
229
|
+
return [] unless delete_files?
|
230
|
+
unless @deletable
|
231
|
+
@deletable = @bucket.objects.map(&:key) - site_files_dest
|
232
|
+
@deletable.reject!{|f| (f =~ /^#{@remote_path}/).nil? }
|
233
|
+
end
|
234
|
+
@deletable
|
235
|
+
end
|
236
|
+
|
237
|
+
# List written and deleted file counts
|
238
|
+
def status_message
|
239
|
+
uploaded = site_files.size
|
240
|
+
deleted = deletable_files.size
|
241
|
+
|
242
|
+
message = "\nSuccess:".green + " #{uploaded} #{pluralize('file', uploaded)} uploaded"
|
243
|
+
message << ", #{deleted} #{pluralize('file', deleted)} deleted."
|
244
|
+
puts message
|
245
|
+
configure_bucket unless @bucket.website?
|
246
|
+
end
|
247
|
+
|
248
|
+
# Print consecutive characters
|
249
|
+
def progress(str)
|
250
|
+
print str
|
251
|
+
$stdout.flush
|
252
|
+
end
|
253
|
+
|
254
|
+
def pluralize(str, num)
|
255
|
+
str << 's' if num != 1
|
256
|
+
str
|
257
|
+
end
|
258
|
+
|
259
|
+
# Return default configuration options for this deployment type
|
260
|
+
def self.default_config(options={})
|
261
|
+
<<-CONFIG
|
262
|
+
#{"bucket_name: #{options[:bucket_name]}".ljust(40)} # Name of the S3 bucket where these files will be stored.
|
263
|
+
#{"access_key_id: #{options[:access_key_id]}".ljust(40)} # Get this from your AWS console at aws.amazon.com.
|
264
|
+
#{"secret_access_key: #{options[:secret_access_key]}".ljust(40)} # Keep it safe; keep it secret. Keep this file in your .gitignore.
|
265
|
+
#{"distribution_id: #{options[:distribution_id]}".ljust(40)} # Get this from your CloudFront page at https://console.aws.amazon.com/cloudfront/
|
266
|
+
#{"remote_path: #{options[:remote_path] || '/'}".ljust(40)} # relative path on bucket where files should be copied.
|
267
|
+
#{"region: #{options[:remote_path] || 'us-east-1'}".ljust(40)} # Region where your bucket is located.
|
268
|
+
#{"verbose: #{options[:verbose] || 'false'}".ljust(40)} # Print out all file operations.
|
269
|
+
#{"incremental: #{options[:incremental] || 'false'}".ljust(40)} # Only upload new/changed files
|
270
|
+
#{"delete: #{options[:delete] || 'false'}".ljust(40)} # Remove files from destination which do not match source files.
|
271
|
+
CONFIG
|
272
|
+
end
|
273
|
+
|
274
|
+
end
|
275
|
+
end
|