nearline 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,224 @@
1
+ module Nearline
2
+ module Models
3
+
4
+ # Represents file metadata and possible related FileContent
5
+ # for a single file on a single system
6
+ class ArchivedFile < ActiveRecord::Base
7
+ belongs_to :file_content
8
+ has_and_belongs_to_many :manifests
9
+
10
+ def self.create_for(system_name, file_path, manifest)
11
+
12
+ file_information = FileInformation.new(system_name, file_path, manifest)
13
+
14
+ # The path doesn't actually exist and fails a File.stat
15
+ return nil if file_information.path_hash.nil?
16
+
17
+ # If we find an exising entry, use it
18
+ hit = self.find_by_path_hash(file_information.path_hash)
19
+ return hit unless hit.nil?
20
+
21
+ # We need to create a record for either a directory or file
22
+ archived_file = ArchivedFile.new(
23
+ file_information.archived_file_parameters
24
+ )
25
+
26
+ # Find a new directory
27
+ if (file_information.is_directory)
28
+ archived_file.save!
29
+ return archived_file
30
+ end
31
+
32
+ # Find a new file that needs persisted
33
+ archived_file.file_content.file_size =
34
+ [file_information.stat.size].pack('Q').unpack('L').first # HACK for Windows
35
+ archived_file.persist(manifest)
36
+ archived_file.save!
37
+ archived_file
38
+
39
+ # TODO: Symbolic links, block devices, ...?
40
+ end
41
+
42
+ class FileInformation
43
+ attr_reader :path_hash, :stat, :is_directory, :archived_file_parameters
44
+ def initialize(system_name, file_path, manifest)
45
+ @manifest = manifest
46
+ @stat = read_stat(file_path)
47
+ @is_directory = File.directory?(file_path)
48
+ @path_hash = generate_path_hash(system_name, file_path)
49
+ @archived_file_parameters = build_parameters(system_name, file_path)
50
+ end
51
+
52
+ def read_stat(file_path)
53
+ stat = nil
54
+ begin
55
+ stat = File.stat(file_path)
56
+ rescue
57
+ @manifest.add_log("File not found on stat: #{file_path}")
58
+ end
59
+ stat
60
+ end
61
+
62
+ def generate_path_hash(system_name, file_path)
63
+ return nil if @stat.nil?
64
+ target = [system_name,
65
+ file_path,
66
+ @stat.uid,
67
+ @stat.gid,
68
+ @stat.mtime.to_i,
69
+ @stat.mode].join(':')
70
+ Digest::SHA1.hexdigest(target)
71
+ end
72
+
73
+ def file_content_entry_for_files_only
74
+ return FileContent.fresh_entry unless @is_directory
75
+ return nil
76
+ end
77
+
78
+ def build_parameters(system_name, file_path)
79
+ return nil if @stat.nil?
80
+ {
81
+ :system_name => system_name,
82
+ :path => file_path,
83
+ :path_hash => @path_hash,
84
+ :file_content => file_content_entry_for_files_only,
85
+ :uid => @stat.uid,
86
+ :gid => @stat.gid,
87
+ :mtime => @stat.mtime.to_i,
88
+ :mode => @stat.mode,
89
+ :is_directory => @is_directory
90
+ }
91
+ end
92
+
93
+ end
94
+
95
+ def restore(*args)
96
+ @options = args.extract_options!
97
+ if (self.is_directory)
98
+ FileUtils.mkdir_p option_override(:path)
99
+ restore_metadata
100
+ return
101
+ end
102
+ target_path = File.dirname(option_override(:path))
103
+ if (!File.exist? target_path)
104
+ FileUtils.mkdir_p target_path
105
+ end
106
+ f = File.open(option_override(:path), "wb")
107
+ self.file_content.restore_to(f)
108
+ f.close
109
+ restore_metadata
110
+ return
111
+ end
112
+
113
+ def option_override(key)
114
+ if (@options.has_key?(key))
115
+ return @options[key]
116
+ end
117
+ return self.send(key.to_s)
118
+ end
119
+
120
+ def restore_metadata
121
+ path = option_override(:path)
122
+ mtime = option_override(:mtime)
123
+ uid = option_override(:uid)
124
+ gid = option_override(:gid)
125
+ mode = option_override(:mode)
126
+ File.utime(0,Time.at(mtime),path)
127
+ File.chown(uid, gid, path)
128
+ File.chmod(mode, path)
129
+ end
130
+
131
+ def before_destroy
132
+ self.file_content.orphan_check if !self.file_content.nil?
133
+ end
134
+
135
+ def orphan_check
136
+ if self.manifests.size == 1
137
+ self.destroy
138
+ end
139
+ end
140
+
141
+ # Actually persist the file to the repository
142
+ # It has already been determined that a new ArchivedFile record is
143
+ # necessary and the file requires persisting
144
+ #
145
+ # But, the content may be identical to something else, and we
146
+ # won't know that until we complete the process and have to
147
+ # clean up our mess.
148
+ def persist(manifest)
149
+ whole_file_hash = Digest::SHA1.new
150
+ file_size = 0
151
+ begin
152
+ file_size = read_file_counting_bytes(whole_file_hash)
153
+ rescue
154
+ manifest.add_log "Got error '#{$!}' on path: #{self.path}"
155
+ self.orphan_check
156
+ return nil
157
+ end
158
+
159
+ size_check(file_size, manifest)
160
+
161
+ # Do we have a unique sequence?
162
+ key = whole_file_hash.hexdigest
163
+ return self if unique_sequence_processed?(key, manifest)
164
+
165
+ # Handle the case where the sequence is not unique...
166
+ clean_up_duplicate_content
167
+ replace_content(key)
168
+ self
169
+ end
170
+
171
+ def read_file_counting_bytes(whole_file_hash)
172
+ sequencer = FileSequencer.new(self.file_content)
173
+ file_size = 0
174
+ buffer = ""
175
+ File.open(self.path, "rb") do |io|
176
+ while (!io.eof) do
177
+ io.read(Block::MAX_SIZE, buffer)
178
+ file_size += buffer.size
179
+ whole_file_hash.update(buffer)
180
+ block = Block.for_content(buffer)
181
+ sequencer.preserve_block(block)
182
+ end
183
+ end
184
+ return file_size
185
+ end
186
+
187
+ def size_check(file_size, manifest)
188
+ if file_size != self.file_content.file_size
189
+ manifest.add_log "recorded file length #{file_size} " +
190
+ "does not match #{self.file_content.file_size} " +
191
+ "reported by the file system on path: #{self.path}"
192
+ end
193
+ end
194
+
195
+ def verify_content(manifest)
196
+ unless (self.file_content.verified?)
197
+ manifest.add_log "failed verification on path: #{self.path}"
198
+ end
199
+ end
200
+
201
+ def unique_sequence_processed?(key,manifest)
202
+ if self.file_content.unique_fingerprint?(key)
203
+ self.file_content.fingerprint = key
204
+ self.file_content.save!
205
+ verify_content(manifest)
206
+ return true
207
+ end
208
+ false
209
+ end
210
+
211
+ def clean_up_duplicate_content
212
+ Sequence.delete_all("file_content_id=#{self.file_content.id}")
213
+ self.file_content.orphan_check
214
+ end
215
+
216
+ def replace_content(key)
217
+ self.file_content = FileContent.find_by_fingerprint(key)
218
+ self.save!
219
+ end
220
+
221
+ end
222
+
223
+ end
224
+ end
@@ -0,0 +1,56 @@
1
+ require 'active_record'
2
+
3
+ module Nearline
4
+ module Models
5
+
6
+ # Represents a unit of file content which may be
7
+ # freely shared across the repository
8
+ # Its sole responsibility is to preserve and provide
9
+ # content access
10
+ class Block < ActiveRecord::Base
11
+ require "zlib"
12
+
13
+ has_many :sequences
14
+
15
+ MAX_SIZE = (64 * 1024)-1
16
+
17
+ def attempt_compression
18
+ return if (self.is_compressed)
19
+ # TODO: Have a bump-the-compression option, here?
20
+ candidate_content = Zlib::Deflate.deflate(self.bulk_content)
21
+ if candidate_content.length < self.bulk_content.length
22
+ self.is_compressed = true
23
+ self.bulk_content = candidate_content
24
+ end
25
+ end
26
+
27
+ def calculate_fingerprint
28
+ self.fingerprint = Digest::SHA1.hexdigest(content)
29
+ end
30
+
31
+ def content
32
+ if (self.is_compressed)
33
+ return Zlib::Inflate.inflate(self.bulk_content)
34
+ end
35
+ self.bulk_content
36
+ end
37
+
38
+ def self.for_content(x)
39
+ block = Models::Block.new(:bulk_content => x)
40
+ block.calculate_fingerprint
41
+ found = find_by_fingerprint(block.fingerprint)
42
+ return found if !found.nil?
43
+ block.attempt_compression
44
+ block.save!
45
+ block
46
+ end
47
+
48
+ def orphan_check
49
+ if self.sequences.size == 0
50
+ self.destroy
51
+ end
52
+ end
53
+
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,85 @@
1
+ module Nearline
2
+ module Models
3
+
4
+ # Has the responsibility of identifying and
5
+ # verifying content
6
+ class FileContent < ActiveRecord::Base
7
+ has_many :sequences
8
+ has_many :archived_files
9
+
10
+ def self.fresh_entry
11
+ file_content = FileContent.new
12
+ file_content.save!
13
+ file_content
14
+ end
15
+
16
+ def restore_to(io)
17
+ sequencer = FileSequencer.new(self)
18
+ sequencer.restore_blocks(io)
19
+ end
20
+
21
+ def verified?
22
+ sequencer = FileSequencer.new(self)
23
+ sequencer.verified?
24
+ end
25
+
26
+ def orphan_check
27
+ if (self.archived_files.size == 1)
28
+ sequences.each do |s|
29
+ s.destroy
30
+ s.block.orphan_check
31
+ end
32
+ self.destroy
33
+ end
34
+ end
35
+
36
+ def unique_fingerprint?(key)
37
+ hit = FileContent.connection.select_one(
38
+ "select id from file_contents where fingerprint='#{key}'"
39
+ )
40
+ return hit.nil?
41
+ end
42
+
43
+ end
44
+
45
+ # Has the responsibility of preserving
46
+ # cardinality of stored blocks
47
+ class Sequence < ActiveRecord::Base
48
+ belongs_to :block
49
+ belongs_to :file_content
50
+ end
51
+
52
+ class FileSequencer
53
+ def initialize(file_content)
54
+ @inc = 0
55
+ @file_content = file_content
56
+ end
57
+
58
+ def preserve_block(block)
59
+ @inc += 1
60
+ sequence = Sequence.new(
61
+ :sequence => @inc,
62
+ :file_content_id => @file_content.id,
63
+ :block_id => block.id
64
+ )
65
+ sequence.save!
66
+ sequence
67
+ end
68
+
69
+ def restore_blocks(io)
70
+ @file_content.sequences.each do |seq|
71
+ io.write(seq.block.content)
72
+ end
73
+ end
74
+
75
+ def verified?
76
+ whole_file_hash = Digest::SHA1.new
77
+ @file_content.sequences.each do |seq|
78
+ whole_file_hash.update(seq.block.content)
79
+ end
80
+ @file_content.fingerprint == whole_file_hash.hexdigest
81
+ end
82
+ end
83
+
84
+ end
85
+ end
@@ -0,0 +1,11 @@
1
+ module Nearline
2
+ module Models
3
+
4
+ # A simple message log for reporting problems during the creation
5
+ # of a Manifest
6
+ class Log < ActiveRecord::Base
7
+
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,106 @@
1
+ module Nearline
2
+ module Models
3
+
4
+ # Recuses paths and finds the files to back up
5
+ class FileFinder
6
+ require 'find'
7
+ def self.recurse(paths, exclusions)
8
+ paths.each do |path|
9
+ Find.find(path) do |f|
10
+ exclusions.each do |exclusion|
11
+ Find.prune if f =~ /#{exclusion}/
12
+ end
13
+ yield f
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ # A Manifest represents the corpus of ArchivedFiles and
20
+ # set of Log messages resulting from a backup attempt
21
+ class Manifest < ActiveRecord::Base
22
+
23
+ has_and_belongs_to_many :archived_files
24
+ has_many :logs
25
+
26
+ # Just needed when you create a manifest
27
+ attr_accessor :backup_paths
28
+ # Just needed when you create a manifest
29
+ attr_accessor :backup_exclusions
30
+
31
+ # Underlying implementation of Nearline.backup
32
+ def self.backup(system_name, backup_paths, backup_exclusions)
33
+ manifest = self.new(:system_name => system_name)
34
+ manifest.save!
35
+
36
+ FileFinder.recurse(backup_paths, backup_exclusions) do |file_name|
37
+ af = ArchivedFile.create_for(system_name, file_name, manifest)
38
+ if (!af.nil?)
39
+ manifest.archived_files << af
40
+ $stdout.write "#{af.path} #{Time.at(af.mtime).asctime}"
41
+ if (!af.file_content.nil?)
42
+ $stdout.write" (#{af.file_content.file_size} bytes)"
43
+ end
44
+ $stdout.write("\n")
45
+ end
46
+ end
47
+
48
+ manifest.completed_at = Time.now
49
+ manifest.save!
50
+ manifest
51
+ end
52
+
53
+ # Find the latest Manifest for a system
54
+ def self.latest_for(system_name)
55
+ m_result = self.connection.select_one("select id from manifests where system_name='#{system_name}' order by created_at desc")
56
+ raise "No manifest found" if m_result.nil?
57
+ self.find(m_result["id"])
58
+ end
59
+
60
+ # Find all Manifest entries which have never finished.
61
+ #
62
+ # They are:
63
+ # * Currently under-way
64
+ # * Have failed in some untimely way
65
+ def self.incomplete_manifests
66
+ self.find_all_by_completed_at(nil)
67
+ end
68
+
69
+ def self.restore_all_missing(system_name)
70
+ manifest = latest_for(system_name)
71
+ manifest.restore_all_missing
72
+ end
73
+
74
+ # Restore all missing files from this manifest back to the filesystem
75
+ def restore_all_missing
76
+ files_restored = []
77
+ self.archived_files.each do |af|
78
+ begin
79
+ File.stat(af.path)
80
+ rescue
81
+ af.restore
82
+ files_restored << af.path
83
+ end
84
+ end
85
+ return files_restored
86
+ end
87
+
88
+ def add_log(message)
89
+ puts message
90
+ log = Nearline::Models::Log.new({:message => message, :manifest_id => self.id})
91
+ log.save!
92
+ end
93
+
94
+ def before_destroy
95
+ archived_files.each do |af|
96
+ af.orphan_check
97
+ end
98
+ logs.each do |log|
99
+ log.destroy
100
+ end
101
+ end
102
+
103
+ end
104
+
105
+ end
106
+ end
@@ -0,0 +1,98 @@
1
+ module Nearline
2
+ module_function
3
+
4
+
5
+ # Establishes the ActiveRecord connection
6
+ #
7
+ # Accepts a Hash to establish the connection or
8
+ # a String referring to an entry in config/database.yml.
9
+ #
10
+ # Will establish the Nearline database tables if they are absent.
11
+ #
12
+ # Stomps on any ActiveRecord::Base.establish_connection you might
13
+ # have already established.
14
+ #
15
+ # === Examples
16
+ # Nearline.connect!({:adapter => 'sqlite3', :database => 'data/sqlite.db'})
17
+ #
18
+ # Nearline.connect! 'production'
19
+ #
20
+ def connect!(config="development")
21
+ if (config.class.to_s == 'String')
22
+ ActiveRecord::Base.establish_connection(YAML.load_file("config/database.yml")[config])
23
+ end
24
+
25
+ if (config.class.to_s == 'Hash')
26
+ ActiveRecord::Base.establish_connection(config)
27
+ end
28
+
29
+ unless Nearline::Models::Block.table_exists?
30
+ Nearline::Models.generate_schema
31
+ end
32
+ Nearline::Models::Block.connected?
33
+ end
34
+
35
+ # Establishes a connection only to the Nearline ActiveDirectory models
36
+ #
37
+ # Will not change the ActiveRecord::Base connection
38
+ #
39
+ # Will not establish Nearline tables in the database
40
+ #
41
+ # Accepts a Hash to establish the connection or
42
+ # a String referring to an entry in config/database.yml.
43
+ # === Examples
44
+ # Nearline.connect({:adapter => 'sqlite3', :database => 'data/sqlite.db'})
45
+ #
46
+ # Nearline.connect 'production'
47
+ #
48
+ def connect(config="development")
49
+ # These are the ActiveRecord models in place
50
+ # Each one needs an explicit establish_connection
51
+ # if you don't want it running though ActiveRecord::Base
52
+ models = [
53
+ Nearline::Models::ArchivedFile,
54
+ Nearline::Models::Block,
55
+ Nearline::Models::FileContent,
56
+ Nearline::Models::Manifest,
57
+ Nearline::Models::Sequence,
58
+ Nearline::Models::Log
59
+ ]
60
+ if (config.class.to_s == 'String')
61
+ hash = YAML.load_file("config/database.yml")[config]
62
+ else
63
+ hash = config
64
+ end
65
+
66
+ models.each do |m|
67
+ m.establish_connection(hash)
68
+ end
69
+ Nearline::Models::Block.connected?
70
+ end
71
+
72
+ # Performs a backup labeled for system_name,
73
+ # Recursing through an array of backup_paths,
74
+ # Excluding any path matching any of the regular
75
+ # expressions in the backup_exclusions array.
76
+ #
77
+ # Expects the Nearline database connection has already
78
+ # been established
79
+ #
80
+ # Returns a Manifest for the backup
81
+ def backup(system_name, backup_paths,backup_exclusions= [])
82
+ Nearline::Models::Manifest.backup(system_name, backup_paths, backup_exclusions)
83
+ end
84
+
85
+ # Restore all missing files from the latest backup
86
+ # for system_name
87
+ #
88
+ # All updated or existing files are left alone
89
+ #
90
+ # Expects the Nearline database connection has already
91
+ # been established
92
+ #
93
+ # Returns an Array of paths restored
94
+ def restore(system_name)
95
+ Nearline::Models::Manifest.restore_all_missing(system_name)
96
+ end
97
+
98
+ end
@@ -0,0 +1,90 @@
1
+ module Nearline
2
+ module Models
3
+
4
+ module_function
5
+
6
+ def destroy_schema
7
+ ActiveRecord::Schema.define do
8
+ drop_table :blocks
9
+ drop_table :file_contents
10
+ drop_table :sequences
11
+ drop_table :archived_files
12
+ drop_table :manifests
13
+ drop_table :archived_files_manifests
14
+ drop_table :logs
15
+ end
16
+ end
17
+
18
+ def empty_schema
19
+ Nearline::Models::Manifest.destroy_all
20
+ end
21
+
22
+ def generate_schema
23
+ ActiveRecord::Schema.define do
24
+
25
+ create_table :blocks do |t|
26
+ t.column :fingerprint, :string, :length => 40, :null => false
27
+ t.column :bulk_content, :binary
28
+ t.column :is_compressed, :boolean, :default => false
29
+ end
30
+
31
+ add_index :blocks, [:fingerprint], :unique => true
32
+
33
+ create_table :file_contents do |t|
34
+ t.column :fingerprint, :string, :length => 40
35
+ t.column :file_size, :integer, :default => 0
36
+ end
37
+
38
+ create_table :sequences do |t|
39
+ t.column :sequence, :integer, :null => false
40
+ t.column :block_id, :integer, :null => false
41
+ t.column :file_content_id, :integer, :null => false
42
+ end
43
+
44
+ add_index :sequences, [:sequence, :file_content_id], :unique => true,
45
+ :name => "sequence_jn_index"
46
+
47
+ create_table :archived_files do |t|
48
+ t.column :system_name, :string, :null => false
49
+ t.column :path, :text, :null => false
50
+ t.column :path_hash, :string, :null => false, :length => 40
51
+ t.column :file_content_id, :integer
52
+ t.column :uid, :integer, :default => -1
53
+ t.column :gid, :integer, :default => -1
54
+ t.column :mtime, :integer, :default => 0
55
+ t.column :mode, :integer, :default => 33206 # "chmod 100666"
56
+ t.column :is_directory, :boolean
57
+ end
58
+
59
+ add_index :archived_files, [:path_hash], :unique => true
60
+
61
+ # Manifests are the reference to a collection of archived files
62
+ create_table :manifests do |t|
63
+ t.column :system_name, :string
64
+ t.column :created_at, :datetime
65
+ t.column :completed_at, :datetime
66
+ end
67
+
68
+ # Joins archived files across manifests so file references may be recycled
69
+ create_table :archived_files_manifests, :id => false do |t|
70
+ t.column :archived_file_id, :integer
71
+ t.column :manifest_id, :integer
72
+ end
73
+
74
+ add_index :archived_files_manifests,
75
+ [:archived_file_id, :manifest_id], {
76
+ :unique => true,
77
+ :name => "manifest_jn_index"
78
+ }
79
+
80
+ # Keeps a record of problems during backup related to a manifest
81
+ create_table :logs do |t|
82
+ t.column :manifest_id, :integer, :null => false
83
+ t.column :message, :text
84
+ t.column :created_at, :datetime
85
+ end
86
+ end
87
+ end
88
+
89
+ end
90
+ end
data/lib/nearline.rb ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Nearline
4
+ # Copyright (C) 2008 Robert J. Osborne
5
+
6
+ require 'nearline/module_methods'
7
+
8
+ # ActiveRecord database definitions
9
+ require 'nearline/schema'
10
+
11
+ # ActiveRecord models
12
+ require 'nearline/block'
13
+ require 'nearline/file_content'
14
+ require 'nearline/archived_file'
15
+ require 'nearline/log'
16
+ require 'nearline/manifest'
data/tasks/clean.rake ADDED
@@ -0,0 +1,3 @@
1
+ require 'rake/clean'
2
+
3
+ CLEAN.include("data","html","pkg","coverage", "temp")
@@ -0,0 +1,25 @@
1
+ require 'rake'
2
+ require 'rake/gempackagetask'
3
+
4
+ SPEC = Gem::Specification.new do |s|
5
+ s.name = "nearline"
6
+ s.version = "0.0.1"
7
+ s.author = "Robert J. Osborne"
8
+ s.email = "rjo1970@gmail.com"
9
+ s.summary = "Nearline is a near-line backup and recovery solution"
10
+ s.description = %{
11
+ Nearline is a library to make managing near-line file repositories
12
+ simple and eleant in pure Ruby.
13
+ }
14
+ s.rubyforge_project = "nearline"
15
+ s.files = FileList["{tests,lib,doc,tasks}/**/*"].exclude("rdoc").to_a
16
+ s.add_dependency("activerecord", '>= 2.0.2')
17
+ s.require_path = "lib"
18
+ s.autorequire = "nearline"
19
+ s.test_file = "test/nearline_test.rb"
20
+ s.has_rdoc = true
21
+ end
22
+
23
+ Rake::GemPackageTask.new(SPEC) do |pkg|
24
+ pkg.need_tar = true
25
+ end
data/tasks/rcov.rake ADDED
@@ -0,0 +1,13 @@
1
+ task :rcov => [:clean] do
2
+ begin
3
+ require 'rcov/rcovtask'
4
+
5
+ Rcov::RcovTask.new do |t|
6
+ t.libs << "test"
7
+ t.rcov_opts = ['--text-report']
8
+ t.test_files = FileList['test/nearline_test.rb']
9
+ t.verbose = true
10
+ end
11
+ rescue LoadError => no_rcov
12
+ end
13
+ end
data/tasks/test.rake ADDED
@@ -0,0 +1,4 @@
1
+ desc "Test nearline"
2
+ task :test => [:clean] do
3
+ require 'test/nearline_test'
4
+ end
@@ -0,0 +1,17 @@
1
+ $:.unshift File.join(File.dirname(__FILE__), "..", "test")
2
+ # This is the suite of tests to run against Nearline
3
+ require 'test/unit'
4
+
5
+ require 'utilities'
6
+
7
+ $data_path = File.join(File.dirname(__FILE__), "..", "data")
8
+ unless File.exist?($data_path)
9
+ FileUtils.mkdir $data_path
10
+ end
11
+
12
+ require 'schema_test'
13
+ require 'block_test'
14
+ require 'nearline_module_test'
15
+ require 'file_content_test'
16
+ require 'archived_file_test'
17
+ require 'manifest_test'
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.9.4
3
+ specification_version: 1
4
+ name: nearline
5
+ version: !ruby/object:Gem::Version
6
+ version: 0.0.1
7
+ date: 2008-04-03 00:00:00 -04:00
8
+ summary: Nearline is a near-line backup and recovery solution
9
+ require_paths:
10
+ - lib
11
+ email: rjo1970@gmail.com
12
+ homepage:
13
+ rubyforge_project: nearline
14
+ description: Nearline is a library to make managing near-line file repositories simple and eleant in pure Ruby.
15
+ autorequire: nearline
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ post_install_message:
29
+ authors:
30
+ - Robert J. Osborne
31
+ files:
32
+ - lib/nearline
33
+ - lib/nearline/archived_file.rb
34
+ - lib/nearline/block.rb
35
+ - lib/nearline/file_content.rb
36
+ - lib/nearline/log.rb
37
+ - lib/nearline/manifest.rb
38
+ - lib/nearline/module_methods.rb
39
+ - lib/nearline/schema.rb
40
+ - lib/nearline.rb
41
+ - tasks/clean.rake
42
+ - tasks/gemspec.rake
43
+ - tasks/rcov.rake
44
+ - tasks/test.rake
45
+ test_files:
46
+ - test/nearline_test.rb
47
+ rdoc_options: []
48
+
49
+ extra_rdoc_files: []
50
+
51
+ executables: []
52
+
53
+ extensions: []
54
+
55
+ requirements: []
56
+
57
+ dependencies:
58
+ - !ruby/object:Gem::Dependency
59
+ name: activerecord
60
+ version_requirement:
61
+ version_requirements: !ruby/object:Gem::Version::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: 2.0.2
66
+ version: