s3db 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3458f2b36ed8c03cf0a51b7974f6404f64d15207
4
+ data.tar.gz: c140dda6f8ee3f73ff1e0331fedaae242c8da6dc
5
+ SHA512:
6
+ metadata.gz: 6c53ba31f46f5fcfcb5a4e5282aa7a0981c52f24e137028ac7c3e425aa01c3aa4b24e1d3de9c270e8df521289894caba2b44ce6bf06ea04afaf71a3a1d36f429
7
+ data.tar.gz: 4816697a89e02e03ee9942c28392581e9cedc36dd6d401f7eeade1837cd395b25e2fba98a7f8f17c4f277fa22ff6371a374ca2476af875bc9ae18fdc14d5fb10
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'uuidtools'
4
+ gem 'json'
5
+
6
+ group :development do
7
+ gem 'rspec'
8
+ gem 'simplecov', require: false
9
+ gem 'guard-rspec', require: false
10
+ end
@@ -0,0 +1,74 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ coderay (1.1.0)
5
+ diff-lcs (1.2.5)
6
+ docile (1.1.5)
7
+ ffi (1.9.10)
8
+ formatador (0.2.5)
9
+ guard (2.13.0)
10
+ formatador (>= 0.2.4)
11
+ listen (>= 2.7, <= 4.0)
12
+ lumberjack (~> 1.0)
13
+ nenv (~> 0.1)
14
+ notiffany (~> 0.0)
15
+ pry (>= 0.9.12)
16
+ shellany (~> 0.0)
17
+ thor (>= 0.18.1)
18
+ guard-compat (1.2.1)
19
+ guard-rspec (4.6.4)
20
+ guard (~> 2.1)
21
+ guard-compat (~> 1.1)
22
+ rspec (>= 2.99.0, < 4.0)
23
+ json (1.8.3)
24
+ listen (3.0.3)
25
+ rb-fsevent (>= 0.9.3)
26
+ rb-inotify (>= 0.9)
27
+ lumberjack (1.0.9)
28
+ method_source (0.8.2)
29
+ nenv (0.2.0)
30
+ notiffany (0.0.8)
31
+ nenv (~> 0.1)
32
+ shellany (~> 0.0)
33
+ pry (0.10.2)
34
+ coderay (~> 1.1.0)
35
+ method_source (~> 0.8.1)
36
+ slop (~> 3.4)
37
+ rb-fsevent (0.9.6)
38
+ rb-inotify (0.9.5)
39
+ ffi (>= 0.5.0)
40
+ rspec (3.3.0)
41
+ rspec-core (~> 3.3.0)
42
+ rspec-expectations (~> 3.3.0)
43
+ rspec-mocks (~> 3.3.0)
44
+ rspec-core (3.3.2)
45
+ rspec-support (~> 3.3.0)
46
+ rspec-expectations (3.3.1)
47
+ diff-lcs (>= 1.2.0, < 2.0)
48
+ rspec-support (~> 3.3.0)
49
+ rspec-mocks (3.3.2)
50
+ diff-lcs (>= 1.2.0, < 2.0)
51
+ rspec-support (~> 3.3.0)
52
+ rspec-support (3.3.0)
53
+ shellany (0.0.1)
54
+ simplecov (0.10.0)
55
+ docile (~> 1.1.0)
56
+ json (~> 1.8)
57
+ simplecov-html (~> 0.10.0)
58
+ simplecov-html (0.10.0)
59
+ slop (3.6.0)
60
+ thor (0.19.1)
61
+ uuidtools (2.1.5)
62
+
63
+ PLATFORMS
64
+ ruby
65
+
66
+ DEPENDENCIES
67
+ guard-rspec
68
+ json
69
+ rspec
70
+ simplecov
71
+ uuidtools
72
+
73
+ BUNDLED WITH
74
+ 1.10.6
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ puts 'success'
4
+
@@ -0,0 +1,16 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ Bundler.require(:default)
4
+
5
+ require_relative 's3db/utils'
6
+ require_relative 's3db/collection'
7
+ require_relative 's3db/database'
8
+ require_relative 's3db/record'
9
+ require_relative 's3db/backend'
10
+ require_relative 's3db/file_backend'
11
+
12
+ module S3DB
13
+ class << self
14
+ attr_accessor :backend
15
+ end
16
+ end
@@ -0,0 +1,4 @@
1
+ module S3DB
2
+ class Backend
3
+ end
4
+ end
@@ -0,0 +1,70 @@
1
+ module S3DB
2
+ class Collection
3
+ attr_reader :database, :name
4
+
5
+ class << self
6
+ # Create a new collection.
7
+ #
8
+ # database - Database attached to collection. Required.
9
+ # name - String name of the collection. Required.
10
+ #
11
+ # returns a new Collection.
12
+ def create(database, name)
13
+ collection = new(database, name)
14
+ collection.save
15
+
16
+ collection
17
+ end
18
+ end
19
+
20
+ # Instantiate a new collection, without writing it to disk.
21
+ #
22
+ # database - Database attached to collection. Required.
23
+ # name - String name of the collection. Required.
24
+ #
25
+ # returns a new Collection, validated but unwritten.
26
+ def initialize(database, name)
27
+
28
+ # Store the database and collection name
29
+ @database = database
30
+ @name = Utils.sanitize(name)
31
+
32
+ # Sanity check the database and collection name
33
+ validate!
34
+
35
+ # Yield self for configs, if people want to.
36
+ yield self if block_given?
37
+ end
38
+
39
+ # Validate a collection to ensure that it's sane.
40
+ #
41
+ # returns nil on success; raises an error on failure.
42
+ def validate!
43
+ unless @database.is_a?(S3DB::Database)
44
+ raise ArgumentError, 'database must be an S3DB::Database!'
45
+ end
46
+
47
+ unless @name.is_a?(String)
48
+ raise ArgumentError, 'name must be a String!'
49
+ end
50
+
51
+ nil
52
+ end
53
+
54
+ def list_records
55
+ @database.backend.list_records(@database.name, @name).map do |file|
56
+ @database.backend.read_record(@database.name, @name, file)
57
+ end
58
+ end
59
+
60
+ # Write the collection skeleton to disk.
61
+ #
62
+ # Returns nil.
63
+ def save
64
+ @database.backend.write_collection(@database.name, @name)
65
+ @database.backend.write_schema(@database.name, @name, @schema.to_json)
66
+
67
+ nil
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,100 @@
1
+ module S3DB
2
+ class Database
3
+ attr_reader :backend, :name
4
+
5
+ class << self
6
+ # Create a new database.
7
+ #
8
+ # backend - S3DB::Backend subclass. Required.
9
+ # db_name - String name of the database to create. Required.
10
+ #
11
+ # returns a new S3DB::Database on success, raises an error on failure.
12
+ def create(backend, db_name)
13
+ begin
14
+ backend.write_db(db_name)
15
+ rescue Errno::EEXIST
16
+ raise ArgumentError, 'database exists!'
17
+ end
18
+
19
+ new(backend, db_name)
20
+ end
21
+
22
+ # Drop a database.
23
+ #
24
+ # backend - S3DB::Backend subclass. Required.
25
+ # db_name - String name of database to drop. Required.
26
+ #
27
+ # returns the String database name on success, raises an error on failure.
28
+ def drop(backend, db_name)
29
+ backend.delete_db(db_name)
30
+ end
31
+ end
32
+
33
+ # Create a new DB instance.
34
+ #
35
+ # backend - S3DB::Backend subclass. Required.
36
+ # db_name - String name of database. Will be used in the storage path,
37
+ # so make sure it's something sane. Required.
38
+ #
39
+ # returns a new Database instance.
40
+ def initialize(backend, db_name)
41
+ @backend = backend
42
+ @name = db_name
43
+
44
+ yield self if block_given?
45
+ end
46
+
47
+ # Save a DB instance to disk.
48
+ #
49
+ # Returns a Database instance.
50
+ def save
51
+ @backend.write_db(@name)
52
+
53
+ self
54
+ end
55
+
56
+ # List all available collections in the database.
57
+ #
58
+ # returns sorted Array of Strings.
59
+ def show_collections
60
+ @backend.list_collections(@name).sort
61
+ end
62
+
63
+ # Create a collection under this database.
64
+ #
65
+ # collection - String name of collection to create. Will also be used as
66
+ # its filesystem path, so use something sane. Required.
67
+ # schema - Hash schema for this collection. Default: {}.
68
+ #
69
+ # returns true on success.
70
+ def create_collection(collection, schema = {})
71
+ Collection.create(self, collection)
72
+ end
73
+
74
+ # Drop a collection from this database.
75
+ #
76
+ # collection - String name of collection to drop.
77
+ #
78
+ # returns the name of the collection on success.
79
+ def drop_collection(collection)
80
+ @backend.delete_collection(@name, collection)
81
+ end
82
+
83
+ # Locate the storage location for the database.
84
+ #
85
+ # returns a String path of the database path. Will vary by backend adapter.
86
+ def path
87
+ @backend.db_path(@name)
88
+ end
89
+
90
+ private
91
+
92
+ # Check to ensure that the database name is valid, from the perspective of
93
+ # the storage engine.
94
+ #
95
+ # returns a Bool.
96
+ def valid?
97
+ @backend.db_exist?(@name)
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,492 @@
1
+ require 'fileutils'
2
+
3
+ module S3DB
4
+ class FileBackend < Backend
5
+ attr_reader :path, :errors
6
+
7
+ PATH_BLACKLIST = [
8
+ 'bin',
9
+ 'boot',
10
+ 'cdrom',
11
+ 'data',
12
+ 'dev',
13
+ 'docker',
14
+ 'etc',
15
+ 'home',
16
+ 'lib',
17
+ 'lib64',
18
+ 'media',
19
+ 'mnt',
20
+ 'opt',
21
+ 'proc',
22
+ 'root',
23
+ 'run',
24
+ 'sbin',
25
+ 'srv',
26
+ 'sys',
27
+ 'usr',
28
+ 'var',
29
+ 'badpath', #this is just to be VERY sure we don't trash a real dir in tests
30
+ ]
31
+
32
+ class << self
33
+ # Create a new base path for a file backend storage location. This method
34
+ # will create the basepath if it doesn't exist, but also use an existing
35
+ # path if it does exest.
36
+ #
37
+ # path - String base path. Required.
38
+ #
39
+ # returns a new FileBackend.
40
+ def create(path)
41
+ be = new(path)
42
+ be.save
43
+
44
+ be
45
+ end
46
+
47
+ # Create a new base path for a file backend storage location. This method
48
+ # will throw an error if the path already exists. This is safer.
49
+ #
50
+ # path - String base path. Required.
51
+ #
52
+ # returns a new FileBackend.
53
+ def create!(path)
54
+ be = new(path)
55
+ be.save!
56
+
57
+ be
58
+ end
59
+
60
+ # Destroy a base path for data storage. This method will raise an error
61
+ # if the directory is not empty.
62
+ #
63
+ # path - String base path. Required.
64
+ #
65
+ # returns the String path that was removed.
66
+ def destroy(path)
67
+ be = new(path).destroy
68
+
69
+ be
70
+ end
71
+
72
+ # Destroy a base path, whether or not it's empty. This is dangerous, and
73
+ # should be used with great care.
74
+ #
75
+ # path - String path to delete. Required.
76
+ #
77
+ # returns itself on success, and raises an error on failure.
78
+ def delete(path)
79
+ be = new(path)
80
+
81
+ if be.valid!
82
+ FileUtils.rm_rf(path)
83
+ end
84
+
85
+ self
86
+ end
87
+ end
88
+
89
+ # Create a new FileBackend.
90
+ #
91
+ # path - String path to use as the base storage location.
92
+ #
93
+ # returns a new FileBackend.
94
+ def initialize(path)
95
+ @errors = []
96
+
97
+ @path = path.strip
98
+ end
99
+
100
+ # Check a path to ensure it does not violate basic sanity rules, such as
101
+ # being a linux system path, or having weird characters in the name.
102
+ #
103
+ # path - String path to check. Required.
104
+ #
105
+ # returns true if it checks out, false otherwise. It will raise an error
106
+ # for dangerous exceptions.
107
+ def validate_path
108
+ PATH_BLACKLIST.each do |p|
109
+ @errors << "`#{p}` is insane to use as a base path!" if @path =~ /#{p}/i
110
+ end
111
+
112
+ if @path !~ /^(\w|\/)+$/
113
+ @errors << "path does not match /^(\w|\/)+$/"
114
+ end
115
+ end
116
+
117
+ # Check to see whether the backend is in a valid state.
118
+ #
119
+ # returns Bool.
120
+ def valid?
121
+ @errors = []
122
+
123
+ validate_path
124
+
125
+ !@errors.any?
126
+ end
127
+
128
+ # Confirm that the backend is in a consistent state. Raises an error on
129
+ # failure.
130
+ #
131
+ # returns true on success, raises an error on failure.
132
+ def valid!
133
+ if !valid?
134
+ raise ArgumentError, @errors.join(', ')
135
+ end
136
+
137
+ true
138
+ end
139
+
140
+ # Save a FileBackend, which means writing its root path directory to the
141
+ # filesystem.
142
+ #
143
+ # returns itself on success, returns false on failure.
144
+ def save
145
+ return false unless valid?
146
+
147
+ FileUtils.mkdir_p(@path)
148
+
149
+ self
150
+ end
151
+
152
+ # Save a FileBackend, which means writing its root path directory to the
153
+ # filesystem.
154
+ #
155
+ # returns itself on success, raises an error on failure.
156
+ def save!
157
+ valid!
158
+
159
+ begin
160
+ Dir.mkdir(@path)
161
+ rescue Errno::EEXIST
162
+ raise ArgumentError, 'base path exists!'
163
+ end
164
+
165
+ self
166
+ end
167
+
168
+
169
+ # Destroy a FileBackend basepath. The directory must be empty (which means
170
+ # you must destroy all collections/dbs in the basepath first).
171
+ #
172
+ # returns itself on success, false on failure.
173
+ def destroy
174
+ return false unless valid?
175
+
176
+ begin
177
+ Dir.rmdir(@path)
178
+ rescue Errno::ENOTEMPTY, Errno::ENOENT
179
+ return false
180
+ end
181
+
182
+ self
183
+ end
184
+
185
+ # Destroy a FileBackend basepath. The directory must be empty (which means
186
+ # you must destroy all collections/dbs in the basepath first).
187
+ #
188
+ # returns itself on success, raises an error on failure.
189
+ def destroy!
190
+ valid!
191
+
192
+ begin
193
+ Dir.rmdir(@path)
194
+ rescue Errno::ENOENT
195
+ raise ArgumentError, 'basepath does not exist!'
196
+ rescue Errno::ENOTEMPTY
197
+ raise ArgumentError, 'basepath not empty!'
198
+ end
199
+
200
+ self
201
+ end
202
+
203
+ # Build a full path from the base path and database name.
204
+ #
205
+ # db_name - String name of database. Required.
206
+ #
207
+ # returns a String path.
208
+ def db_path(db_name)
209
+ File.join(@path, db_name)
210
+ end
211
+
212
+ # Build a full path to a collection from the base path, database name and
213
+ # collection name.
214
+ #
215
+ # db_name - String name of database. Required.
216
+ # collection_name - String name of collection. Required.
217
+ #
218
+ # returns a String path.
219
+ def collection_path(db_name, collection_name)
220
+ File.join(@path, db_name, collection_name)
221
+ end
222
+
223
+ # Build a full path to a scema from the base path, database name and
224
+ # collection name.
225
+ #
226
+ # db_name - String name of database. Required.
227
+ # collection_name - String name of collection. Required.
228
+ #
229
+ # returns a String path.
230
+ def schema_path(db_name, collection_name)
231
+ File.join(@path, db_name, collection_name, 'schema.json')
232
+ end
233
+
234
+ # Build a full path to the data dir from the base path, database name and
235
+ # collection name.
236
+ #
237
+ # db_name - String name of database. Required.
238
+ # collection_name - String name of collection. Required.
239
+ #
240
+ # returns a String path.
241
+ def data_path(db_name, collection_name)
242
+ File.join(@path, db_name, collection_name, 'data')
243
+ end
244
+
245
+ # Build a full path to a record from the base path, database name,
246
+ # collection name, and filename.
247
+ #
248
+ # db_name - String name of database. Required.
249
+ # collection_name - String name of collection. Required.
250
+ #
251
+ # returns a String path.
252
+ def record_path(db_name, collection_name, filename)
253
+ File.join(@path, db_name, collection_name, 'data', filename)
254
+ end
255
+
256
+ # Write a directory to disk for the name of the database. Will fail if the
257
+ # database already exists.
258
+ #
259
+ # db_name - String name of database. Required.
260
+ #
261
+ # returns db_name on success; raises an error on failure.
262
+ def write_db(db_name)
263
+ FileUtils.mkdir_p(db_path(db_name))
264
+
265
+ db_name
266
+ end
267
+
268
+ # Write a directory to disk for the name of the collection. Will ignore the
269
+ # write if the directory exists.
270
+ #
271
+ # db_name - String name of database. Required.
272
+ # collection_name - String name of collection. Required.
273
+ #
274
+ # returns collection_name on success; raises an error on failure.
275
+ def write_collection(db_name, collection_name)
276
+ dir = collection_path(db_name, collection_name)
277
+
278
+ unless Dir.exist?(dir)
279
+ Dir.mkdir(dir)
280
+ end
281
+
282
+ dir = data_path(db_name, collection_name)
283
+
284
+ unless Dir.exist?(dir)
285
+ Dir.mkdir(dir)
286
+ end
287
+
288
+ collection_name
289
+ end
290
+
291
+ # Write a file to disk for the schema for a database/collection.
292
+ #
293
+ # db_name - String name of database. Required.
294
+ # collection_name - String name of collection. Required.
295
+ # schema - String schema contents. Required.
296
+ #
297
+ # returns the schema on success; raises an error on failure.
298
+ def write_schema(db_name, collection_name, schema)
299
+ File.open(schema_path(db_name, collection_name), 'w') do |f|
300
+ f.puts schema
301
+ end
302
+
303
+ schema
304
+ end
305
+
306
+ # Write a record to a database/collection. The record can be in any format.
307
+ # No parsing of the file is performed. Data must be sent as a string.
308
+ #
309
+ # db_name - String name of database. Required.
310
+ # collection_name - String name of collection. Required.
311
+ # filename - String name of file to write, w/extension. Required.
312
+ # data - String data to write to the file. Required.
313
+ #
314
+ # returns the data written.
315
+ def write_record(db_name, coll_name, filename, data)
316
+ raise ArgumentError, 'data must be a string!' unless data.is_a?(String)
317
+
318
+ File.open(record_path(db_name, coll_name, filename), 'w') do |f|
319
+ f.puts data
320
+ end
321
+
322
+ data
323
+ end
324
+
325
+ # List all available collections in a database.
326
+ #
327
+ # db_name - String name of database to look in. Required.
328
+ #
329
+ # returns an Array of String collection names.
330
+ def list_collections(db_name)
331
+ Dir.entries(db_path(db_name)).select do |dir|
332
+ !File.directory?(dir)
333
+ end
334
+ end
335
+
336
+ # List all available records for a database/collection.
337
+ #
338
+ # db_name - String name of database to look in. Required.
339
+ # collection_name - String name of collection to look in. Required.
340
+ #
341
+ # returns an Array of String record file names.
342
+ def list_records(db_name, coll_name)
343
+ output = []
344
+
345
+ Dir.open(data_path(db_name, coll_name)) do |dir|
346
+ dir.each do |file|
347
+ output << file unless File.directory?(file)
348
+ end
349
+ end
350
+
351
+ output
352
+ end
353
+
354
+ # Read the contents of the schema file.
355
+ #
356
+ # db_name - String name of database to look in. Required.
357
+ # collection_name - String name of collection to look in. Required.
358
+ #
359
+ # returns the String contents of the schema file. Raises an error if the
360
+ # file is missing.
361
+ def read_schema(db_name, collection_name)
362
+ file = schema_path(db_name, collection_name)
363
+
364
+ begin
365
+ File.read(file)
366
+ rescue Errno::ENOENT
367
+ raise ArgumentError, 'schema does not exist!'
368
+ end
369
+ end
370
+
371
+ # Read the contents of a record in a database/collection
372
+ #
373
+ # db_name - String name of database to look in. Required.
374
+ # collection_name - String name of collection to look in. Required.
375
+ # filename - String name of the file to read w/extension. Required.
376
+ #
377
+ # returns the String contents of the record file. Raises an error if the
378
+ # file is missing.
379
+ def read_record(db_name, coll_name, filename)
380
+ file = record_path(db_name, coll_name, filename)
381
+
382
+ begin
383
+ File.read(file)
384
+ rescue Errno::ENOENT
385
+ raise ArgumentError, 'record does not exist!'
386
+ end
387
+ end
388
+
389
+ # Delete a database directory from disk. Will only succed if the database is
390
+ # empty.
391
+ #
392
+ # db_name - String name of database to remove. Required.
393
+ #
394
+ # returns an Array of the databases deleted, or empty if the database did
395
+ # not exist.
396
+ def delete_db(db_name)
397
+ path = db_path(db_name)
398
+
399
+ if Dir.exist?(path)
400
+ begin
401
+ Dir.rmdir(path)
402
+ rescue Errno::ENOTEMPTY
403
+ raise ArgumentError, 'database is not empty!'
404
+ end
405
+
406
+ return [db_name]
407
+ end
408
+
409
+ []
410
+ end
411
+
412
+ # Delete a collection directory from disk. Will only succed if the
413
+ # collection is empty.
414
+ #
415
+ # db_name - String name of database to remove. Required.
416
+ # collection_name - String name of collection to remove. Required.
417
+ #
418
+ # returns an Array of the databases deleted, or empty if the database did
419
+ # not exist.
420
+ def delete_collection(db_name, collection_name)
421
+ path = collection_path(db_name, collection_name)
422
+
423
+ if File.exist?(schema_path(db_name, collection_name))
424
+ File.delete(schema_path(db_name, collection_name))
425
+ end
426
+
427
+ if Dir.exist?(data_path(db_name, collection_name))
428
+ begin
429
+ Dir.rmdir(data_path(db_name, collection_name))
430
+ rescue Errno::ENOTEMPTY
431
+ raise ArgumentError, 'collection/data is not empty!'
432
+ end
433
+ end
434
+
435
+ if Dir.exist?(path)
436
+ begin
437
+ Dir.rmdir(path)
438
+ rescue Errno::ENOTEMPTY
439
+ raise ArgumentError, 'collection is not empty!'
440
+ end
441
+
442
+ return [collection_name]
443
+ end
444
+
445
+ []
446
+ end
447
+
448
+ # Delete a collection directory from disk. Will only succed if the
449
+ # collection is empty.
450
+ #
451
+ # db_name - String name of database to remove. Required.
452
+ # collection_name - String name of collection to remove. Required.
453
+ #
454
+ # returns an Array of the databases deleted, or empty if the database did
455
+ # not exist.
456
+ def delete_record(db_name, collection_name, filename)
457
+ path = record_path(db_name, collection_name, filename)
458
+
459
+ begin
460
+ File.delete(path)
461
+ rescue Errno::ENOENT
462
+ return ''
463
+ end
464
+
465
+ filename
466
+ end
467
+
468
+ # Delete a collection directory from disk. Will only succed if the
469
+ # collection is empty.
470
+ #
471
+ # db_name - String name of database to remove. Required.
472
+ # collection_name - String name of collection to remove. Required.
473
+ #
474
+ # returns an Array of the databases deleted, or empty if the database did
475
+ # not exist.
476
+ def delete_record!(db_name, collection_name, filename)
477
+ path = record_path(db_name, collection_name, filename)
478
+
479
+ begin
480
+ File.delete(path)
481
+ rescue Errno::ENOENT
482
+ raise ArgumentError, 'record does not exist!'
483
+ end
484
+
485
+ filename
486
+ end
487
+
488
+ def db_exist?(db_name)
489
+ Dir.exist?(db_path(db_name))
490
+ end
491
+ end
492
+ end