s3browser 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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