shift-nanite 0.4.1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +201 -0
- data/README.rdoc +430 -0
- data/Rakefile +76 -0
- data/TODO +24 -0
- data/bin/nanite-admin +65 -0
- data/bin/nanite-agent +79 -0
- data/bin/nanite-mapper +50 -0
- data/lib/nanite.rb +74 -0
- data/lib/nanite/actor.rb +71 -0
- data/lib/nanite/actor_registry.rb +26 -0
- data/lib/nanite/admin.rb +138 -0
- data/lib/nanite/agent.rb +264 -0
- data/lib/nanite/amqp.rb +58 -0
- data/lib/nanite/cluster.rb +250 -0
- data/lib/nanite/config.rb +112 -0
- data/lib/nanite/console.rb +39 -0
- data/lib/nanite/daemonize.rb +13 -0
- data/lib/nanite/identity.rb +16 -0
- data/lib/nanite/job.rb +104 -0
- data/lib/nanite/local_state.rb +38 -0
- data/lib/nanite/log.rb +66 -0
- data/lib/nanite/log/formatter.rb +39 -0
- data/lib/nanite/mapper.rb +309 -0
- data/lib/nanite/mapper_proxy.rb +67 -0
- data/lib/nanite/nanite_dispatcher.rb +92 -0
- data/lib/nanite/packets.rb +365 -0
- data/lib/nanite/pid_file.rb +52 -0
- data/lib/nanite/reaper.rb +39 -0
- data/lib/nanite/security/cached_certificate_store_proxy.rb +24 -0
- data/lib/nanite/security/certificate.rb +55 -0
- data/lib/nanite/security/certificate_cache.rb +66 -0
- data/lib/nanite/security/distinguished_name.rb +34 -0
- data/lib/nanite/security/encrypted_document.rb +46 -0
- data/lib/nanite/security/rsa_key_pair.rb +53 -0
- data/lib/nanite/security/secure_serializer.rb +68 -0
- data/lib/nanite/security/signature.rb +46 -0
- data/lib/nanite/security/static_certificate_store.rb +35 -0
- data/lib/nanite/security_provider.rb +47 -0
- data/lib/nanite/serializer.rb +52 -0
- data/lib/nanite/state.rb +168 -0
- data/lib/nanite/streaming.rb +125 -0
- data/lib/nanite/util.rb +58 -0
- data/spec/actor_registry_spec.rb +60 -0
- data/spec/actor_spec.rb +77 -0
- data/spec/agent_spec.rb +240 -0
- data/spec/cached_certificate_store_proxy_spec.rb +34 -0
- data/spec/certificate_cache_spec.rb +49 -0
- data/spec/certificate_spec.rb +27 -0
- data/spec/cluster_spec.rb +622 -0
- data/spec/distinguished_name_spec.rb +24 -0
- data/spec/encrypted_document_spec.rb +21 -0
- data/spec/job_spec.rb +251 -0
- data/spec/local_state_spec.rb +130 -0
- data/spec/nanite_dispatcher_spec.rb +136 -0
- data/spec/packet_spec.rb +220 -0
- data/spec/rsa_key_pair_spec.rb +33 -0
- data/spec/secure_serializer_spec.rb +41 -0
- data/spec/serializer_spec.rb +107 -0
- data/spec/signature_spec.rb +30 -0
- data/spec/spec_helper.rb +33 -0
- data/spec/static_certificate_store_spec.rb +30 -0
- data/spec/util_spec.rb +63 -0
- metadata +129 -0
@@ -0,0 +1,46 @@
|
|
1
|
+
if defined?(OpenSSL::PKCS7::PKCS7)
|
2
|
+
Nanite::PKCS7 = OpenSSL::PKCS7::PKCS7
|
3
|
+
else
|
4
|
+
Nanite::PKCS7 = OpenSSL::PKCS7
|
5
|
+
end
|
6
|
+
|
7
|
+
module Nanite
|
8
|
+
|
9
|
+
# Signature that can be validated against certificates
|
10
|
+
class Signature
|
11
|
+
|
12
|
+
FLAGS = OpenSSL::PKCS7::NOCERTS || OpenSSL::PKCS7::BINARY || OpenSSL::PKCS7::NOATTR || OpenSSL::PKCS7::NOSMIMECAP || OpenSSL::PKCS7::DETACH
|
13
|
+
|
14
|
+
# Create signature using certificate and key pair.
|
15
|
+
#
|
16
|
+
# Arguments:
|
17
|
+
# - 'data': Data to be signed
|
18
|
+
# - 'cert': Certificate used for signature
|
19
|
+
# - 'key': RsaKeyPair used for signature
|
20
|
+
#
|
21
|
+
def initialize(data, cert, key)
|
22
|
+
@p7 = OpenSSL::PKCS7.sign(cert.raw_cert, key.raw_key, data, [], FLAGS)
|
23
|
+
@store = OpenSSL::X509::Store.new
|
24
|
+
end
|
25
|
+
|
26
|
+
# Load signature previously serialized via 'data'
|
27
|
+
def self.from_data(data)
|
28
|
+
sig = Signature.allocate
|
29
|
+
sig.instance_variable_set(:@p7, Nanite::PKCS7.new(data))
|
30
|
+
sig.instance_variable_set(:@store, OpenSSL::X509::Store.new)
|
31
|
+
sig
|
32
|
+
end
|
33
|
+
|
34
|
+
# 'true' if signature was created using given cert, 'false' otherwise
|
35
|
+
def match?(cert)
|
36
|
+
@p7.verify([cert.raw_cert], @store, nil, OpenSSL::PKCS7::NOVERIFY)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Signature in PEM format
|
40
|
+
def data
|
41
|
+
@p7.to_pem
|
42
|
+
end
|
43
|
+
alias :to_s :data
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Nanite
|
2
|
+
|
3
|
+
# Simple certificate store, serves a static set of certificates.
|
4
|
+
class StaticCertificateStore
|
5
|
+
|
6
|
+
# Initialize store:
|
7
|
+
#
|
8
|
+
# - Signer certificates are used when loading data to check the digital
|
9
|
+
# signature. The signature associated with the serialized data needs
|
10
|
+
# to match with one of the signer certificates for loading to succeed.
|
11
|
+
#
|
12
|
+
# - Recipient certificates are used when serializing data for encryption.
|
13
|
+
# Loading the data can only be done through serializers that have been
|
14
|
+
# initialized with a certificate that's in the recipient certificates if
|
15
|
+
# encryption is enabled.
|
16
|
+
#
|
17
|
+
def initialize(signer_certs, recipients_certs)
|
18
|
+
signer_certs = [ signer_certs ] unless signer_certs.respond_to?(:each)
|
19
|
+
@signer_certs = signer_certs
|
20
|
+
recipients_certs = [ recipients_certs ] unless recipients_certs.respond_to?(:each)
|
21
|
+
@recipients_certs = recipients_certs
|
22
|
+
end
|
23
|
+
|
24
|
+
# Retrieve signer certificate for given id
|
25
|
+
def get_signer(identity)
|
26
|
+
@signer_certs
|
27
|
+
end
|
28
|
+
|
29
|
+
# Recipient certificate(s) that will be able to decrypt the serialized data
|
30
|
+
def get_recipients(obj)
|
31
|
+
@recipients_certs
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Nanite
|
2
|
+
# This class is used to interface the nanite mapper with an external security
|
3
|
+
# module.
|
4
|
+
# There are two points of integration:
|
5
|
+
# 1. When an agent registers with a mapper
|
6
|
+
# 2. When an agent sends a request to another agent
|
7
|
+
#
|
8
|
+
# In both these cases the security module is called back and can deny the
|
9
|
+
# operation.
|
10
|
+
# Note: it's the responsability of the module to do any logging or
|
11
|
+
# notification that is required.
|
12
|
+
class SecurityProvider
|
13
|
+
|
14
|
+
# Register an external security module
|
15
|
+
# This module should expose the 'authorize_registration' and
|
16
|
+
# 'authorize_request' methods.
|
17
|
+
def self.register(mod)
|
18
|
+
@security_module = mod
|
19
|
+
end
|
20
|
+
|
21
|
+
# Used internally by nanite to retrieve the current security module
|
22
|
+
def self.get
|
23
|
+
@security_module || default_security_module
|
24
|
+
end
|
25
|
+
|
26
|
+
# Default security module, authorizes all operations
|
27
|
+
def self.default_security_module
|
28
|
+
@default_sec_mod ||= DefaultSecurityModule.new
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
# Default security module
|
34
|
+
class DefaultSecurityModule
|
35
|
+
|
36
|
+
# Authorize registration of agent (registration is an instance of Register)
|
37
|
+
def authorize_registration(registration)
|
38
|
+
true
|
39
|
+
end
|
40
|
+
|
41
|
+
# Authorize given inter-agent request (request is an instance of Request)
|
42
|
+
def authorize_request(request)
|
43
|
+
true
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Nanite
|
2
|
+
class Serializer
|
3
|
+
|
4
|
+
class SerializationError < StandardError
|
5
|
+
attr_accessor :action, :packet
|
6
|
+
def initialize(action, packet, serializers, msg = nil)
|
7
|
+
@action, @packet = action, packet
|
8
|
+
msg = ":\n#{msg}" if msg && !msg.empty?
|
9
|
+
super("Could not #{action} #{packet.inspect} using #{serializers.inspect}#{msg}")
|
10
|
+
end
|
11
|
+
end # SerializationError
|
12
|
+
|
13
|
+
# The secure serializer should not be part of the cascading
|
14
|
+
def initialize(preferred_format = :marshal)
|
15
|
+
preferred_format ||= :marshal
|
16
|
+
if preferred_format.to_s == 'secure'
|
17
|
+
@serializers = [ SecureSerializer ]
|
18
|
+
else
|
19
|
+
preferred_serializer = SERIALIZERS[preferred_format.to_sym]
|
20
|
+
@serializers = SERIALIZERS.values.clone
|
21
|
+
@serializers.unshift(@serializers.delete(preferred_serializer)) if preferred_serializer
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def dump(packet)
|
26
|
+
cascade_serializers(:dump, packet)
|
27
|
+
end
|
28
|
+
|
29
|
+
def load(packet)
|
30
|
+
cascade_serializers(:load, packet)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
SERIALIZERS = {:json => JSON, :marshal => Marshal, :yaml => YAML}.freeze
|
36
|
+
|
37
|
+
def cascade_serializers(action, packet)
|
38
|
+
errors = []
|
39
|
+
@serializers.map do |serializer|
|
40
|
+
begin
|
41
|
+
o = serializer.send(action, packet)
|
42
|
+
rescue Exception => e
|
43
|
+
o = nil
|
44
|
+
errors << "#{e.message}\n\t#{e.backtrace[0]}"
|
45
|
+
end
|
46
|
+
return o if o
|
47
|
+
end
|
48
|
+
raise SerializationError.new(action, packet, @serializers, errors.join("\n"))
|
49
|
+
end
|
50
|
+
|
51
|
+
end # Serializer
|
52
|
+
end # Nanite
|
data/lib/nanite/state.rb
ADDED
@@ -0,0 +1,168 @@
|
|
1
|
+
require 'redis'
|
2
|
+
|
3
|
+
module Nanite
|
4
|
+
class State
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
# this class encapsulates the state of a nanite system using redis as the
|
8
|
+
# data store. here is the schema, for each agent we store a number of items,
|
9
|
+
# for a nanite with the identity: nanite-foobar we store the following things:
|
10
|
+
#
|
11
|
+
# nanite-foobar: 0.72 # load average or 'status'
|
12
|
+
# s-nanite-foobar: { /foo/bar, /foo/nik } # a SET of the provided services
|
13
|
+
# tg-nanite-foobar: { foo-42, customer-12 } # a SET of the tags for this agent
|
14
|
+
# t-nanite-foobar: 123456789 # unix timestamp of the last state update
|
15
|
+
#
|
16
|
+
# also we do an inverted index for quick lookup of agents providing a certain
|
17
|
+
# service, so for each service the agent provides, we add the nanite to a SET
|
18
|
+
# of all the nanites that provide said service:
|
19
|
+
#
|
20
|
+
# foo/bar: { nanite-foobar, nanite-nickelbag, nanite-another } # redis SET
|
21
|
+
#
|
22
|
+
# we do that same thing for tags:
|
23
|
+
# some-tag: { nanite-foobar, nanite-nickelbag, nanite-another } # redis SET
|
24
|
+
#
|
25
|
+
# This way we can do a lookup of what nanites provide a set of services and tags based
|
26
|
+
# on redis SET intersection:
|
27
|
+
#
|
28
|
+
# nanites_for('/gems/list', 'some-tag')
|
29
|
+
# => returns a nested array of nanites and their state that provide the intersection
|
30
|
+
# of these two service tags
|
31
|
+
|
32
|
+
def initialize(redis)
|
33
|
+
Nanite::Log.info("[setup] initializing redis state: #{redis}")
|
34
|
+
host, port = redis.split(':')
|
35
|
+
host ||= '127.0.0.1'
|
36
|
+
port ||= '6379'
|
37
|
+
@redis = Redis.new :host => host, :port => port
|
38
|
+
end
|
39
|
+
|
40
|
+
def log_redis_error(meth,&blk)
|
41
|
+
blk.call
|
42
|
+
rescue Exception => e
|
43
|
+
Nanite::Log.info("redis error in method: #{meth}")
|
44
|
+
raise e
|
45
|
+
end
|
46
|
+
|
47
|
+
def [](nanite)
|
48
|
+
log_redis_error("[]") do
|
49
|
+
status = @redis[nanite]
|
50
|
+
timestamp = @redis["t-#{nanite}"]
|
51
|
+
services = @redis.set_members("s-#{nanite}")
|
52
|
+
tags = @redis.set_members("tg-#{nanite}")
|
53
|
+
return nil unless status && timestamp && services
|
54
|
+
{:services => services, :status => status, :timestamp => timestamp.to_i, :tags => tags}
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def []=(nanite, attributes)
|
59
|
+
log_redis_error("[]=") do
|
60
|
+
update_state(nanite, attributes[:status], attributes[:services], attributes[:tags])
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def delete(nanite)
|
65
|
+
log_redis_error("delete") do
|
66
|
+
(@redis.set_members("s-#{nanite}")||[]).each do |srv|
|
67
|
+
@redis.set_delete(srv, nanite)
|
68
|
+
if @redis.set_count(srv) == 0
|
69
|
+
@redis.delete(srv)
|
70
|
+
@redis.set_delete("naniteservices", srv)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
(@redis.set_members("tg-#{nanite}")||[]).each do |tag|
|
74
|
+
@redis.set_delete(tag, nanite)
|
75
|
+
if @redis.set_count(tag) == 0
|
76
|
+
@redis.delete(tag)
|
77
|
+
@redis.set_delete("nanitetags", tag)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
@redis.delete nanite
|
81
|
+
@redis.delete "s-#{nanite}"
|
82
|
+
@redis.delete "t-#{nanite}"
|
83
|
+
@redis.delete "tg-#{nanite}"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def all_services
|
88
|
+
log_redis_error("all_services") do
|
89
|
+
@redis.set_members("naniteservices")
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def all_tags
|
94
|
+
log_redis_error("all_tags") do
|
95
|
+
@redis.set_members("nanitetags")
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def update_state(name, status, services, tags)
|
100
|
+
old_services = @redis.set_members("s-#{name}")
|
101
|
+
if old_services
|
102
|
+
(old_services - services).each do |s|
|
103
|
+
@redis.set_delete(s, name)
|
104
|
+
@redis.set_delete("naniteservices", s)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
old_tags = @redis.set_members("tg-#{name}")
|
108
|
+
if old_tags
|
109
|
+
(old_tags - tags).each do |t|
|
110
|
+
@redis.set_delete(t, name)
|
111
|
+
@redis.set_delete("nanitetags", t)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
@redis.delete("s-#{name}")
|
115
|
+
services.each do |srv|
|
116
|
+
@redis.set_add(srv, name)
|
117
|
+
@redis.set_add("s-#{name}", srv)
|
118
|
+
@redis.set_add("naniteservices", srv)
|
119
|
+
end
|
120
|
+
@redis.delete("tg-#{name}")
|
121
|
+
tags.each do |tag|
|
122
|
+
next if tag.nil?
|
123
|
+
@redis.set_add(tag, name)
|
124
|
+
@redis.set_add("tg-#{name}", tag)
|
125
|
+
@redis.set_add("nanitetags", tag)
|
126
|
+
end
|
127
|
+
update_status(name, status)
|
128
|
+
end
|
129
|
+
|
130
|
+
def update_status(name, status)
|
131
|
+
@redis[name] = status
|
132
|
+
@redis["t-#{name}"] = Time.now.utc.to_i
|
133
|
+
end
|
134
|
+
|
135
|
+
def list_nanites
|
136
|
+
log_redis_error("list_nanites") do
|
137
|
+
@redis.keys("nanite-*")
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def size
|
142
|
+
list_nanites.size
|
143
|
+
end
|
144
|
+
|
145
|
+
def clear_state
|
146
|
+
log_redis_error("clear_state") do
|
147
|
+
@redis.keys("*").each {|k| @redis.delete k}
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def each
|
152
|
+
list_nanites.each do |nan|
|
153
|
+
yield nan, self[nan]
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def nanites_for(service, *tags)
|
158
|
+
keys = tags.dup << service
|
159
|
+
log_redis_error("nanites_for") do
|
160
|
+
res = []
|
161
|
+
(@redis.set_intersect(keys)||[]).each do |nan|
|
162
|
+
res << [nan, self[nan]]
|
163
|
+
end
|
164
|
+
res
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
module Nanite
|
2
|
+
# Nanite actors can transfer files to each other.
|
3
|
+
#
|
4
|
+
# ==== Options
|
5
|
+
#
|
6
|
+
# filename : you guessed it, name of the file!
|
7
|
+
# domain : part of the routing key used to locate receiver(s)
|
8
|
+
# destination : is a name of the file as it gonna be stored at the destination
|
9
|
+
# meta :
|
10
|
+
#
|
11
|
+
# File streaming is done in chunks. When file streaming starts,
|
12
|
+
# Nanite::FileStart packet is sent, followed by one or more (usually more ;))
|
13
|
+
# Nanite::FileChunk packets each 16384 (16K) in size. Once file streaming is done,
|
14
|
+
# Nanite::FileEnd packet is sent.
|
15
|
+
#
|
16
|
+
# 16K is a packet size because on certain UNIX-like operating systems, you cannot read/write
|
17
|
+
# more than that in one operation via socket.
|
18
|
+
#
|
19
|
+
# ==== Domains
|
20
|
+
#
|
21
|
+
# Streaming happens using a topic exchange called 'file broadcast', with keys
|
22
|
+
# formatted as "nanite.filepeer.DOMAIN". Domain variable in the key lets senders and
|
23
|
+
# receivers find each other in the cluster. Default domain is 'global'.
|
24
|
+
#
|
25
|
+
# Domains also serve as a way to register a callback Nanite agent executes once file
|
26
|
+
# streaming is completed. If a callback with name of domain is registered, it is called.
|
27
|
+
#
|
28
|
+
# Callbacks are registered by passing a block to subscribe_to_files method.
|
29
|
+
module FileStreaming
|
30
|
+
def broadcast_file(filename, options = {})
|
31
|
+
if File.exist?(filename)
|
32
|
+
File.open(filename, 'rb') do |file|
|
33
|
+
broadcast_data(filename, file, options)
|
34
|
+
end
|
35
|
+
else
|
36
|
+
return "file not found"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def broadcast_data(filename, io, options = {})
|
41
|
+
domain = options[:domain] || 'global'
|
42
|
+
filename = File.basename(filename)
|
43
|
+
dest = options[:destination] || filename
|
44
|
+
sent = 0
|
45
|
+
|
46
|
+
begin
|
47
|
+
file_push = Nanite::FileStart.new(filename, dest, Identity.generate)
|
48
|
+
amq.topic('file broadcast').publish(serializer.dump(file_push), :key => "nanite.filepeer.#{domain}")
|
49
|
+
res = Nanite::FileChunk.new(file_push.token)
|
50
|
+
while chunk = io.read(16384)
|
51
|
+
res.chunk = chunk
|
52
|
+
amq.topic('file broadcast').publish(serializer.dump(res), :key => "nanite.filepeer.#{domain}")
|
53
|
+
sent += chunk.length
|
54
|
+
end
|
55
|
+
fend = Nanite::FileEnd.new(file_push.token, options[:meta])
|
56
|
+
amq.topic('file broadcast').publish(serializer.dump(fend), :key => "nanite.filepeer.#{domain}")
|
57
|
+
""
|
58
|
+
ensure
|
59
|
+
io.close
|
60
|
+
end
|
61
|
+
|
62
|
+
sent
|
63
|
+
end
|
64
|
+
|
65
|
+
# FileState represents a file download in progress.
|
66
|
+
# It incapsulates the following information:
|
67
|
+
#
|
68
|
+
# * unique operation token
|
69
|
+
# * domain (namespace for file streaming operations)
|
70
|
+
# * file IO chunks are written to on receiver's side
|
71
|
+
class FileState
|
72
|
+
|
73
|
+
def initialize(token, dest, domain, write, blk)
|
74
|
+
@token = token
|
75
|
+
@cb = blk
|
76
|
+
@domain = domain
|
77
|
+
@write = write
|
78
|
+
|
79
|
+
if write
|
80
|
+
@filename = File.join(Nanite.agent.file_root, dest)
|
81
|
+
@dest = File.open(@filename, 'wb')
|
82
|
+
else
|
83
|
+
@dest = dest
|
84
|
+
end
|
85
|
+
|
86
|
+
@data = ""
|
87
|
+
end
|
88
|
+
|
89
|
+
def handle_packet(packet)
|
90
|
+
case packet
|
91
|
+
when Nanite::FileChunk
|
92
|
+
Nanite::Log.debug "written chunk to #{@dest.inspect}"
|
93
|
+
@data << packet.chunk
|
94
|
+
|
95
|
+
if @write
|
96
|
+
@dest.write(packet.chunk)
|
97
|
+
end
|
98
|
+
when Nanite::FileEnd
|
99
|
+
Nanite::Log.debug "#{@dest.inspect} receiving is completed"
|
100
|
+
if @write
|
101
|
+
@dest.close
|
102
|
+
end
|
103
|
+
|
104
|
+
@cb.call(@data, @dest, packet.meta)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
def subscribe_to_files(domain='global', write=false, &blk)
|
111
|
+
Nanite::Log.info "subscribing to file broadcasts for #{domain}"
|
112
|
+
@files ||= {}
|
113
|
+
amq.queue("files#{domain}").bind(amq.topic('file broadcast'), :key => "nanite.filepeer.#{domain}").subscribe do |packet|
|
114
|
+
case msg = serializer.load(packet)
|
115
|
+
when FileStart
|
116
|
+
@files[msg.token] = FileState.new(msg.token, msg.dest, domain, write, blk)
|
117
|
+
when FileChunk, FileEnd
|
118
|
+
if file = @files[msg.token]
|
119
|
+
file.handle_packet(msg)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|