nearline 0.0.1
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.
- data/lib/nearline/archived_file.rb +224 -0
- data/lib/nearline/block.rb +56 -0
- data/lib/nearline/file_content.rb +85 -0
- data/lib/nearline/log.rb +11 -0
- data/lib/nearline/manifest.rb +106 -0
- data/lib/nearline/module_methods.rb +98 -0
- data/lib/nearline/schema.rb +90 -0
- data/lib/nearline.rb +16 -0
- data/tasks/clean.rake +3 -0
- data/tasks/gemspec.rake +25 -0
- data/tasks/rcov.rake +13 -0
- data/tasks/test.rake +4 -0
- data/test/nearline_test.rb +17 -0
- metadata +66 -0
@@ -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
|
data/lib/nearline/log.rb
ADDED
@@ -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
data/tasks/gemspec.rake
ADDED
@@ -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,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:
|