file_pool 0.2.0 → 0.3.0

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