fastdfs-client 0.0.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.
@@ -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: