fastdfs-client 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 711de59fe7605cadbe4820f3f852e79860e5c1ab
4
+ data.tar.gz: cd68e14f5f1932d19045e5c3c31fa77e91d75472
5
+ SHA512:
6
+ metadata.gz: 673c80215e1b30fea0aae0f2b3b2471d6641027491f548365213915762cb092f8fec7d6643bbf3c7fc22501e534eb730f22e495e08253de45a4fda92cbe88374
7
+ data.tar.gz: 310349702c4c17c9316cae1a165c9f305a88f34445023a5f75139d26741a9642bdddcb5ca3176407211ec7e2f1c41c9cb7a83b384f838077dcd55b385be76a58
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source 'http://gems.ruby-china.org/'
2
+
3
+ gemspec
4
+
5
+ group :test, :development do
6
+ gem 'debugger'
7
+ gem 'rspec'
8
+ end
@@ -0,0 +1,38 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ fastdfs-client (0.0.2)
5
+
6
+ GEM
7
+ remote: http://gems.ruby-china.org/
8
+ specs:
9
+ columnize (0.9.0)
10
+ debugger (1.6.8)
11
+ columnize (>= 0.3.1)
12
+ debugger-linecache (~> 1.2.0)
13
+ debugger-ruby_core_source (~> 1.3.5)
14
+ debugger-linecache (1.2.0)
15
+ debugger-ruby_core_source (1.3.8)
16
+ diff-lcs (1.2.5)
17
+ rspec (3.4.0)
18
+ rspec-core (~> 3.4.0)
19
+ rspec-expectations (~> 3.4.0)
20
+ rspec-mocks (~> 3.4.0)
21
+ rspec-core (3.4.4)
22
+ rspec-support (~> 3.4.0)
23
+ rspec-expectations (3.4.0)
24
+ diff-lcs (>= 1.2.0, < 2.0)
25
+ rspec-support (~> 3.4.0)
26
+ rspec-mocks (3.4.1)
27
+ diff-lcs (>= 1.2.0, < 2.0)
28
+ rspec-support (~> 3.4.0)
29
+ rspec-support (3.4.1)
30
+
31
+ PLATFORMS
32
+ ruby
33
+
34
+ DEPENDENCIES
35
+ bundler (~> 1.3)
36
+ debugger
37
+ fastdfs-client!
38
+ rspec
@@ -0,0 +1,21 @@
1
+ # fastdfs-client-ruby
2
+
3
+ fastdfs client for ruby
4
+
5
+ ### Install
6
+
7
+ #Gemfile
8
+ gem 'fastdfs-client', git: "git@github.com:huxinghai1988/fastdfs-client-ruby.git"
9
+
10
+ ### Using
11
+
12
+ ```RUBY
13
+ tracker = new Fastdfs::Client::Tracker("192.168.1.1", "22122")
14
+
15
+ @storage = tracker.get_storage
16
+
17
+ @storage.upload(@file)
18
+
19
+ #group_name + path
20
+ @storage.delete(path)
21
+ ```
@@ -0,0 +1,23 @@
1
+ # coding: utf-8
2
+
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'fastdfs-client/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = "fastdfs-client"
9
+ spec.version = Fastdfs::Client::VERSION
10
+ spec.authors = ["Ka Ka"]
11
+ spec.email = ["huxinghai1988@gmail.com"]
12
+ spec.description = "fastdfs upload file client for ruby"
13
+ spec.summary = "fastdfs upload file client for ruby"
14
+ spec.homepage = "https://github.com/huxinghai1988/fastdfs-client-ruby.git"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files`.split($/)
18
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
19
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_development_dependency "bundler", "~> 1.3"
23
+ end
@@ -0,0 +1,7 @@
1
+ require "fastdfs-client/extend_core"
2
+ require 'fastdfs-client/socket'
3
+ require 'fastdfs-client/cmd'
4
+ require 'fastdfs-client/proto_common'
5
+ require 'fastdfs-client/utils'
6
+ require 'fastdfs-client/hook'
7
+ require "fastdfs-client/tracker"
@@ -0,0 +1,12 @@
1
+ module Fastdfs
2
+ module Client
3
+
4
+ module CMD
5
+ STORE_WITHOUT_GROUP_ONE = 101
6
+ UPLOAD_FILE = 11
7
+ RESP_CODE = 100
8
+ DELETE_FILE = 12
9
+ end
10
+
11
+ end
12
+ end
@@ -0,0 +1,14 @@
1
+
2
+ class Array
3
+ def to_pack_long
4
+ self.each_with_index.inject(0){|s, item| s = s | (item[0] << (56 - (item[1] * 8))); s }
5
+ end
6
+
7
+ end
8
+
9
+ class Object
10
+
11
+ def blank?
12
+ self.empty? || self.nil?
13
+ end
14
+ end
@@ -0,0 +1,41 @@
1
+ module Hook
2
+ def before(*meth_names, &callback)
3
+ meth_names.each{|meth_name| add_hook :before, meth_name, &callback }
4
+ end
5
+
6
+ def after(*meth_names, &callback)
7
+ meth_names.each{|meth_name| add_hook :after, meth_name, &callback }
8
+ end
9
+
10
+ def hooks
11
+ @hooks ||= Hash.new do |hash, method_name|
12
+ hash[method_name] = { before: [], after: [], hijacked: false }
13
+ end
14
+ end
15
+
16
+ def add_hook(where, meth_name, &callback)
17
+ hooks[meth_name][where] << callback
18
+ ensure_hijacked meth_name
19
+ end
20
+
21
+ def method_added(meth_name)
22
+ ensure_hijacked meth_name if hooks.has_key? meth_name
23
+ end
24
+
25
+ def ensure_hijacked(meth_name)
26
+ return if hooks[meth_name][:hijacked] || !instance_methods.include?(meth_name)
27
+ meth = instance_method meth_name
28
+ _hooks = hooks
29
+ _hooks[meth_name][:hijacked] = true
30
+ define_method meth_name do |*args, &block|
31
+ _hooks[meth_name][:before].each do |callback|
32
+ self.instance_exec(&callback)
33
+ end
34
+ return_value = meth.bind(self).call *args, &block
35
+ _hooks[meth_name][:after].each do |callback|
36
+ self.instance_exec(&callback)
37
+ end
38
+ return_value
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,24 @@
1
+ module Fastdfs
2
+ module Client
3
+
4
+ module ProtoCommon
5
+ TRACKER_BODY_LEN = 40
6
+
7
+ IPADDR = 16...31
8
+ PORT = 31...-1
9
+ SIZE_LEN = 9
10
+ HEAD_LEN = 10
11
+ EXTNAME_LEN = 6
12
+ GROUP_NAME_MAX_LEN = 16
13
+
14
+ def self.header_bytes(cmd, hex_long, erron=0)
15
+ hex_bytes = Utils.number_to_Buffer(hex_long)
16
+ header = hex_bytes.fill(0, hex_bytes.length...HEAD_LEN)
17
+ header[8] = cmd
18
+ header[9] = erron
19
+ header
20
+ end
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,69 @@
1
+ require 'socket'
2
+ require 'timeout'
3
+ require 'fastdfs-client/cmd'
4
+
5
+
6
+ module Fastdfs
7
+ module Client
8
+
9
+ class Socket
10
+ attr_accessor :header, :content, :header_len, :cmd, :socket, :host, :port
11
+
12
+ def initialize(host, port, options = {})
13
+ @host = host
14
+ @port = port
15
+ connection
16
+ @header_len = ProtoCommon::HEAD_LEN
17
+ @options = options || {}
18
+ @connection_timeout = @options[:connection_timeout] || 3
19
+ @recv_timeout = @options[:recv_timeout] || 3
20
+ end
21
+
22
+ def write(*args)
23
+ @cmd = args.shift
24
+ pkg = args.shift
25
+ pkg = pkg.pack("C*") if pkg.is_a?(Array)
26
+ @socket.write pkg
27
+ end
28
+
29
+ def close
30
+ @socket.close if connected
31
+ end
32
+
33
+ def connection
34
+ if @socket.nil? || !connected
35
+ Timeout.timeout(@connection_timeout) do
36
+ @socket = TCPSocket.new(@host, @port)
37
+ end
38
+ end
39
+ end
40
+
41
+ def connected
42
+ !@socket.closed?
43
+ end
44
+
45
+ def receive
46
+ @content = nil
47
+ Timeout.timeout(@recv_timeout) do
48
+ @header = @socket.recv(@header_len).unpack("C*")
49
+ end
50
+ res_header = parseHeader
51
+ if res_header[:body_length] > 0
52
+ Timeout.timeout(@recv_timeout) do
53
+ @content = @socket.recv(@header.to_pack_long)
54
+ end
55
+ end
56
+ yield @content if block_given?
57
+ end
58
+
59
+ private
60
+ def parseHeader
61
+ raise "recv package size #{@header} != #{@header_len}, cmd: #{@cmd}" unless @header.length == @header_len
62
+ raise "recv cmd: #{@header[8]} is not correct, expect cmd: #{CMD::RESP_CODE}, cmd: #{@cmd}" unless @header[8] == CMD::RESP_CODE
63
+ raise "recv erron #{@header[9]} 0 is correct, cmd: #{@cmd}" unless @header[9] == 0
64
+ {status: true, body_length: @header[0...8].to_pack_long}
65
+ end
66
+
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,71 @@
1
+ require 'tempfile'
2
+
3
+ module Fastdfs
4
+ module Client
5
+
6
+ class Storage
7
+ extend Hook
8
+
9
+ before(:upload, :delete){ @socket.connection }
10
+ after(:upload, :delete){ @socket.close }
11
+
12
+ attr_accessor :host, :port, :group_name, :store_path, :socket, :options
13
+
14
+ def initialize(host, port, store_path = nil, options = {})
15
+ @host = host
16
+ @port = port
17
+ @options = options || {}
18
+ @options = store_path if store_path.is_a?(Hash)
19
+ @socket = Socket.new(host, port, @options[:socket])
20
+ @extname_len = ProtoCommon::EXTNAME_LEN
21
+ @size_len = ProtoCommon::SIZE_LEN
22
+ @store_path = store_path || 0
23
+ end
24
+
25
+ def upload(file)
26
+ _upload(file)
27
+ end
28
+
29
+ def delete(path, group_name = nil)
30
+ cmd = CMD::DELETE_FILE
31
+ raise "path arguments is empty!" if path.blank?
32
+ if group_name.blank?
33
+ group_name = /^\/?(\w+)/.match(path)[1]
34
+ path = path.gsub("/#{group_name}")
35
+ end
36
+ raise "group_name arguments is empty!" if group_name.blank?
37
+ group_bytes = group_name.bytes.fill(0, group_name.length...ProtoCommon::GROUP_NAME_MAX_LEN)
38
+ path_length = (group_bytes.length + path.bytes.length)
39
+
40
+ @socket.write(cmd, (ProtoCommon.header_bytes(cmd, path_length) + group_bytes + path.bytes))
41
+ @socket.receive
42
+ end
43
+
44
+ private
45
+ def _upload(file)
46
+ cmd = CMD::UPLOAD_FILE
47
+
48
+ extname = File.extname(file)[1..-1]
49
+ ext_name_bs = extname.bytes.fill(0, extname.length...@extname_len)
50
+ hex_len_bytes = Utils.number_to_Buffer(file.size)
51
+ size_byte = [@store_path].concat(hex_len_bytes).fill(0, (hex_len_bytes.length+1)...@size_len)
52
+
53
+ header = ProtoCommon.header_bytes(cmd, (size_byte.length + @extname_len + file.size))
54
+ pkg = header + size_byte + ext_name_bs
55
+
56
+ @socket.write(cmd, pkg)
57
+ @socket.write(cmd, IO.read(file))
58
+ @socket.receive do |body|
59
+ group_name_max_len = ProtoCommon::GROUP_NAME_MAX_LEN
60
+
61
+ {
62
+ group_name: body[0...group_name_max_len].strip,
63
+ path: body[group_name_max_len..-1]
64
+ }
65
+ end
66
+ end
67
+
68
+ end
69
+
70
+ end
71
+ end
@@ -0,0 +1,35 @@
1
+ require 'fastdfs-client/storage'
2
+
3
+ module Fastdfs
4
+ module Client
5
+
6
+ class Tracker
7
+ extend Hook
8
+
9
+ before(:get_storage){ @socket.connection }
10
+ after(:get_storage){ @socket.close }
11
+
12
+ attr_accessor :socket, :cmd, :options
13
+
14
+ def initialize(host, port, options = {})
15
+ @socket = Socket.new(host, port, options[:socket])
16
+ @cmd = CMD::STORE_WITHOUT_GROUP_ONE
17
+ end
18
+
19
+ def get_storage
20
+ header = ProtoCommon.header_bytes(@cmd, 0)
21
+ @socket.write(@cmd, header)
22
+ @socket.receive do |body|
23
+ storage_ip = body[ProtoCommon::IPADDR].strip
24
+ storage_port = body[ProtoCommon::PORT].unpack("C*").to_pack_long
25
+ store_path = body[ProtoCommon::TRACKER_BODY_LEN-1].unpack("C*")[0]
26
+
27
+ Storage.new(storage_ip, storage_port, store_path, options)
28
+ end
29
+
30
+
31
+ end
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,13 @@
1
+ module Utils
2
+
3
+ def self.number_to_Buffer(num)
4
+ 8.times.map{|i| (num >> (56 - 8 * i)) & 255}
5
+ end
6
+
7
+ def self.array_merge(arr1, arr2)
8
+ raise "argument must be array" unless arr1.is_a?(Array) || arr2.is_a?(Array)
9
+ arr2.each_with_index.map{|v, i| arr1[i] = v }
10
+ arr1
11
+ end
12
+
13
+ end
@@ -0,0 +1,5 @@
1
+ module Fastdfs
2
+ module Client
3
+ VERSION = '0.0.2'
4
+ end
5
+ end
@@ -0,0 +1,74 @@
1
+
2
+ class TCPSocket
3
+ include Fastdfs::Client
4
+
5
+ attr_accessor :host, :port, :cmd, :recv_offset, :connect_state
6
+
7
+ def initialize(host, port)
8
+ @host = host
9
+ @port = port
10
+ @recv_offset = 0
11
+ @connect_state = true
12
+ @cmd = nil
13
+ end
14
+
15
+ def write(*args)
16
+ pkg = args[0].unpack("C*")
17
+ @cmd ||= pkg[8]
18
+ end
19
+
20
+ def recv(len)
21
+ recv_data = recv_config[@cmd.to_s] || {}
22
+ data = recv_data.key?(:recv_bytes) ? recv_data[:recv_bytes].call(len) : nil
23
+ @recv_offset = len
24
+ data
25
+ end
26
+
27
+ def close
28
+ @recv_offset = 0
29
+ @connect_state = false
30
+ @cmd = nil
31
+ end
32
+
33
+ def closed?
34
+ @connect_state
35
+ end
36
+
37
+ private
38
+
39
+ def recv_config
40
+ {
41
+ "101" => {
42
+ recv_bytes: lambda do |len|
43
+ header = ProtoCommon.header_bytes(CMD::RESP_CODE, 0)
44
+ header[7] = ProtoCommon::TRACKER_BODY_LEN
45
+
46
+ group_name = Utils.array_merge([].fill(0, 0...16), TestConfig::GROUP_NAME.bytes)
47
+ ip = Utils.array_merge([].fill(0, 0...15), TestConfig::STORAGE_IP.bytes)
48
+ port = Utils.number_to_Buffer(TestConfig::STORAGE_PORT.to_i)
49
+ store_path = Array(TestConfig::STORE_PATH)
50
+
51
+ (header+group_name+ip+port+store_path)[@recv_offset...@recv_offset+len].pack("C*")
52
+ end
53
+ },
54
+ "11" => {
55
+ recv_bytes: lambda do |len|
56
+ header = ProtoCommon.header_bytes(CMD::RESP_CODE, 0)
57
+ group_name = Utils.array_merge([].fill(0, 0...16), TestConfig::GROUP_NAME.bytes)
58
+ file_name = TestConfig::FILE_NAME.bytes
59
+ res = (group_name + file_name)
60
+ header[7] = (header + res).length
61
+ res = (header + res)
62
+
63
+ res[@recv_offset...@recv_offset+len].pack("C*")
64
+ end
65
+ },
66
+ "12" => {
67
+ recv_bytes: lambda do |len|
68
+ header = ProtoCommon.header_bytes(CMD::RESP_CODE, 0)
69
+ header.pack("C*")
70
+ end
71
+ }
72
+ }
73
+ end
74
+ end
@@ -0,0 +1,15 @@
1
+ require 'debugger'
2
+ require 'rspec'
3
+ require 'rspec/core'
4
+ require 'rspec/mocks'
5
+ require File.expand_path('../../lib/fastdfs-client', __FILE__)
6
+ require File.expand_path('../test_config', __FILE__)
7
+ require File.expand_path('../mock_tcp_socket', __FILE__)
8
+
9
+ FC = Fastdfs::Client
10
+
11
+ RSpec.configure do |config|
12
+ config.mock_with :rspec do |c|
13
+ c.syntax = [:should, :expect]
14
+ end
15
+ end
@@ -0,0 +1,40 @@
1
+ require 'spec_helper'
2
+
3
+ describe Fastdfs::Client::Storage do
4
+
5
+ let(:host){ "192.168.9.16" }
6
+ let(:port){ "22122" }
7
+
8
+ let(:tracker){ FC::Tracker.new(host, port) }
9
+ let(:storage){ tracker.get_storage }
10
+
11
+ it "initialize the server" do
12
+ expect(FC::Socket).to receive(:new).with(host, port, nil)
13
+ FC::Storage.new(host, port)
14
+ end
15
+
16
+ it "should have access to the storage connection" do
17
+ expect(storage.socket).to receive(:connection)
18
+ expect(storage.socket).to receive(:close)
19
+ storage.upload(TestConfig::FILE)
20
+ end
21
+
22
+ it "should the result attributes group_name and path" do
23
+ res = storage.upload(TestConfig::FILE)
24
+ expect(res).to include(:group_name)
25
+ expect(res).to include(:path)
26
+ end
27
+
28
+ it "can delete file by group and path" do
29
+ res = storage.upload(TestConfig::FILE)
30
+ storage.delete(res[:path], res[:group_name])
31
+ end
32
+
33
+ it "can delete file raise exception" do
34
+ res = storage.upload(TestConfig::FILE)
35
+ result = FC::ProtoCommon.header_bytes(FC::CMD::RESP_CODE, 0, 22)
36
+ TCPSocket.any_instance.stub("recv").and_return(result.pack("C*"))
37
+ expect{ storage.delete("fdsaf", res[:group_name]) }.to raise_error(RuntimeError)
38
+ end
39
+
40
+ end
@@ -0,0 +1,12 @@
1
+ module TestConfig
2
+ STORAGE_IP = "192.168.8.23"
3
+ STORAGE_PORT = "23000"
4
+ STORE_PATH = 0
5
+ GROUP_NAME = "group1"
6
+ FILE_NAME = "M00/04/47/wKgIF1cHcQyAeAF7AAACVHeY6n8267.png"
7
+
8
+ FILE = Tempfile.new("test.jpg")
9
+ FILE.write("testtest")
10
+ FILE.close
11
+
12
+ end
@@ -0,0 +1,38 @@
1
+ require 'spec_helper'
2
+
3
+ describe Fastdfs::Client::Tracker do
4
+
5
+ let(:host){ "192.168.9.16" }
6
+ let(:port){ "22122" }
7
+
8
+ let(:tracker){ FC::Tracker.new(host, port) }
9
+
10
+ it "initialize the server" do
11
+ expect(FC::Socket).to receive(:new).with(host, port, nil)
12
+ FC::Tracker.new(host, port)
13
+ end
14
+
15
+ it "should have access to the storage connection" do
16
+ expect(tracker.socket).to receive(:connection)
17
+ expect(tracker.socket).to receive(:close)
18
+ tracker.get_storage
19
+ end
20
+
21
+ it "should have access to the storage class" do
22
+ expect(tracker.get_storage.class).to eq(FC::Storage)
23
+ end
24
+
25
+ it "verify the server address and port" do
26
+ expect(tracker.get_storage.host).to eq(TestConfig::STORAGE_IP)
27
+ #[0, 0, 0, 0, 0, 89, 216, 0]
28
+ expect(tracker.get_storage.port.to_s).to eq(TestConfig::STORAGE_PORT)
29
+ expect(tracker.get_storage.store_path).to eq(TestConfig::STORE_PATH)
30
+ end
31
+
32
+ it "run server flow" do
33
+ # storage = tracker.get_storage
34
+ # puts "#{storage.host}, #{storage.port}"
35
+ # results = storage.upload(File.open("/Users/huxinghai/Documents/shark/app/assets/images/page.png"))
36
+ # puts storage.delete(results[:path], results[:group_name])
37
+ end
38
+ end
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fastdfs-client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Ka Ka
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-04-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ description: fastdfs upload file client for ruby
28
+ email:
29
+ - huxinghai1988@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - Gemfile
35
+ - Gemfile.lock
36
+ - README.md
37
+ - fastdfs-client.gemspec
38
+ - lib/fastdfs-client.rb
39
+ - lib/fastdfs-client/cmd.rb
40
+ - lib/fastdfs-client/extend_core.rb
41
+ - lib/fastdfs-client/hook.rb
42
+ - lib/fastdfs-client/proto_common.rb
43
+ - lib/fastdfs-client/socket.rb
44
+ - lib/fastdfs-client/storage.rb
45
+ - lib/fastdfs-client/tracker.rb
46
+ - lib/fastdfs-client/utils.rb
47
+ - lib/fastdfs-client/version.rb
48
+ - spec/mock_tcp_socket.rb
49
+ - spec/spec_helper.rb
50
+ - spec/storage_spec.rb
51
+ - spec/test_config.rb
52
+ - spec/tracker_spec.rb
53
+ homepage: https://github.com/huxinghai1988/fastdfs-client-ruby.git
54
+ licenses:
55
+ - MIT
56
+ metadata: {}
57
+ post_install_message:
58
+ rdoc_options: []
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ requirements: []
72
+ rubyforge_project:
73
+ rubygems_version: 2.2.2
74
+ signing_key:
75
+ specification_version: 4
76
+ summary: fastdfs upload file client for ruby
77
+ test_files:
78
+ - spec/mock_tcp_socket.rb
79
+ - spec/spec_helper.rb
80
+ - spec/storage_spec.rb
81
+ - spec/test_config.rb
82
+ - spec/tracker_spec.rb
83
+ has_rdoc: