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