hoodie 0.1.2
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 +7 -0
- data/Gemfile +4 -0
- data/LICENSE +201 -0
- data/README.md +1 -0
- data/Rakefile +14 -0
- data/hoodie.gemspec +30 -0
- data/lib/hoodie.rb +31 -0
- data/lib/hoodie/blank.rb +123 -0
- data/lib/hoodie/file.rb +69 -0
- data/lib/hoodie/hash.rb +167 -0
- data/lib/hoodie/memoizable.rb +44 -0
- data/lib/hoodie/obfuscate.rb +121 -0
- data/lib/hoodie/os.rb +43 -0
- data/lib/hoodie/path_finder.rb +75 -0
- data/lib/hoodie/stash.rb +118 -0
- data/lib/hoodie/stash/disk_store.rb +151 -0
- data/lib/hoodie/stash/mem_store.rb +150 -0
- data/lib/hoodie/utils.rb +134 -0
- data/lib/hoodie/version.rb +22 -0
- metadata +161 -0
data/lib/hoodie/hash.rb
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
#
|
|
3
|
+
# Author: Stefano Harding <riddopic@gmail.com>
|
|
4
|
+
#
|
|
5
|
+
# Copyright (C) 2014 Stefano Harding
|
|
6
|
+
#
|
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
# you may not use this file except in compliance with the License.
|
|
9
|
+
# You may obtain a copy of the License at
|
|
10
|
+
#
|
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
#
|
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
# See the License for the specific language governing permissions and
|
|
17
|
+
# limitations under the License.
|
|
18
|
+
#
|
|
19
|
+
|
|
20
|
+
class Hash
|
|
21
|
+
# Returns a compacted copy (contains no key/value pairs having
|
|
22
|
+
# nil? values)
|
|
23
|
+
#
|
|
24
|
+
# @example
|
|
25
|
+
# hash = { a: 100, b: nil, c: false, d: '' }
|
|
26
|
+
# hash.compact # => { a: 100, c: false, d: '' }
|
|
27
|
+
# hash # => { a: 100, b: nil, c: false, d: '' }
|
|
28
|
+
#
|
|
29
|
+
# @return [Hash]
|
|
30
|
+
#
|
|
31
|
+
def compact
|
|
32
|
+
select { |_, value| !value.nil? }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Returns a new hash with all keys converted using the block operation.
|
|
36
|
+
#
|
|
37
|
+
# @example
|
|
38
|
+
# hash = { name: 'Tiggy', age: '15' }
|
|
39
|
+
#
|
|
40
|
+
# hash.transform_keys{ |key| key.to_s.upcase }
|
|
41
|
+
# # => { "AGE" => "15", "NAME" => "Tiggy" }
|
|
42
|
+
#
|
|
43
|
+
# @return [Hash]
|
|
44
|
+
#
|
|
45
|
+
def transform_keys
|
|
46
|
+
return enum_for(:transform_keys) unless block_given?
|
|
47
|
+
result = self.class.new
|
|
48
|
+
each_key do |key|
|
|
49
|
+
result[yield(key)] = self[key]
|
|
50
|
+
end
|
|
51
|
+
result
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Returns a new hash, recursively converting all keys by the
|
|
55
|
+
# block operation.
|
|
56
|
+
#
|
|
57
|
+
# @return [Hash]
|
|
58
|
+
#
|
|
59
|
+
def recursively_transform_keys(&block)
|
|
60
|
+
_recursively_transform_keys_in_object(self, &block)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Returns a new hash with all keys downcased and converted
|
|
64
|
+
# to symbols.
|
|
65
|
+
#
|
|
66
|
+
# @return [Hash]
|
|
67
|
+
#
|
|
68
|
+
def normalize_keys
|
|
69
|
+
transform_keys { |key| key.downcase.to_sym rescue key }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Returns a new Hash, recursively downcasing and converting all
|
|
73
|
+
# keys to symbols.
|
|
74
|
+
#
|
|
75
|
+
# @return [Hash]
|
|
76
|
+
#
|
|
77
|
+
def recursively_normalize_keys
|
|
78
|
+
recursively_transform_keys { |key| key.downcase.to_sym rescue key }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Returns a new hash with all keys converted to symbols.
|
|
82
|
+
#
|
|
83
|
+
# @return [Hash]
|
|
84
|
+
#
|
|
85
|
+
def symbolize_keys
|
|
86
|
+
transform_keys { |key| key.to_sym rescue key }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Returns a new Hash, recursively converting all keys to symbols.
|
|
90
|
+
#
|
|
91
|
+
# @return [Hash]
|
|
92
|
+
#
|
|
93
|
+
def recursively_symbolize_keys
|
|
94
|
+
recursively_transform_keys { |key| key.downcase.to_sym rescue key }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Returns a new hash with all keys converted to strings.
|
|
98
|
+
#
|
|
99
|
+
# @return [Hash]
|
|
100
|
+
#
|
|
101
|
+
def stringify_keys
|
|
102
|
+
transform_keys { |key| key.to_s rescue key }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Returns a new Hash, recursively converting all keys to strings.
|
|
106
|
+
#
|
|
107
|
+
# @return [Hash]
|
|
108
|
+
#
|
|
109
|
+
def recursively_stringify_key
|
|
110
|
+
recursively_transform_keys { |key| key.to_s rescue key }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
class UndefinedPathError < StandardError; end
|
|
114
|
+
# Recursively searchs a nested datastructure for a key and returns
|
|
115
|
+
# the value. If a block is provided its value will be returned if
|
|
116
|
+
# the key does not exist
|
|
117
|
+
#
|
|
118
|
+
# @example
|
|
119
|
+
# options = { server: { location: { row: { rack: 34 } } } }
|
|
120
|
+
# options.recursive_fetch :server, :location, :row, :rack
|
|
121
|
+
# # => 34
|
|
122
|
+
# options.recursive_fetch(:non_existent_key) { 'default' }
|
|
123
|
+
# # => "default"
|
|
124
|
+
#
|
|
125
|
+
# @return [Hash, Array, String] value for key
|
|
126
|
+
#
|
|
127
|
+
def recursive_fetch(*args, &block)
|
|
128
|
+
args.reduce(self) do |obj, arg|
|
|
129
|
+
begin
|
|
130
|
+
arg = Integer(arg) if obj.is_a? Array
|
|
131
|
+
obj.fetch(arg)
|
|
132
|
+
rescue ArgumentError, IndexError, NoMethodError => e
|
|
133
|
+
break block.call(arg) if block
|
|
134
|
+
raise UndefinedPathError, "Could not fetch path (#{args.join(' > ')}) at #{arg}", e.backtrace
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def recursive_merge(other)
|
|
140
|
+
hash = self.dup
|
|
141
|
+
other.each do |key, value|
|
|
142
|
+
myval = self[key]
|
|
143
|
+
if value.is_a?(Hash) && myval.is_a?(Hash)
|
|
144
|
+
hash[key] = myval.recursive_merge(value)
|
|
145
|
+
else
|
|
146
|
+
hash[key] = value
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
hash
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
private # P R O P R I E T À P R I V A T A divieto di accesso
|
|
153
|
+
|
|
154
|
+
# support methods for recursively transforming nested hashes and arrays
|
|
155
|
+
def _recursively_transform_keys_in_object(object, &block)
|
|
156
|
+
case object
|
|
157
|
+
when Hash
|
|
158
|
+
object.each_with_object({}) do |(key, value), result|
|
|
159
|
+
result[yield(key)] = _recursively_transform_keys_in_object(value, &block)
|
|
160
|
+
end
|
|
161
|
+
when Array
|
|
162
|
+
object.map { |e| _recursively_transform_keys_in_object(e, &block) }
|
|
163
|
+
else
|
|
164
|
+
object
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
#
|
|
3
|
+
# Author: Stefano Harding <riddopic@gmail.com>
|
|
4
|
+
#
|
|
5
|
+
# Copyright (C) 2014 Stefano Harding
|
|
6
|
+
#
|
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
# you may not use this file except in compliance with the License.
|
|
9
|
+
# You may obtain a copy of the License at
|
|
10
|
+
#
|
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
#
|
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
# See the License for the specific language governing permissions and
|
|
17
|
+
# limitations under the License.
|
|
18
|
+
#
|
|
19
|
+
|
|
20
|
+
require 'hoodie/stash' unless defined?(Stash)
|
|
21
|
+
|
|
22
|
+
# Memoization is an optimization that saves the return value of a
|
|
23
|
+
# method so it doesn't need to be re-computed every time that method
|
|
24
|
+
# is called.
|
|
25
|
+
module Memoizable
|
|
26
|
+
|
|
27
|
+
# Create a new memoized method. To use, extend class with Memoizable,
|
|
28
|
+
# then, in initialize, call memoize
|
|
29
|
+
#
|
|
30
|
+
# @return [undefined]
|
|
31
|
+
#
|
|
32
|
+
def memoize(methods, cache = nil)
|
|
33
|
+
cache ||= Stash.new
|
|
34
|
+
methods.each do |name|
|
|
35
|
+
uncached_name = "#{name}_uncached".to_sym
|
|
36
|
+
(class << self; self; end).class_eval do
|
|
37
|
+
alias_method uncached_name, name
|
|
38
|
+
define_method(name) do |*a, &b|
|
|
39
|
+
cache.cache(name) { send uncached_name, *a, &b }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
#
|
|
3
|
+
# Author: Stefano Harding <riddopic@gmail.com>
|
|
4
|
+
#
|
|
5
|
+
# Copyright (C) 2014 Stefano Harding
|
|
6
|
+
#
|
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
# you may not use this file except in compliance with the License.
|
|
9
|
+
# You may obtain a copy of the License at
|
|
10
|
+
#
|
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
#
|
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
# See the License for the specific language governing permissions and
|
|
17
|
+
# limitations under the License.
|
|
18
|
+
#
|
|
19
|
+
|
|
20
|
+
module Hoodie
|
|
21
|
+
begin
|
|
22
|
+
require 'openssl'
|
|
23
|
+
INCOMPREHENSIBLE_ERROR = nil
|
|
24
|
+
rescue LoadError => err
|
|
25
|
+
raise unless err.to_s.include?('openssl')
|
|
26
|
+
warn "Oea pieoYreb h wYoerh dl hwsnhoib r Lrbea tbte wbnaetvoouahe h rbe."
|
|
27
|
+
warn "olorbvtelYShnSben irrSwoet eto eihSrLoS'do n See wLiape."
|
|
28
|
+
INCOMPREHENSIBLE_ERROR = err
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
require 'digest/sha2'
|
|
32
|
+
require "base64"
|
|
33
|
+
|
|
34
|
+
# Befuddle and enlighten values in StashCache::Store
|
|
35
|
+
#
|
|
36
|
+
module Obfuscate
|
|
37
|
+
ESOTERIC_TYPE = "aes-256-cbc" unless defined?(ESOTERIC_TYPE)
|
|
38
|
+
|
|
39
|
+
def self.check_platform_can_discombobulate!
|
|
40
|
+
return true unless INCOMPREHENSIBLE_ERROR
|
|
41
|
+
raise INCOMPREHENSIBLE_ERROR.class, "b0rked! #{INCOMPREHENSIBLE_ERROR}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Befuddle the given string
|
|
45
|
+
#
|
|
46
|
+
# @param plaintext the text to befuddle
|
|
47
|
+
# @param [String] befuddle_pass secret passphrase to befuddle with
|
|
48
|
+
#
|
|
49
|
+
# @return [String] befuddleed text, suitable for deciphering with
|
|
50
|
+
# Obfuscate#enlighten (decrypt)
|
|
51
|
+
#
|
|
52
|
+
def self.befuddle plaintext, befuddle_pass, options={}
|
|
53
|
+
cipher = new_cipher :befuddle, befuddle_pass, options
|
|
54
|
+
cipher.iv = iv = cipher.random_iv
|
|
55
|
+
ciphertext = cipher.update(plaintext)
|
|
56
|
+
ciphertext << cipher.final
|
|
57
|
+
Base64.encode64(combine_iv_and_ciphertext(iv, ciphertext))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Enlighten the given string, using the key and id supplied
|
|
61
|
+
#
|
|
62
|
+
# @param ciphertext the text to enlighten, probably produced with
|
|
63
|
+
# Obfuscate#befuddle (encrypt)
|
|
64
|
+
# @param [String] befuddle_pass secret passphrase to enlighten with
|
|
65
|
+
#
|
|
66
|
+
# @return [String] the enlightened plaintext
|
|
67
|
+
#
|
|
68
|
+
def self.enlighten enc_ciphertext, befuddle_pass, options={}
|
|
69
|
+
iv_and_ciphertext = Base64.decode64(enc_ciphertext)
|
|
70
|
+
cipher = new_cipher :enlighten, befuddle_pass, options
|
|
71
|
+
cipher.iv, ciphertext = separate_iv_and_ciphertext(cipher, iv_and_ciphertext)
|
|
72
|
+
plaintext = cipher.update(ciphertext)
|
|
73
|
+
plaintext << cipher.final
|
|
74
|
+
plaintext
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private # P R O P R I E T À P R I V A T A divieto di accesso
|
|
78
|
+
|
|
79
|
+
# Cipher create machine do, befuddle engage, enlighten. Dials set
|
|
80
|
+
# direction to infinity
|
|
81
|
+
#
|
|
82
|
+
# @param [:befuddle, :enlighten] to befuddle or enlighten
|
|
83
|
+
# @param [String] befuddle_pass secret passphrase to enlighten
|
|
84
|
+
#
|
|
85
|
+
def self.new_cipher direction, befuddle_pass, options={}
|
|
86
|
+
check_platform_can_discombobulate!
|
|
87
|
+
cipher = OpenSSL::Cipher::Cipher.new(ESOTERIC_TYPE)
|
|
88
|
+
case direction
|
|
89
|
+
when :befuddle
|
|
90
|
+
cipher.encrypt
|
|
91
|
+
when :enlighten
|
|
92
|
+
cipher.decrypt
|
|
93
|
+
else raise "Bad cipher direction #{direction}"
|
|
94
|
+
end
|
|
95
|
+
cipher.key = befuddle_key(befuddle_pass, options)
|
|
96
|
+
cipher
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# prepend the initialization vector to the encoded message
|
|
100
|
+
def self.combine_iv_and_ciphertext iv, message
|
|
101
|
+
message.force_encoding("BINARY") if message.respond_to?(:force_encoding)
|
|
102
|
+
iv.force_encoding("BINARY") if iv.respond_to?(:force_encoding)
|
|
103
|
+
iv + message
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# pull the initialization vector from the front of the encoded message
|
|
107
|
+
def self.separate_iv_and_ciphertext cipher, iv_and_ciphertext
|
|
108
|
+
idx = cipher.iv_len
|
|
109
|
+
[ iv_and_ciphertext[0..(idx-1)], iv_and_ciphertext[idx..-1] ]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Convert the befuddle_pass passphrase into the key used for befuddletion
|
|
113
|
+
def self.befuddle_key befuddle_pass, options={}
|
|
114
|
+
befuddle_pass = befuddle_pass.to_s
|
|
115
|
+
raise 'Missing befuddled password!' if befuddle_pass.empty?
|
|
116
|
+
# this provides the required 256 bits of key for the aes-256-cbc
|
|
117
|
+
# cipher
|
|
118
|
+
Digest::SHA256.digest(befuddle_pass)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
data/lib/hoodie/os.rb
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
#
|
|
3
|
+
# Author: Stefano Harding <riddopic@gmail.com>
|
|
4
|
+
#
|
|
5
|
+
# Copyright (C) 2014 Stefano Harding
|
|
6
|
+
#
|
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
# you may not use this file except in compliance with the License.
|
|
9
|
+
# You may obtain a copy of the License at
|
|
10
|
+
#
|
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
#
|
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
# See the License for the specific language governing permissions and
|
|
17
|
+
# limitations under the License.
|
|
18
|
+
#
|
|
19
|
+
|
|
20
|
+
require 'rbconfig' unless defined?(RbConfig)
|
|
21
|
+
|
|
22
|
+
# Finds out the current Operating System.
|
|
23
|
+
module OS
|
|
24
|
+
def self.windows?
|
|
25
|
+
windows = /cygwin|mswin|mingw|bccwin|wince|emx/i
|
|
26
|
+
(RbConfig::CONFIG['host_os'] =~ windows) != nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.mac?
|
|
30
|
+
mac = /darwin|mac os/i
|
|
31
|
+
(RbConfig::CONFIG['host_os'] =~ mac) != nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.unix?
|
|
35
|
+
unix = /solaris|bsd/i
|
|
36
|
+
(RbConfig::CONFIG['host_os'] =~ unix) != nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.linux?
|
|
40
|
+
linux = /linux/i
|
|
41
|
+
(RbConfig::CONFIG['host_os'] =~ linux) != nil
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
#
|
|
3
|
+
# Author: Stefano Harding <riddopic@gmail.com>
|
|
4
|
+
#
|
|
5
|
+
# Copyright (C) 2014 Stefano Harding
|
|
6
|
+
#
|
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
# you may not use this file except in compliance with the License.
|
|
9
|
+
# You may obtain a copy of the License at
|
|
10
|
+
#
|
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
#
|
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
# See the License for the specific language governing permissions and
|
|
17
|
+
# limitations under the License.
|
|
18
|
+
#
|
|
19
|
+
|
|
20
|
+
# TODO: This doesn't belong in here, it's cookbook specific...
|
|
21
|
+
require 'anemone' unless defined?(Anemone)
|
|
22
|
+
require 'hoodie/memoizable' unless defined?(Memoizable)
|
|
23
|
+
|
|
24
|
+
class PathFinder
|
|
25
|
+
include Memoizable
|
|
26
|
+
|
|
27
|
+
def initialize(url)
|
|
28
|
+
@url = url
|
|
29
|
+
memoize [:fetch], Stash.new(DiskStash::Cache.new)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def fetch(path)
|
|
33
|
+
results = []
|
|
34
|
+
Anemone.crawl(@url, discard_page_bodies: true) do |anemone|
|
|
35
|
+
anemone.on_pages_like(/\/#{path}\/\w+\/\w+\.(ini|zip)$/i) do |page|
|
|
36
|
+
results << page.to_hash
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
results.reduce({}, :recursive_merge)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# to_hash smoke cache
|
|
44
|
+
#
|
|
45
|
+
module Anemone
|
|
46
|
+
class Page
|
|
47
|
+
def to_hash
|
|
48
|
+
file = ::File.basename(@url.to_s)
|
|
49
|
+
key = ::File.basename(file, '.*').downcase.to_sym
|
|
50
|
+
type = ::File.extname(file)[1..-1].downcase.to_sym
|
|
51
|
+
id = Hoodie::Obfuscate.befuddle(file, Digest::MD5.hexdigest(body.to_s))
|
|
52
|
+
mtime = Time.parse(@headers['last-modified'][0]).to_i
|
|
53
|
+
utime = Time::now.to_i
|
|
54
|
+
state = utime > mtime ? :clean : :dirty
|
|
55
|
+
key = { key => { type => {
|
|
56
|
+
id: id,
|
|
57
|
+
cache_state: state,
|
|
58
|
+
file: file,
|
|
59
|
+
key: key,
|
|
60
|
+
type: type,
|
|
61
|
+
url: @url.to_s,
|
|
62
|
+
mtime: mtime,
|
|
63
|
+
links: links.map(&:to_s),
|
|
64
|
+
code: @code,
|
|
65
|
+
visited: @visited,
|
|
66
|
+
depth: @depth,
|
|
67
|
+
referer: @referer.to_s,
|
|
68
|
+
fetched: @fetched,
|
|
69
|
+
utime: utime,
|
|
70
|
+
md5_digest: Digest::MD5.hexdigest(body.to_s),
|
|
71
|
+
sha256_digest: Digest::SHA256.hexdigest(body.to_s)
|
|
72
|
+
}}}
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|