attache 1.0.5 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 712ee55417c8e98d23531489e2e094c5848fbe85
4
- data.tar.gz: e7961934d82f5da524db5f0e238c8002bfafbc04
3
+ metadata.gz: 8a5ec5e8b4431d1505fb6519428f01091da7a658
4
+ data.tar.gz: 1bdfa46bee9d2015ff5afc0fd9caaff55bb3f5f2
5
5
  SHA512:
6
- metadata.gz: 45e693e294ae935e974c003f7ffbcdf905cb68a73ce87dedccaf7f637a075f29e949ad42390106101b7924325beb67c42c74b3bdd5b9b3f636ac2d6ecc42047e
7
- data.tar.gz: 3b20dede4e93a1b95eca4cf8e4b37fd0385630729dfd47176b999e1b8b14063637383b09dde95a59ed3ff0c7cf4c4c33f1d2de53089273ed06e9beda0f183700
6
+ metadata.gz: 00659d9c0716246aa7c501dd6a37f8e6fd6a0e2c4e9ec23f17418f7536362f013ee05c885b9cbd74b7159b68ed12d33cb83326b4c1ff6ef8eca2b31ab46cb0fa
7
+ data.tar.gz: 1b0cb02c0d32909d23f5b5cb120e5a463f0aaa323c9304367ed28fc6f4b9d884ff2ce085f2db99654cca892d01355e7a99e00bd7482a456f768ca63179cd70b2
data/README.md CHANGED
@@ -173,6 +173,23 @@ Removing 1 or more files from the local cache and remote storage can be done via
173
173
 
174
174
  The `paths` value should be delimited by the newline character, aka `\n`. In the example above, 3 files will be requested for deletion: `image1.jpg`, `prefix2/image2.jpg`, and `image3.jpg`.
175
175
 
176
+ #### Backup
177
+
178
+ > ```
179
+ > POST /backup
180
+ > paths=image1.jpg%0Aprefix2%2Fimage2.jpg%0Aimage3.jpg
181
+ > ```
182
+
183
+ Copying 1 or more files from the default remote storage to the backup remote storage (backup) can be done via a http `POST` request to `/backup`, with a `paths` parameter in the request body.
184
+
185
+ The `paths` value should be delimited by the newline character, aka `\n`. In the example above, 3 files will be requested for backup: `image1.jpg`, `prefix2/image2.jpg`, and `image3.jpg`.
186
+
187
+ If backup remote storage is not configured, this API call will be a noop. If configured, the backup storage must be accessible by the same credentials as default cloud storage as the system. Please refer to the `BACKUP_CONFIG` configuration illustrated in `config/vhost.example.yml` file in this repository.
188
+
189
+ By default, `backup` operation is performed synchronously. Set `BACKUP_ASYNC` environment variable to make it follow the same synchronicity as `delete`
190
+
191
+ The main reason to configure a backup storage is to make the default cloud storage auto expire files; mitigating [abuse](https://github.com/choonkeat/attache/issues/13). You should consult the documentation of your cloud storage provider on how to setup auto expiry, e.g. [here](https://aws.amazon.com/blogs/aws/amazon-s3-object-expiration/) or [here](https://cloud.google.com/storage/docs/lifecycle)
192
+
176
193
  ## License
177
194
 
178
195
  MIT
data/config.ru CHANGED
@@ -4,6 +4,7 @@ use Attache::Delete
4
4
  use Attache::Upload
5
5
  use Attache::Download
6
6
  use Attache::Tus::Upload
7
+ use Attache::Backup
7
8
  use Rack::Static, urls: ["/"], root: Attache.publicdir, index: "index.html"
8
9
 
9
10
  run proc {|env| [200, {}, []] }
@@ -20,6 +20,9 @@
20
20
  "aws_secret_access_key": CHANGEME
21
21
  "bucket": CHANGEME
22
22
  "region": us-west-1
23
+ "BACKUP_CONFIG":
24
+ "bucket": CHANGEME_BAK
25
+ # only supports 1 key: `bucket`
23
26
 
24
27
  # This section will only take effect if a request is made to `localhost:9292`
25
28
  "localhost:9292":
data/lib/attache.rb CHANGED
@@ -52,6 +52,7 @@ require 'attache/base'
52
52
  require 'attache/vhost'
53
53
  require 'attache/upload'
54
54
  require 'attache/delete'
55
+ require 'attache/backup'
55
56
  require 'attache/download'
56
57
  require 'attache/file_response_body'
57
58
 
@@ -0,0 +1,29 @@
1
+ class Attache::Backup < Attache::Base
2
+ def initialize(app)
3
+ @app = app
4
+ end
5
+
6
+ def _call(env, config)
7
+ case env['PATH_INFO']
8
+ when '/backup'
9
+ request = Rack::Request.new(env)
10
+ params = request.params
11
+ return config.unauthorized unless config.authorized?(params)
12
+
13
+ if config.storage && config.bucket
14
+ sync_method = (ENV['BACKUP_ASYNC'] ? :async : :send)
15
+ params['paths'].to_s.split("\n").each do |relpath|
16
+ Attache.logger.info "BACKUP remote #{relpath}"
17
+ config.send(sync_method, :backup_file, relpath: relpath)
18
+ end
19
+ end
20
+ [200, config.headers_with_cors, []]
21
+ else
22
+ @app.call(env)
23
+ end
24
+ rescue Exception
25
+ Attache.logger.error $@
26
+ Attache.logger.error $!
27
+ [500, { 'X-Exception' => $!.to_s }, []]
28
+ end
29
+ end
@@ -11,13 +11,28 @@ class Attache::Delete < Attache::Base
11
11
  return config.unauthorized unless config.authorized?(params)
12
12
 
13
13
  params['paths'].to_s.split("\n").each do |relpath|
14
- Attache.logger.info "DELETING local #{relpath}"
15
- cachekey = File.join(request_hostname(env), relpath)
16
- Attache.cache.delete(cachekey)
14
+ threads = []
15
+
16
+ if Attache.cache
17
+ threads << Thread.new do
18
+ Attache.logger.info "DELETING local #{relpath}"
19
+ cachekey = File.join(request_hostname(env), relpath)
20
+ Attache.cache.delete(cachekey)
21
+ end
22
+ end
17
23
  if config.storage && config.bucket
18
- Attache.logger.info "DELETING remote #{relpath}"
19
- config.async(:storage_destroy, relpath: relpath)
24
+ threads << Thread.new do
25
+ Attache.logger.info "DELETING remote #{relpath}"
26
+ config.async(:storage_destroy, relpath: relpath)
27
+ end
28
+ end
29
+ if config.backup
30
+ threads << Thread.new do
31
+ Attache.logger.info "DELETING backup #{relpath}"
32
+ config.backup.async(:storage_destroy, relpath: relpath)
33
+ end
20
34
  end
35
+ threads.each(&:join)
21
36
  end
22
37
  [200, config.headers_with_cors, []]
23
38
  else
@@ -1,7 +1,6 @@
1
1
  require 'connection_pool'
2
2
 
3
3
  class Attache::Download < Attache::Base
4
- REMOTE_GEOMETRY = ENV.fetch('REMOTE_GEOMETRY') { 'remote' }
5
4
  OUTPUT_EXTENSIONS = %w[png jpg jpeg gif]
6
5
  RESIZE_JOB_POOL = ConnectionPool.new(JSON.parse(ENV.fetch('RESIZE_POOL') { '{ "size": 2, "timeout": 60 }' }).symbolize_keys) { Attache::ResizeJob.new }
7
6
 
@@ -12,10 +11,14 @@ class Attache::Download < Attache::Base
12
11
  def _call(env, config)
13
12
  case env['PATH_INFO']
14
13
  when %r{\A/view/}
14
+ vhosts = {}
15
+ vhosts[ENV.fetch('REMOTE_GEOMETRY') { 'remote' }] = config.storage && config.bucket && config
16
+ vhosts[ENV.fetch('BACKUP_GEOMETRY') { 'backup' }] = config.backup
17
+
15
18
  parse_path_info(env['PATH_INFO']['/view/'.length..-1]) do |dirname, geometry, basename, relpath|
16
- if geometry == REMOTE_GEOMETRY && config.storage && config.bucket
17
- headers = config.download_headers.merge({
18
- 'Location' => config.storage_url(relpath: relpath),
19
+ if vhost = vhosts[geometry]
20
+ headers = vhost.download_headers.merge({
21
+ 'Location' => vhost.storage_url(relpath: relpath),
19
22
  'Cache-Control' => 'private, no-cache',
20
23
  })
21
24
  return [302, headers, []]
@@ -24,11 +27,11 @@ class Attache::Download < Attache::Base
24
27
  file = begin
25
28
  cachekey = File.join(request_hostname(env), relpath)
26
29
  Attache.cache.fetch(cachekey) do
27
- config.storage_get(relpath: relpath) if config.storage && config.bucket
30
+ get_first_result_async(vhosts.inject({}) {|sum,(k,v)|
31
+ v ? sum.merge("#{k} #{relpath}" => lambda { v.storage_get(relpath: relpath) }) : sum
32
+ })
28
33
  end
29
34
  rescue Exception # Errno::ECONNREFUSED, OpenURI::HTTPError, Excon::Errors, Fog::Errors::Error
30
- Attache.logger.error $@
31
- Attache.logger.error $!
32
35
  Attache.logger.error "ERROR REFERER #{env['HTTP_REFERER'].inspect}"
33
36
  nil
34
37
  end
@@ -37,7 +40,8 @@ class Attache::Download < Attache::Base
37
40
  return [404, config.download_headers, []]
38
41
  end
39
42
 
40
- thumbnail = if geometry == 'original' || geometry == REMOTE_GEOMETRY
43
+ thumbnail = case geometry
44
+ when 'original', *vhosts.keys
41
45
  file
42
46
  else
43
47
  extension = basename.split(/\W+/).last
@@ -83,4 +87,25 @@ class Attache::Download < Attache::Base
83
87
  end
84
88
  end
85
89
 
90
+ def get_first_result_async(name_code_pairs)
91
+ result = nil
92
+ threads = name_code_pairs.collect {|name, code|
93
+ Thread.new do
94
+ Thread.handle_interrupt(BasicObject => :on_blocking) { # if killed
95
+ begin
96
+ if current_result = code.call
97
+ result = current_result
98
+ (threads - [Thread.current]).each(&:kill) # kill siblings
99
+ Attache.logger.info "[POOL] found #{name.inspect}"
100
+ end
101
+ rescue Exception
102
+ Attache.logger.info "[POOL] not found #{name.inspect}"
103
+ ensure
104
+ end
105
+ }
106
+ end
107
+ }
108
+ threads.each(&:join)
109
+ result
110
+ end
86
111
  end
@@ -1,3 +1,3 @@
1
1
  module Attache
2
- VERSION = "1.0.5"
2
+ VERSION = "1.1.0"
3
3
  end
data/lib/attache/vhost.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  class Attache::VHost
2
2
  attr_accessor :remotedir,
3
3
  :secret_key,
4
+ :backup,
4
5
  :bucket,
5
6
  :storage,
6
7
  :download_headers,
@@ -14,6 +15,11 @@ class Attache::VHost
14
15
  if env['FOG_CONFIG']
15
16
  self.bucket = env['FOG_CONFIG'].fetch('bucket')
16
17
  self.storage = Fog::Storage.new(env['FOG_CONFIG'].except('bucket').symbolize_keys)
18
+
19
+ if env['BACKUP_CONFIG']
20
+ backup_fog = env['FOG_CONFIG'].merge(env['BACKUP_CONFIG'])
21
+ self.backup = Attache::VHost.new(env.except('BACKUP_CONFIG').merge('FOG_CONFIG' => backup_fog))
22
+ end
17
23
  end
18
24
  self.download_headers = {
19
25
  "Cache-Control" => "public, max-age=31536000"
@@ -86,4 +92,11 @@ class Attache::VHost
86
92
  def unauthorized
87
93
  [401, headers_with_cors.merge('X-Exception' => 'Authorization failed'), []]
88
94
  end
95
+
96
+ def backup_file(args)
97
+ if backup
98
+ key = File.join(*remotedir, args[:relpath])
99
+ storage.copy_object(bucket, key, backup.bucket, key)
100
+ end
101
+ end
89
102
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: attache
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.5
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - choonkeat
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2015-11-16 00:00:00.000000000 Z
11
+ date: 2015-12-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -249,6 +249,7 @@ files:
249
249
  - config/vhost.example.yml
250
250
  - exe/attache
251
251
  - lib/attache.rb
252
+ - lib/attache/backup.rb
252
253
  - lib/attache/base.rb
253
254
  - lib/attache/delete.rb
254
255
  - lib/attache/deleteme.log