file_pool 0.2.0 → 0.3.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 73a8c6c237185c15b0385f37405649571c3668db
4
+ data.tar.gz: 9f6a23a5c02abe1394956d3ee74590f477224595
5
+ SHA512:
6
+ metadata.gz: f348a4491e460c47cc3d5e288cb365aa3ba965cebcfd2858646c03e680a587951538406a3615e3e1d7ba9371c6f058e47c3578cae725ce69612b146691bf3102
7
+ data.tar.gz: fb19cf25d543f63d3c364a96bd08f9ad2a4a52d56c46826eb78fec6361419faceab4c8865d6a37a6948fae964c0c78ee9aa95ce798527593b53726cc69f045f5
data/README.md CHANGED
@@ -2,8 +2,9 @@
2
2
 
3
3
  FilePool helps to manage a large number of files in a Ruby project. It
4
4
  takes care of the storage of files in a balanced directory tree and
5
- generates unique identifiers for all files. It also comes in handy
6
- when dealing with only a few files.
5
+ generates unique identifiers for all files.
6
+
7
+ Optionally FilePool can use symmetric encryption for all files managed.
7
8
 
8
9
  FilePool does not deal with file meta information. It's only purpose
9
10
  is to return a file's location given a file identifier, which was
@@ -17,7 +18,7 @@ the 3 first hexadecimal digits of a UUID as path. For example:
17
18
  0/c/f/0cfb082a-fd57-490c-978b-e47d5948bc8b
18
19
  6/1/d/61ddfe33-13f3-4f71-9234-5fbbf5c4fc2c
19
20
 
20
- FilePool is tested with Ruby 1.8.7 and 1.9.3.
21
+ FilePool is tested with Ruby 1.8.7, 2.0.0 and 2.2.1.
21
22
 
22
23
  ## Installation
23
24
 
@@ -36,13 +37,14 @@ Or install it yourself as:
36
37
  ## Usage
37
38
 
38
39
  ### Setup
39
- The root path and optionally a Logger must be defined:
40
+
41
+ Set up the root path under which all files will reside:
40
42
 
41
43
  FilePool.setup '/var/lib/files'
42
44
 
43
45
  In a Rails project the file pool setup would be placed in an intializer:
44
46
 
45
- config/initializers/file_pool.
47
+ config/initializers/file_pool.rb
46
48
 
47
49
  ### Example Usage
48
50
 
@@ -58,6 +60,21 @@ Remove a file
58
60
 
59
61
  FilePool.remove(fid)
60
62
 
63
+ ### Encryption
64
+
65
+ FilePool can store files symmetrically encrypted using AES-256-CBC. In order to
66
+ switch FilePool to encryption pass the location of a file containing the secret:
67
+
68
+ FilePool.setup '/var/lib/files', :secrets_file => '/etc/filepool_secrets.yml'
69
+
70
+ This file is initialized with a random secret if it is not present. The key and
71
+ initialization vector stored there will be used for all files. When you request
72
+ a file with the FilePool#path method FilePool decrypts it first and returns a path to
73
+ the decrypted file.
74
+
75
+ In encryption mode the filepool root directory name is suffixed with "_secured",
76
+ so that mixed mode (encryped and plain) operation is possible.
77
+
61
78
  ### Maintenance
62
79
 
63
80
  FilePool has a straight forward way of storing files. It doesn't use
@@ -1,3 +1,3 @@
1
1
  module FilePool
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
data/lib/file_pool.rb CHANGED
@@ -1,77 +1,10 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  require 'file_pool/version'
3
3
  require 'uuidtools'
4
- =begin
5
- <em>Robert Anniés (2012)</em>
4
+ require 'tempfile'
5
+ require 'openssl'
6
+ require 'yaml'
6
7
 
7
- == Introduction
8
-
9
- FilePool helps to manage a large number of files in a Ruby project. It
10
- takes care of the storage of files in a balanced directory tree and
11
- generates unique identifiers for all files. It also comes in handy
12
- when delaing with only a few files.
13
-
14
- FilePool does not deal with file meta information. It's only purpose
15
- is to return a file's location given a file identifier, which was
16
- generated when the file was added to the pool.
17
-
18
- The identifiers are strings of UUID Type 4 (random), which are also
19
- used as file names. The directory tree is a 3 level structure using
20
- the 3 first hexadecimal digits of a UUID as path. For example:
21
-
22
- 0/d/6/0d6f8dd9-8deb-4500-bb85-2d0796241963
23
- 0/c/f/0cfb082a-fd57-490c-978b-e47d5948bc8b
24
- 6/1/d/61ddfe33-13f3-4f71-9234-5fbbf5c4fc2c
25
-
26
- == Examples
27
-
28
- === Setup
29
- The root path and optionally a Logger must be defined:
30
-
31
- FilePool.setup '/var/lib/files'
32
-
33
- In a Rails project the file pool setup should be placed in an intializer:
34
-
35
- config/initializers/file_pool.rb
36
-
37
- === Usage
38
-
39
- Adding files (perhaps after completed upload)
40
-
41
- fid = FilePool.add('/Temp/p348dvhn4')
42
-
43
- Get location of previously added file
44
-
45
- path = FilePool.path(fid)
46
-
47
- Remove a file
48
-
49
- FilePool.remove(fid)
50
-
51
- == Maintenance
52
-
53
- FilePool has a straight forward way of storing files. It doesn't use
54
- any form of index. As long as you stick to directory structure
55
- outlined above you can:
56
-
57
- * move the entire pool somewhere else
58
- * split the pool using symbolic links or mount points to remote file systems
59
- * merge file pools by copying them into one
60
-
61
- There is no risk of overwriting, because UUID type 4 file names are
62
- unique. (up to an extremely small collision probability).
63
-
64
- == Notes
65
-
66
- Make sure to store the generated file identifiers safely. There is no
67
- way of identifying a file again when it's ID is lost. In doubt generate a hash
68
- value from the file and store it somewhere else.
69
-
70
- For large files the pool root should be on the same file system as the files
71
- added to the pool. Then adding a file returns immediately. Otherwise
72
- files will be copied which may take a significant time.
73
-
74
- =end
75
8
  module FilePool
76
9
 
77
10
  class InvalidFileId < Exception; end
@@ -84,8 +17,12 @@ module FilePool
84
17
  #
85
18
  # root (String)::
86
19
  # absolute path of the file pool's root directory under which all files will be stored.
87
- def self.setup root
20
+ # config_file_path (String)::
21
+ # path to the config file of the filepool.
22
+ def self.setup root, options={}
88
23
  @@root = root
24
+ @@crypted_mode = false
25
+ configure options[:secrets_file]
89
26
  end
90
27
 
91
28
  #
@@ -110,7 +47,12 @@ module FilePool
110
47
  newid = uuid
111
48
  target = path newid
112
49
 
113
- FileUtils.mkpath(id2dir newid)
50
+ if @@crypted_mode
51
+ FileUtils.mkpath(id2dir_secured newid)
52
+ path = crypt(path)
53
+ else
54
+ FileUtils.mkpath(id2dir newid)
55
+ end
114
56
  FileUtils.link(path, target)
115
57
 
116
58
  return newid
@@ -142,19 +84,52 @@ module FilePool
142
84
  end
143
85
 
144
86
  #
145
- # Return the path of a previously added file by its ID.
87
+ # Return the file's path corresponding to the passed file ID, no matter if it
88
+ # exists or not. In encrypting mode the file is first decrypted and the
89
+ # returned path will point to a temporary location of the decrypted file.
90
+ #
91
+ # To get the path of the encrypted file pass :decrypt => false, as an option.
146
92
  #
147
93
  # === Parameters:
148
94
  #
149
95
  # fid (String)::
150
96
  # File ID which was generated by a previous #add operation.
151
97
  #
98
+ # options (Hash)::
99
+ # :decrypt (true,false) In encryption mode don't decrypt, but return the encrypted file's path. Defaults to +true+.
100
+ #
152
101
  # === Return Value:
153
102
  #
154
- # :: *String*, absolute path of the file in the pool.
155
- def self.path fid
103
+ # :: *String*, absolute path of the file in the pool or to temporary location if it was decrypted.
104
+ def self.path fid, options={}
105
+ options[:decrypt] = true unless options[:decrypt] == false
106
+
156
107
  raise InvalidFileId unless valid?(fid)
157
- id2dir(fid) + "/#{fid}"
108
+
109
+ # file present in pool?
110
+ if File.file?(id2dir_secured(fid) + "/#{fid}")
111
+ # present in secured tree
112
+ if @@crypted_mode
113
+ if options[:decrypt]
114
+ # return path of decrypted file (tmp path)
115
+ decrypt id2dir_secured(fid) + "/#{fid}"
116
+ else
117
+ id2dir_secured(fid) + "/#{fid}"
118
+ end
119
+ else
120
+ id2dir_secured(fid) + "/#{fid}"
121
+ end
122
+ elsif File.file?(id2dir(fid) + "/#{fid}")
123
+ # present in plain tree
124
+ id2dir(fid) + "/#{fid}"
125
+ else
126
+ # not present
127
+ if @@crypted_mode
128
+ id2dir_secured(fid) + "/#{fid}"
129
+ else
130
+ id2dir(fid) + "/#{fid}"
131
+ end
132
+ end
158
133
  end
159
134
 
160
135
  #
@@ -166,7 +141,7 @@ module FilePool
166
141
  # fid (String)::
167
142
  # File ID which was generated by a previous #add operation.
168
143
  def self.remove! fid
169
- FileUtils.rm path(fid)
144
+ FileUtils.rm path(fid, :decrypt => false)
170
145
  end
171
146
 
172
147
  #
@@ -204,7 +179,8 @@ module FilePool
204
179
  # Time and Date of last add operation
205
180
 
206
181
  def self.stat
207
- all_files = Dir.glob("#{root}/*/*/*/*")
182
+ all_files = Dir.glob("#{root}_secured/*/*/*/*")
183
+ all_files << Dir.glob("#{root}/*/*/*/*")
208
184
  all_stats = all_files.map{|f| File.stat(f) }
209
185
 
210
186
  {
@@ -227,6 +203,11 @@ module FilePool
227
203
  "#{root}/#{fid[0,1]}/#{fid[1,1]}/#{fid[2,1]}"
228
204
  end
229
205
 
206
+ # secured path from fid without file name
207
+ def self.id2dir_secured fid
208
+ "#{root}_secured/#{fid[0,1]}/#{fid[1,1]}/#{fid[2,1]}"
209
+ end
210
+
230
211
  # return a new UUID type 4 (random) as String
231
212
  def self.uuid
232
213
  UUIDTools::UUID.random_create.to_s
@@ -250,4 +231,110 @@ module FilePool
250
231
  (sortedarr[medpt1] + sortedarr[medpt2]).to_f / 2
251
232
  end
252
233
 
234
+ #
235
+ # Crypt a file and store the result in the temp.
236
+ #
237
+ # Returns the path to the crypted file.
238
+ #
239
+ # === Parameters:
240
+ #
241
+ # path (String)::
242
+ # path of the file to crypt.
243
+ #
244
+ # === Return Value:
245
+ #
246
+ # :: *String*Path and name of the crypted file.
247
+ def self.crypt path
248
+ # Crypt the file in the temp folder and copy after
249
+ cipher = create_cipher
250
+ result = Tempfile.new uuid
251
+ crypted_content = cipher.update(File.read(path))
252
+ crypted_content << cipher.final
253
+ result.write crypted_content
254
+ result.close
255
+ result.path
256
+ end
257
+
258
+ #
259
+ # Decrypt a file and give a path to it.
260
+ #
261
+ # Returns the path to the decrypted file.
262
+ #
263
+ # === Parameters:
264
+ #
265
+ # path (String)::
266
+ # path of the file to decrypt.
267
+ #
268
+ # === Return Value:
269
+ #
270
+ # :: *String*Path and name of the crypted file.
271
+ def self.decrypt path
272
+ decipher = create_decipher
273
+ # Now decrypt the data:
274
+ decrypted_content = decipher.update(File.read(path))
275
+ decrypted_content << decipher.final
276
+ # Put it in a temp file
277
+ output = Tempfile.new uuid
278
+ output.write decrypted_content
279
+ output.open
280
+ output.path
281
+ end
282
+
283
+ #
284
+ # Creates a cipher to encrypt data.
285
+ #
286
+ # Returns the cipher.
287
+ #
288
+ # === Return Value:
289
+ #
290
+ # :: *Openssl*Cipher object.
291
+ def self.create_cipher
292
+ cipher = OpenSSL::Cipher::AES.new(256, :CBC)
293
+ cipher.encrypt
294
+ cipher.key = @@key
295
+ cipher.iv = @@iv
296
+ cipher
297
+ end
298
+
299
+ #
300
+ # Creates a decipher to decrypt data.
301
+ #
302
+ # Returns the decipher.
303
+ #
304
+ # === Return Value:
305
+ #
306
+ # :: *Openssl*Cipher object
307
+ def self.create_decipher
308
+ decipher = OpenSSL::Cipher::AES.new(256, :CBC)
309
+ decipher.decrypt
310
+ decipher.key = @@key
311
+ decipher.iv = @@iv
312
+ decipher
313
+ end
314
+
315
+ #
316
+ # Retrieves configuration from config file or creates
317
+ # a new one in case there's none available.
318
+ #
319
+ def self.configure config_file
320
+ unless config_file.nil?
321
+ @@crypted_mode = true
322
+ begin
323
+ config = YAML.load_file(config_file)
324
+ @@iv = config[:iv]
325
+ @@key = config[:key]
326
+ rescue Errno::ENOENT
327
+ cipher = OpenSSL::Cipher::AES.new(256, :CBC)
328
+ @@iv = cipher.random_iv
329
+ @@key = cipher.random_key
330
+ cipher.key = @@key
331
+ cfg = File.open(config_file, 'w')
332
+ cfg.write({:iv => @@iv, :key => @@key}.to_yaml)
333
+ cfg.close
334
+ File.chmod(0400, config_file)
335
+ rescue => other_error
336
+ raise "FilePool: Could not load secrets from #{config_file}: #{other_error}"
337
+ end
338
+ end
339
+ end
253
340
  end
@@ -1,5 +1,8 @@
1
1
  require 'rubygems'
2
- require 'shoulda'
2
+ require 'bundler/setup'
3
+
4
+ require 'test/unit'
5
+ require 'shoulda-context'
3
6
  require 'file_pool'
4
7
 
5
8
  class FilePoolTest < Test::Unit::TestCase
@@ -21,7 +24,7 @@ class FilePoolTest < Test::Unit::TestCase
21
24
  assert UUIDTools::UUID.parse(fid).valid?
22
25
 
23
26
  md5_orig = Digest::MD5.hexdigest(File.open(@test_dir+"/a").read)
24
- md5_pooled = Digest::MD5.hexdigest(File.open(@pool_root + "/#{fid[0,1]}/#{fid[1,1]}/#{fid[2,1]}/#{fid}").read)
27
+ md5_pooled = Digest::MD5.hexdigest(File.open(FilePool.path(fid)).read)
25
28
 
26
29
  assert_equal md5_orig, md5_pooled
27
30
  end
@@ -0,0 +1,100 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require 'test/unit'
5
+ require 'shoulda-context'
6
+ require 'file_pool'
7
+
8
+ class FilePoolEncryptionTest < Test::Unit::TestCase
9
+
10
+ def setup
11
+ @test_dir = "#{File.dirname(__FILE__)}/files"
12
+ @pool_root = "#{File.dirname(__FILE__)}/fp_root"
13
+ @file_pool_config = "#{File.dirname(__FILE__)}/file_pool_cfg.yml"
14
+ FilePool.setup @pool_root, :secrets_file => @file_pool_config
15
+ end
16
+
17
+ def teardown
18
+ FileUtils.rm_r(Dir.glob @pool_root+"/*")
19
+ FileUtils.rm_r(Dir.glob "#{@pool_root}_secured/*")
20
+ FileUtils.rm_r(Dir.glob @file_pool_config)
21
+ end
22
+
23
+ context "File Pool" do
24
+ should "store encrypted files" do
25
+ fid = FilePool.add(@test_dir+"/a")
26
+
27
+ assert UUIDTools::UUID.parse(fid).valid?
28
+
29
+ md5_orig = Digest::MD5.hexdigest(File.open(@test_dir+"/a").read)
30
+ md5_pooled = Digest::MD5.hexdigest(File.open(FilePool.path(fid)).read)
31
+
32
+ assert_equal md5_orig, md5_pooled
33
+ end
34
+
35
+ should "return path from stored encrypted files is in the tmp folder" do
36
+
37
+ fida = FilePool.add(@test_dir+"/a")
38
+ assert UUIDTools::UUID.parse(fida).valid?
39
+
40
+ fidb = FilePool.add(@test_dir+"/b")
41
+ assert UUIDTools::UUID.parse(fidb).valid?
42
+
43
+ fidc = FilePool.add(@test_dir+"/c")
44
+ assert UUIDTools::UUID.parse(fidc).valid?
45
+
46
+ fidd = FilePool.add!(@test_dir+"/d")
47
+ assert UUIDTools::UUID.parse(fidd).valid?
48
+
49
+ assert_equal Digest::MD5.hexdigest(File.open(@test_dir+"/a").read),
50
+ Digest::MD5.hexdigest(File.open(FilePool.path(fida)).read)
51
+ assert_equal Digest::MD5.hexdigest(File.open(@test_dir+"/b").read),
52
+ Digest::MD5.hexdigest(File.open(FilePool.path(fidb)).read)
53
+ assert_equal Digest::MD5.hexdigest(File.open(@test_dir+"/c").read),
54
+ Digest::MD5.hexdigest(File.open(FilePool.path(fidc)).read)
55
+ assert_equal Digest::MD5.hexdigest(File.open(@test_dir+"/d").read),
56
+ Digest::MD5.hexdigest(File.open(FilePool.path(fidd)).read)
57
+
58
+ assert_equal Dir.tmpdir, File.dirname(FilePool.path(fida))
59
+ assert_equal Dir.tmpdir, File.dirname(FilePool.path(fidb))
60
+ assert_equal Dir.tmpdir, File.dirname(FilePool.path(fidc))
61
+ assert_equal Dir.tmpdir,File.dirname( FilePool.path(fidd))
62
+ end
63
+
64
+ should "remove files from encrypted pool" do
65
+
66
+ fidb = FilePool.add(@test_dir+"/b")
67
+ fidc = FilePool.add!(@test_dir+"/c")
68
+ fidd = FilePool.add!(@test_dir+"/d")
69
+
70
+ path_c = FilePool.path(fidc, :decrypt => false)
71
+ FilePool.remove(fidc)
72
+
73
+ assert !File.exist?(path_c)
74
+ assert File.exist?(FilePool.path(fidb, :decrypt => false))
75
+ assert File.exist?(FilePool.path(fidd, :decrypt => false))
76
+
77
+ end
78
+
79
+ should "throw exceptions when using add! and remove! on failure in encrypted mode" do
80
+ assert_raises(FilePool::InvalidFileId) do
81
+ FilePool.remove!("invalid-id")
82
+ end
83
+
84
+ assert_raises(Errno::ENOENT) do
85
+ FilePool.remove!("61e9b2d1-1738-440d-9b3d-e3c64876f2b0")
86
+ end
87
+
88
+ assert_raises(Errno::ENOENT) do
89
+ FilePool.add!("/not/here/foo.png")
90
+ end
91
+
92
+ end
93
+
94
+ should "not throw exceptions when using add and remove on failure in encrypted mode" do
95
+ assert !FilePool.remove("invalid-id")
96
+ assert !FilePool.remove("61e9b2d1-1738-440d-9b3d-e3c64876f2b0")
97
+ assert !FilePool.add("/not/here/foo.png")
98
+ end
99
+ end
100
+ end
metadata CHANGED
@@ -1,103 +1,69 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: file_pool
3
- version: !ruby/object:Gem::Version
4
- hash: 23
5
- prerelease:
6
- segments:
7
- - 0
8
- - 2
9
- - 0
10
- version: 0.2.0
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.0
11
5
  platform: ruby
12
- authors:
13
- - "robokopp (Robert Anni\xC3\xA9s)"
6
+ authors:
7
+ - robokopp (Robert Anniés)
14
8
  autorequire:
15
9
  bindir: bin
16
10
  cert_chain: []
17
-
18
- date: 2012-09-17 00:00:00 Z
19
- dependencies:
20
- - !ruby/object:Gem::Dependency
21
- name: shoulda
22
- prerelease: false
23
- requirement: &id001 !ruby/object:Gem::Requirement
24
- none: false
25
- requirements:
26
- - - ">="
27
- - !ruby/object:Gem::Version
28
- hash: 3
29
- segments:
30
- - 0
31
- version: "0"
32
- type: :development
33
- version_requirements: *id001
34
- - !ruby/object:Gem::Dependency
11
+ date: 2017-02-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
35
14
  name: uuidtools
36
- prerelease: false
37
- requirement: &id002 !ruby/object:Gem::Requirement
38
- none: false
39
- requirements:
40
- - - ~>
41
- - !ruby/object:Gem::Version
42
- hash: 15
43
- segments:
44
- - 2
45
- - 1
46
- - 2
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
47
19
  version: 2.1.2
48
20
  type: :runtime
49
- version_requirements: *id002
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 2.1.2
50
27
  description: |
51
28
  FilePool helps to manage a large number of files in a Ruby
52
29
  project. It takes care of the storage of files in a balanced directory
53
30
  tree and generates unique identifiers for all files.
54
-
55
- email:
31
+ email:
56
32
  - robokopp@fernwerk.net
57
33
  executables: []
58
-
59
34
  extensions: []
60
-
61
- extra_rdoc_files:
35
+ extra_rdoc_files:
36
+ - README.md
37
+ files:
62
38
  - README.md
63
- files:
64
39
  - lib/file_pool.rb
65
40
  - lib/file_pool/version.rb
66
- - README.md
67
41
  - test/test_file_pool.rb
42
+ - test/test_file_pool_encryption.rb
68
43
  homepage: https://github.com/robokopp/file_pool
69
44
  licenses: []
70
-
45
+ metadata: {}
71
46
  post_install_message:
72
47
  rdoc_options: []
73
-
74
- require_paths:
48
+ require_paths:
75
49
  - lib
76
- required_ruby_version: !ruby/object:Gem::Requirement
77
- none: false
78
- requirements:
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ requirements:
79
52
  - - ">="
80
- - !ruby/object:Gem::Version
81
- hash: 3
82
- segments:
83
- - 0
84
- version: "0"
85
- required_rubygems_version: !ruby/object:Gem::Requirement
86
- none: false
87
- requirements:
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ requirements:
88
57
  - - ">="
89
- - !ruby/object:Gem::Version
90
- hash: 3
91
- segments:
92
- - 0
93
- version: "0"
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
94
60
  requirements: []
95
-
96
61
  rubyforge_project:
97
- rubygems_version: 1.8.24
62
+ rubygems_version: 2.4.5
98
63
  signing_key:
99
- specification_version: 3
64
+ specification_version: 4
100
65
  summary: Manage a large number files in a pool
101
- test_files:
66
+ test_files:
102
67
  - test/test_file_pool.rb
68
+ - test/test_file_pool_encryption.rb
103
69
  has_rdoc: