s3db 0.0.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,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