attache 1.0.5 → 1.1.0

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.
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