s3browser 0.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 +7 -0
- data/.gitignore +11 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +164 -0
- data/Rakefile +3 -0
- data/Vagrantfile +53 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/exe/s3browser-worker +25 -0
- data/lib/s3browser.rb +5 -0
- data/lib/s3browser/fetch.rb +43 -0
- data/lib/s3browser/gem_tasks.rb +158 -0
- data/lib/s3browser/plugins/es.rb +206 -0
- data/lib/s3browser/plugins/images.rb +33 -0
- data/lib/s3browser/plugins/upload.rb +22 -0
- data/lib/s3browser/policy.json +19 -0
- data/lib/s3browser/server.rb +81 -0
- data/lib/s3browser/store.rb +106 -0
- data/lib/s3browser/version.rb +3 -0
- data/lib/s3browser/views/bucket.haml +55 -0
- data/lib/s3browser/views/column_header.haml +4 -0
- data/lib/s3browser/views/flash_messages.haml +12 -0
- data/lib/s3browser/views/index.haml +12 -0
- data/lib/s3browser/views/layout.haml +39 -0
- data/lib/s3browser/worker.rb +41 -0
- data/s3browser.gemspec +40 -0
- metadata +213 -0
@@ -0,0 +1,206 @@
|
|
1
|
+
require 'elasticsearch'
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
module S3Browser
|
5
|
+
class Store
|
6
|
+
module StorePlugins
|
7
|
+
module ES
|
8
|
+
def self.configure(plugin)
|
9
|
+
# TODO Maybe setup and check index here?
|
10
|
+
end
|
11
|
+
|
12
|
+
module InstanceMethods
|
13
|
+
attr_reader :index
|
14
|
+
|
15
|
+
def initialize(index)
|
16
|
+
@index = index
|
17
|
+
check_index
|
18
|
+
end
|
19
|
+
|
20
|
+
def add(bucket, object)
|
21
|
+
# TODO Can be optimized to do a bulk index every X requests
|
22
|
+
object[:bucket] = bucket
|
23
|
+
object[:last_modified] = object[:last_modified].to_i
|
24
|
+
object[:url] = "http://#{bucket}.s3.amazonaws.com/#{object[:key]}"
|
25
|
+
client.index(index: index, type: 'objects', id: object[:key], body: object)
|
26
|
+
super(bucket, object)
|
27
|
+
end
|
28
|
+
|
29
|
+
def objects(bucket, options)
|
30
|
+
body = get_body(bucket, options)
|
31
|
+
if options[:key]
|
32
|
+
raise 'Not implemented yet'
|
33
|
+
else
|
34
|
+
result = client.search(index: index, type: 'objects', body: body)
|
35
|
+
result['hits']['hits'].map {|val| val['_source'].inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo} }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def search(bucket, term, options = {})
|
40
|
+
get(bucket, {term: term}.merge(options))
|
41
|
+
end
|
42
|
+
|
43
|
+
def buckets
|
44
|
+
client.search(index: index, type: 'objects', body: {
|
45
|
+
query: { match_all: {} },
|
46
|
+
size: 0,
|
47
|
+
aggregations: {
|
48
|
+
buckets: {
|
49
|
+
terms: {
|
50
|
+
field: :bucket,
|
51
|
+
size: 0,
|
52
|
+
order: { '_term' => :asc }
|
53
|
+
}
|
54
|
+
}
|
55
|
+
}
|
56
|
+
})['aggregations']['buckets']['buckets'].map {|val| val['key'] }
|
57
|
+
end
|
58
|
+
|
59
|
+
def indices
|
60
|
+
lines = client.cat.indices
|
61
|
+
lines.split("\n").map do |line|
|
62
|
+
line = Hash[*[
|
63
|
+
:health,
|
64
|
+
:state,
|
65
|
+
:index,
|
66
|
+
:primaries ,
|
67
|
+
:replicas,
|
68
|
+
:count,
|
69
|
+
:deleted,
|
70
|
+
:total_size,
|
71
|
+
:size
|
72
|
+
].zip(line.split(' ')).flatten]
|
73
|
+
|
74
|
+
[:primaries, :replicas, :count, :deleted].each {|key| line[key] = line[key].to_i}
|
75
|
+
|
76
|
+
line
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
def get_body(bucket, options = {})
|
82
|
+
body = {
|
83
|
+
query: {
|
84
|
+
bool: {
|
85
|
+
filter: {
|
86
|
+
terms: {
|
87
|
+
bucket: [ bucket ]
|
88
|
+
}
|
89
|
+
}
|
90
|
+
}
|
91
|
+
}
|
92
|
+
}
|
93
|
+
|
94
|
+
# Sort using the raw field
|
95
|
+
options[:sort] = 'key.raw' if options[:sort] == 'key'
|
96
|
+
body[:sort] = { options[:sort] => options[:direction] ? options[:direction] : 'asc'} if options[:sort]
|
97
|
+
|
98
|
+
if options[:term]
|
99
|
+
body[:query][:bool][:must] = {
|
100
|
+
simple_query_string: {
|
101
|
+
fields: [ 'key', 'key.raw' ],
|
102
|
+
default_operator: 'OR',
|
103
|
+
query: options[:term]
|
104
|
+
}
|
105
|
+
}
|
106
|
+
end
|
107
|
+
|
108
|
+
body
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
def client
|
113
|
+
@client ||= ::Elasticsearch::Client.new(client_options)
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
def client_options
|
118
|
+
{
|
119
|
+
log: true,
|
120
|
+
logger: Logger.new(STDOUT),
|
121
|
+
host: ENV['ELASTICSEARCH_URL'] || 'http://localhost:9200'
|
122
|
+
}
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
def check_index
|
127
|
+
client.indices.create index: index, body: {
|
128
|
+
settings: {
|
129
|
+
index: {
|
130
|
+
number_of_shards: 1,
|
131
|
+
number_of_replicas: 0
|
132
|
+
},
|
133
|
+
analysis: {
|
134
|
+
analyzer: {
|
135
|
+
filename: {
|
136
|
+
type: :custom,
|
137
|
+
char_filter: [ ],
|
138
|
+
tokenizer: :standard,
|
139
|
+
filter: [ :word_delimiter, :standard, :lowercase, :stop ]
|
140
|
+
}
|
141
|
+
}
|
142
|
+
}
|
143
|
+
},
|
144
|
+
mappings: mappings
|
145
|
+
}
|
146
|
+
rescue Elasticsearch::Transport::Transport::Errors::BadRequest
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
def mappings
|
151
|
+
{
|
152
|
+
objects: {
|
153
|
+
_timestamp: {
|
154
|
+
enabled: false
|
155
|
+
},
|
156
|
+
properties: {
|
157
|
+
accept_ranges: {
|
158
|
+
type: :string,
|
159
|
+
index: :not_analyzed
|
160
|
+
},
|
161
|
+
last_modified: {
|
162
|
+
type: :date
|
163
|
+
},
|
164
|
+
content_length: {
|
165
|
+
type: :integer
|
166
|
+
},
|
167
|
+
etag: {
|
168
|
+
type: :string,
|
169
|
+
index: :not_analyzed
|
170
|
+
},
|
171
|
+
content_type: {
|
172
|
+
type: :string,
|
173
|
+
index: :not_analyzed
|
174
|
+
},
|
175
|
+
metadata: {
|
176
|
+
type: :nested
|
177
|
+
},
|
178
|
+
key: {
|
179
|
+
type: :string,
|
180
|
+
index: :analyzed,
|
181
|
+
analyzer: :filename,
|
182
|
+
fields: {
|
183
|
+
raw: {
|
184
|
+
type: :string,
|
185
|
+
index: :not_analyzed
|
186
|
+
}
|
187
|
+
}
|
188
|
+
},
|
189
|
+
size: {
|
190
|
+
type: :integer
|
191
|
+
},
|
192
|
+
storage_class: {
|
193
|
+
type: :string,
|
194
|
+
index: :not_analyzed
|
195
|
+
}
|
196
|
+
}
|
197
|
+
}
|
198
|
+
}
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
register_plugin(:es, ES)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module S3Browser
|
2
|
+
class Store
|
3
|
+
module StorePlugins
|
4
|
+
module Images
|
5
|
+
module InstanceMethods
|
6
|
+
def add(bucket, object)
|
7
|
+
return super(bucket, object) unless handle?(object)
|
8
|
+
# TODO Create a thumbnail and make it available
|
9
|
+
|
10
|
+
super(bucket, object)
|
11
|
+
end
|
12
|
+
|
13
|
+
def objects(bucket, options)
|
14
|
+
result = super(bucket, options)
|
15
|
+
result.map do |elm|
|
16
|
+
elm[:thumbnail] = {
|
17
|
+
url: "http://#{bucket}.s3.amazonaws.com/#{elm[:key]}",
|
18
|
+
width: 200
|
19
|
+
} if handle?(elm)
|
20
|
+
elm
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def handle?(object)
|
25
|
+
object[:content_type] =~ /^image\/.*/
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
register_plugin(:images, Images)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'aws-sdk'
|
2
|
+
|
3
|
+
module S3Browser
|
4
|
+
class Store
|
5
|
+
module StorePlugins
|
6
|
+
module Upload
|
7
|
+
module InstanceMethods
|
8
|
+
def upload(file)
|
9
|
+
filename = file[:filename]
|
10
|
+
file = file[:tempfile]
|
11
|
+
bucket = 's3browser'
|
12
|
+
|
13
|
+
s3 = Aws::S3::Resource.new
|
14
|
+
s3.bucket(bucket).object(filename).upload_file(file.path)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
register_plugin(:upload, Upload)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
{
|
2
|
+
"Version": "2012-10-17",
|
3
|
+
"Statement": [
|
4
|
+
{
|
5
|
+
"Sid": "example-statement-ID",
|
6
|
+
"Effect": "Allow",
|
7
|
+
"Principal": { "AWS": "*" },
|
8
|
+
"Action": [
|
9
|
+
"SQS:SendMessage"
|
10
|
+
],
|
11
|
+
"Resource": "SQS-queue-ARN",
|
12
|
+
"Condition": {
|
13
|
+
"ArnLike": {
|
14
|
+
"aws:SourceArn": "arn:aws:s3:*:*:bucket-name"
|
15
|
+
}
|
16
|
+
}
|
17
|
+
}
|
18
|
+
]
|
19
|
+
}
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
require 'rack-flash'
|
3
|
+
require 'tilt/haml'
|
4
|
+
require 's3browser/store'
|
5
|
+
|
6
|
+
module S3Browser
|
7
|
+
class Server < Sinatra::Base
|
8
|
+
enable :sessions
|
9
|
+
use Rack::Flash
|
10
|
+
|
11
|
+
class Store < S3Browser::Store
|
12
|
+
plugin :es
|
13
|
+
plugin :images
|
14
|
+
plugin :upload
|
15
|
+
end
|
16
|
+
|
17
|
+
configure :development do
|
18
|
+
set :port, 9292
|
19
|
+
set :bind, '0.0.0.0'
|
20
|
+
enable :logging
|
21
|
+
set :store, Store.new('s3browser')
|
22
|
+
end
|
23
|
+
|
24
|
+
get '/' do
|
25
|
+
buckets = settings.store.buckets
|
26
|
+
haml :index, locals: { title: 'S3Browser', buckets: buckets }
|
27
|
+
end
|
28
|
+
|
29
|
+
get '/:bucket' do |bucket|
|
30
|
+
params['q'] = nil if params['q'] == ''
|
31
|
+
objects = settings.store.objects(bucket, term: params['q'], sort: params['s'], direction: params['d'])
|
32
|
+
haml :bucket, locals: { title: bucket, bucket: bucket, objects: objects, q: params['q'] }
|
33
|
+
end
|
34
|
+
|
35
|
+
post '/upload' do
|
36
|
+
if params['upload']
|
37
|
+
begin
|
38
|
+
settings.store.upload params['upload']
|
39
|
+
flash[:success] = 'File uploaded'
|
40
|
+
rescue
|
41
|
+
flash[:error] = 'Could not upload the file'
|
42
|
+
end
|
43
|
+
end
|
44
|
+
redirect back
|
45
|
+
end
|
46
|
+
|
47
|
+
helpers do
|
48
|
+
def bucket
|
49
|
+
ENV['AWS_S3_BUCKET']
|
50
|
+
end
|
51
|
+
|
52
|
+
def sort_url(field)
|
53
|
+
field = field.to_s
|
54
|
+
url = '?s=' + field
|
55
|
+
if params['s'] == field
|
56
|
+
if params['d'] == 'desc'
|
57
|
+
url = url + '&d=asc'
|
58
|
+
else
|
59
|
+
url = url + '&d=desc'
|
60
|
+
end
|
61
|
+
end
|
62
|
+
url
|
63
|
+
end
|
64
|
+
|
65
|
+
def sort_icon(field)
|
66
|
+
field = field.to_s
|
67
|
+
if params['s'] == field
|
68
|
+
if ['asc', 'desc'].include?(params['d'])
|
69
|
+
'fa-sort-' + params['d']
|
70
|
+
else
|
71
|
+
'fa-sort-asc'
|
72
|
+
end
|
73
|
+
else
|
74
|
+
'fa-sort'
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
run! if app_file == $0
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'aws-sdk'
|
2
|
+
|
3
|
+
module S3Browser
|
4
|
+
class Store
|
5
|
+
class StoreError < StandardError; end
|
6
|
+
|
7
|
+
# A thread safe cache class, offering only #[] and #[]= methods,
|
8
|
+
# each protected by a mutex.
|
9
|
+
# Ripped off from Roda - https://github.com/jeremyevans/roda
|
10
|
+
class StoreCache
|
11
|
+
# Create a new thread safe cache.
|
12
|
+
def initialize
|
13
|
+
@mutex = Mutex.new
|
14
|
+
@hash = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
# Make getting value from underlying hash thread safe.
|
18
|
+
def [](key)
|
19
|
+
@mutex.synchronize{@hash[key]}
|
20
|
+
end
|
21
|
+
|
22
|
+
# Make setting value in underlying hash thread safe.
|
23
|
+
def []=(key, value)
|
24
|
+
@mutex.synchronize{@hash[key] = value}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Ripped off from Roda - https://github.com/jeremyevans/roda
|
29
|
+
module StorePlugins
|
30
|
+
# Stores registered plugins
|
31
|
+
@plugins = StoreCache.new
|
32
|
+
|
33
|
+
# If the registered plugin already exists, use it. Otherwise,
|
34
|
+
# require it and return it. This raises a LoadError if such a
|
35
|
+
# plugin doesn't exist, or a StoreError if it exists but it does
|
36
|
+
# not register itself correctly.
|
37
|
+
def self.load_plugin(name)
|
38
|
+
h = @plugins
|
39
|
+
unless plugin = h[name]
|
40
|
+
require "s3browser/plugins/#{name}"
|
41
|
+
raise StoreError, "Plugin #{name} did not register itself correctly in S3Browser::Store::StorePlugins" unless plugin = h[name]
|
42
|
+
end
|
43
|
+
plugin
|
44
|
+
end
|
45
|
+
|
46
|
+
# Register the given plugin with Store, so that it can be loaded using #plugin
|
47
|
+
# with a symbol. Should be used by plugin files. Example:
|
48
|
+
#
|
49
|
+
# S3Browser::Store::StorePlugins.register_plugin(:plugin_name, PluginModule)
|
50
|
+
def self.register_plugin(name, mod)
|
51
|
+
@plugins[name] = mod
|
52
|
+
end
|
53
|
+
|
54
|
+
module Base
|
55
|
+
module ClassMethods
|
56
|
+
# Load a new plugin into the current class. A plugin can be a module
|
57
|
+
# which is used directly, or a symbol represented a registered plugin
|
58
|
+
# which will be required and then used. Returns nil.
|
59
|
+
#
|
60
|
+
# Store.plugin PluginModule
|
61
|
+
# Store.plugin :csrf
|
62
|
+
def plugin(plugin, *args, &block)
|
63
|
+
raise StoreError, "Cannot add a plugin to a frozen Store class" if frozen?
|
64
|
+
plugin = StorePlugins.load_plugin(plugin) if plugin.is_a?(Symbol)
|
65
|
+
plugin.load_dependencies(self, *args, &block) if plugin.respond_to?(:load_dependencies)
|
66
|
+
include(plugin::InstanceMethods) if defined?(plugin::InstanceMethods)
|
67
|
+
extend(plugin::ClassMethods) if defined?(plugin::ClassMethods)
|
68
|
+
plugin.configure(self, *args, &block) if plugin.respond_to?(:configure)
|
69
|
+
nil
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
module InstanceMethods
|
74
|
+
def initialize(bucket)
|
75
|
+
end
|
76
|
+
|
77
|
+
def add(bucket, object)
|
78
|
+
nil
|
79
|
+
end
|
80
|
+
|
81
|
+
def objects(bucket, options)
|
82
|
+
s3.list_objects(bucket: bucket).contents.map do |object|
|
83
|
+
object.to_h.merge(bucket: bucket, url: "http://#{bucket}.s3.amazonaws.com/#{object[:key]}")
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def search(bucket, term, options = {})
|
88
|
+
[]
|
89
|
+
end
|
90
|
+
|
91
|
+
def buckets
|
92
|
+
s3.list_buckets.buckets
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
def s3
|
97
|
+
@s3 ||= Aws::S3::Client.new
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
extend StorePlugins::Base::ClassMethods
|
104
|
+
plugin StorePlugins::Base
|
105
|
+
end
|
106
|
+
end
|