metahash 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/.gitignore +6 -0
- data/Gemfile +4 -0
- data/Rakefile +1 -0
- data/lib/metahash/version.rb +3 -0
- data/lib/metahash.rb +91 -0
- data/metahash.gemspec +28 -0
- data/spec/metahash_spec.rb +132 -0
- data/spec/test.txt +0 -0
- metadata +118 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/lib/metahash.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'metahash/version'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'bson'
|
4
|
+
require 'json'
|
5
|
+
module Metahash
|
6
|
+
class Metahash
|
7
|
+
BOM = (JSON '["\uFEFF"]')[0]
|
8
|
+
SEARCH_BYTE_OFFSET = 4
|
9
|
+
SEARCH_BYTE_SEQUENCE = "\003bson-\357\273\277"
|
10
|
+
def initialize(path,options={})
|
11
|
+
raise ArgumentError unless File.exists? path
|
12
|
+
@path = path
|
13
|
+
@options = options
|
14
|
+
bson = read_for_search File.open(@path,"rb")
|
15
|
+
if bson
|
16
|
+
@bson_range = bson[:range]
|
17
|
+
@bson_size = bson[:size]
|
18
|
+
@bytes = bson[:bytes]
|
19
|
+
end
|
20
|
+
self
|
21
|
+
end
|
22
|
+
# returns current state of metahash, false if no tag attahed, true if tag attached
|
23
|
+
def tagged?
|
24
|
+
!@bson_size.nil?
|
25
|
+
end
|
26
|
+
|
27
|
+
# returns a hash
|
28
|
+
def to_h
|
29
|
+
return nil if @bytes.nil?
|
30
|
+
packet = BSON.deserialize(BSON::ByteBuffer.new(@bytes[@bson_range]))
|
31
|
+
packet["bson-#{BOM}"]["object"]
|
32
|
+
end
|
33
|
+
|
34
|
+
# allow transparent usage like hash
|
35
|
+
# TODO: This is ridiculous, truly a monkey patch to provide cool functionality, please implement hash correctly
|
36
|
+
def method_missing(method,*args)
|
37
|
+
if {}.respond_to?(method)
|
38
|
+
hash = to_h||{}
|
39
|
+
result = hash.send(method,*args)
|
40
|
+
write hash
|
41
|
+
result
|
42
|
+
else
|
43
|
+
super
|
44
|
+
end
|
45
|
+
end
|
46
|
+
def write(obj)
|
47
|
+
byte_string = BSON.serialize(wrap_obj(obj)).to_s
|
48
|
+
if @bytes
|
49
|
+
#take 0..bson_start
|
50
|
+
#serialize object
|
51
|
+
#with bson_end..-1
|
52
|
+
@bytes=@bytes[0..@bson_range.first]+byte_string+@bytes[@bson_range.last..-1]
|
53
|
+
#write and reinitialize
|
54
|
+
File.truncate(@path,0)
|
55
|
+
File.open(@path,"wb+") do |file|
|
56
|
+
file.write(@bytes)
|
57
|
+
end
|
58
|
+
else
|
59
|
+
File.open(@path,"ab+") do |file|
|
60
|
+
file.write(byte_string)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
initialize(@path,@options)
|
64
|
+
end
|
65
|
+
private
|
66
|
+
|
67
|
+
# wraps obj in Meta Hash header
|
68
|
+
def wrap_obj(obj)
|
69
|
+
meta_bson = {
|
70
|
+
"bson-#{BOM}"=>{
|
71
|
+
"version"=>VERSION,
|
72
|
+
"ns"=>["http://json-schema.org/card.properties","http://groups.google.com/group/json-schema/browse_thread/thread/dd1a8c9e55035c67?pli=1"],
|
73
|
+
"object"=> obj
|
74
|
+
}
|
75
|
+
}
|
76
|
+
end
|
77
|
+
def read_for_search(io)
|
78
|
+
bytes = io.read
|
79
|
+
search_position = bytes.index(SEARCH_BYTE_SEQUENCE)
|
80
|
+
return nil unless search_position
|
81
|
+
bson_start = search_position - SEARCH_BYTE_OFFSET
|
82
|
+
bson_size = bytes[bson_start..(search_position-1)].unpack("S")[0]
|
83
|
+
bson_end = bson_start + bson_size
|
84
|
+
return {
|
85
|
+
:range => bson_start..bson_end,
|
86
|
+
:size => bson_size,
|
87
|
+
:bytes => bytes
|
88
|
+
}
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
data/metahash.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "metahash/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "metahash"
|
7
|
+
s.version = Metahash::VERSION
|
8
|
+
s.authors = ["Thomas Devol"]
|
9
|
+
s.email = ["thomas.devol@gmail.com"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{Provides metadata management for many file types}
|
12
|
+
s.description = %q{Successor to exif/id3/xmp for metadata management}
|
13
|
+
|
14
|
+
s.rubyforge_project = "metahash"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_development_dependency "rspec"
|
22
|
+
s.add_development_dependency "ruby-debug"
|
23
|
+
s.add_runtime_dependency "bson"
|
24
|
+
s.add_runtime_dependency "json"
|
25
|
+
# specify any dependencies here; for example:
|
26
|
+
# s.add_development_dependency "rspec"
|
27
|
+
# s.add_runtime_dependency "rest-client"
|
28
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
require 'metahash'
|
2
|
+
require 'ruby-debug'
|
3
|
+
module Metahash
|
4
|
+
describe Metahash do
|
5
|
+
before {
|
6
|
+
@test_path = File.dirname(__FILE__)+"/test.txt"
|
7
|
+
@test_data = {"hello"=>"world"}
|
8
|
+
}
|
9
|
+
|
10
|
+
context "generally" do #high level expectations
|
11
|
+
|
12
|
+
it "should be able to append to a new file" do
|
13
|
+
|
14
|
+
mh = Metahash.new @test_path
|
15
|
+
mh.write({"hello"=>"world"})
|
16
|
+
|
17
|
+
mh = Metahash.new @test_path
|
18
|
+
obj = mh.to_h
|
19
|
+
obj.should be_a(Hash)
|
20
|
+
obj["hello"].should == "world"
|
21
|
+
|
22
|
+
end
|
23
|
+
it "should be able to modify an already tagged file" do
|
24
|
+
mh = Metahash.new @test_path
|
25
|
+
mh.write({"hello"=>"world"})
|
26
|
+
|
27
|
+
mh = Metahash.new @test_path
|
28
|
+
mh.write({"hello"=>"holmes"})
|
29
|
+
|
30
|
+
mh = Metahash.new @test_path
|
31
|
+
obj = mh.to_h
|
32
|
+
obj.should be_a(Hash)
|
33
|
+
obj["hello"].should == "holmes"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
describe "specifically" do
|
37
|
+
let(:new_metahash_with_untagged_file) do
|
38
|
+
f = File.open @test_path,"w+"
|
39
|
+
f.write "Hello World"
|
40
|
+
f.close
|
41
|
+
Metahash.new @test_path
|
42
|
+
end
|
43
|
+
let(:new_metahash_with_tagged_file) do
|
44
|
+
mh = new_metahash_with_untagged_file
|
45
|
+
mh.write @test_data
|
46
|
+
mh
|
47
|
+
end
|
48
|
+
|
49
|
+
describe "#initialize" do
|
50
|
+
it "should throw an error on an invalid path" do
|
51
|
+
invalid_path = rand().to_s+@test_path
|
52
|
+
expect{
|
53
|
+
mh = Metahash.new invalid_path
|
54
|
+
}.to raise_error(ArgumentError)
|
55
|
+
end
|
56
|
+
it "should not throw an error on a valid path" do
|
57
|
+
lambda {
|
58
|
+
new_metahash_with_untagged_file
|
59
|
+
}.should_not raise_error(ArgumentError)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
describe "#tagged?" do
|
63
|
+
it "should return false for untagged files" do
|
64
|
+
new_metahash_with_untagged_file.should_not be_tagged
|
65
|
+
end
|
66
|
+
it "should return true for tagged files" do
|
67
|
+
new_metahash_with_tagged_file.should be_tagged
|
68
|
+
end
|
69
|
+
end
|
70
|
+
describe "#write" do
|
71
|
+
context "with an untagged file" do
|
72
|
+
subject &:new_metahash_with_untagged_file
|
73
|
+
it "should call wrap obj and serialize" do
|
74
|
+
subject.should_receive(:wrap_obj).with(@test_data)
|
75
|
+
BSON.should_receive :serialize
|
76
|
+
subject.write @test_data
|
77
|
+
end
|
78
|
+
it "should call initialize after write" do
|
79
|
+
subject.should_receive(:initialize).exactly(1).times
|
80
|
+
subject.write @test_data
|
81
|
+
end
|
82
|
+
end
|
83
|
+
context "with a tagged file" do
|
84
|
+
subject &:new_metahash_with_tagged_file
|
85
|
+
it "should truncate already tagged file" do
|
86
|
+
File.should_receive(:truncate).with(@test_path,0)
|
87
|
+
subject.write({"hello"=>"holmes"})
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
describe "#to_h" do
|
92
|
+
context "with an untagged file" do
|
93
|
+
subject &:new_metahash_with_untagged_file
|
94
|
+
it "should return nil" do
|
95
|
+
subject.to_h.should be_nil
|
96
|
+
end
|
97
|
+
end
|
98
|
+
context "with a tagged file" do
|
99
|
+
subject &:new_metahash_with_tagged_file
|
100
|
+
it "should return test data" do
|
101
|
+
subject.to_h.should == @test_data
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
describe "#method_missing" do
|
106
|
+
context "with a tagged file" do
|
107
|
+
subject &:new_metahash_with_tagged_file
|
108
|
+
it "should read just like a hash" do
|
109
|
+
subject["hello"].should == "world"
|
110
|
+
end
|
111
|
+
it "should write and persist" do
|
112
|
+
#debugger
|
113
|
+
subject["foo"] = "bar"
|
114
|
+
subject["foo"].should == "bar"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
context "with an untagged file" do
|
118
|
+
subject &:new_metahash_with_untagged_file
|
119
|
+
it "should just work" do
|
120
|
+
subject["artist"] = "dave matthews"
|
121
|
+
|
122
|
+
mh = Metahash.new @test_path
|
123
|
+
mh["artist"].should == "dave matthews"
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
end
|
data/spec/test.txt
ADDED
Binary file
|
metadata
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: metahash
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
version: 0.0.1
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Thomas Devol
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2011-09-30 00:00:00 -05:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: rspec
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 0
|
29
|
+
version: "0"
|
30
|
+
type: :development
|
31
|
+
version_requirements: *id001
|
32
|
+
- !ruby/object:Gem::Dependency
|
33
|
+
name: ruby-debug
|
34
|
+
prerelease: false
|
35
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
segments:
|
40
|
+
- 0
|
41
|
+
version: "0"
|
42
|
+
type: :development
|
43
|
+
version_requirements: *id002
|
44
|
+
- !ruby/object:Gem::Dependency
|
45
|
+
name: bson
|
46
|
+
prerelease: false
|
47
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
segments:
|
52
|
+
- 0
|
53
|
+
version: "0"
|
54
|
+
type: :runtime
|
55
|
+
version_requirements: *id003
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: json
|
58
|
+
prerelease: false
|
59
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
segments:
|
64
|
+
- 0
|
65
|
+
version: "0"
|
66
|
+
type: :runtime
|
67
|
+
version_requirements: *id004
|
68
|
+
description: Successor to exif/id3/xmp for metadata management
|
69
|
+
email:
|
70
|
+
- thomas.devol@gmail.com
|
71
|
+
executables: []
|
72
|
+
|
73
|
+
extensions: []
|
74
|
+
|
75
|
+
extra_rdoc_files: []
|
76
|
+
|
77
|
+
files:
|
78
|
+
- .gitignore
|
79
|
+
- Gemfile
|
80
|
+
- Rakefile
|
81
|
+
- lib/metahash.rb
|
82
|
+
- lib/metahash/version.rb
|
83
|
+
- metahash.gemspec
|
84
|
+
- spec/metahash_spec.rb
|
85
|
+
- spec/test.txt
|
86
|
+
has_rdoc: true
|
87
|
+
homepage: ""
|
88
|
+
licenses: []
|
89
|
+
|
90
|
+
post_install_message:
|
91
|
+
rdoc_options: []
|
92
|
+
|
93
|
+
require_paths:
|
94
|
+
- lib
|
95
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
96
|
+
requirements:
|
97
|
+
- - ">="
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
segments:
|
100
|
+
- 0
|
101
|
+
version: "0"
|
102
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
103
|
+
requirements:
|
104
|
+
- - ">="
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
segments:
|
107
|
+
- 0
|
108
|
+
version: "0"
|
109
|
+
requirements: []
|
110
|
+
|
111
|
+
rubyforge_project: metahash
|
112
|
+
rubygems_version: 1.3.6
|
113
|
+
signing_key:
|
114
|
+
specification_version: 3
|
115
|
+
summary: Provides metadata management for many file types
|
116
|
+
test_files:
|
117
|
+
- spec/metahash_spec.rb
|
118
|
+
- spec/test.txt
|