pantry 0.0.0 → 0.1.0
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 +4 -4
- data/.gitignore +9 -0
- data/.ruby-version +1 -0
- data/.travis.yml +19 -0
- data/Gemfile +15 -0
- data/Guardfile +16 -0
- data/LICENSE +20 -0
- data/README.md +53 -0
- data/Rakefile +18 -0
- data/Vagrantfile +86 -0
- data/bin/pantry +11 -0
- data/bin/pantry-client +38 -0
- data/bin/pantry-server +33 -0
- data/dist/client.yml +79 -0
- data/dist/server.yml +56 -0
- data/dist/upstart/pantry-client.conf +12 -0
- data/dist/upstart/pantry-server.conf +12 -0
- data/doc/message_packet.dot +19 -0
- data/doc/message_packet.dot.png +0 -0
- data/doc/network_topology.dot +42 -0
- data/doc/network_topology.dot.png +0 -0
- data/lib/celluloid_zmq_patches.rb +16 -0
- data/lib/opt_parse_plus.rb +184 -0
- data/lib/pantry.rb +197 -0
- data/lib/pantry/cli.rb +154 -0
- data/lib/pantry/client.rb +131 -0
- data/lib/pantry/client_info.rb +34 -0
- data/lib/pantry/client_registry.rb +104 -0
- data/lib/pantry/command.rb +194 -0
- data/lib/pantry/command_handler.rb +53 -0
- data/lib/pantry/command_line.rb +115 -0
- data/lib/pantry/commands/create_client.rb +30 -0
- data/lib/pantry/commands/download_directory.rb +35 -0
- data/lib/pantry/commands/echo.rb +32 -0
- data/lib/pantry/commands/edit_application.rb +60 -0
- data/lib/pantry/commands/register_client.rb +38 -0
- data/lib/pantry/commands/status.rb +78 -0
- data/lib/pantry/commands/sync_directory.rb +50 -0
- data/lib/pantry/commands/update_application.rb +45 -0
- data/lib/pantry/commands/upload_file.rb +68 -0
- data/lib/pantry/communication.rb +20 -0
- data/lib/pantry/communication/client.rb +75 -0
- data/lib/pantry/communication/client_filter.rb +117 -0
- data/lib/pantry/communication/file_service.rb +125 -0
- data/lib/pantry/communication/file_service/file_progress.rb +164 -0
- data/lib/pantry/communication/file_service/receive_file.rb +97 -0
- data/lib/pantry/communication/file_service/send_file.rb +74 -0
- data/lib/pantry/communication/publish_socket.rb +20 -0
- data/lib/pantry/communication/reading_socket.rb +89 -0
- data/lib/pantry/communication/receive_socket.rb +23 -0
- data/lib/pantry/communication/security.rb +44 -0
- data/lib/pantry/communication/security/authentication.rb +98 -0
- data/lib/pantry/communication/security/curve_key_store.rb +120 -0
- data/lib/pantry/communication/security/curve_security.rb +70 -0
- data/lib/pantry/communication/security/null_security.rb +32 -0
- data/lib/pantry/communication/send_socket.rb +19 -0
- data/lib/pantry/communication/serialize_message.rb +84 -0
- data/lib/pantry/communication/server.rb +97 -0
- data/lib/pantry/communication/subscribe_socket.rb +33 -0
- data/lib/pantry/communication/wait_list.rb +45 -0
- data/lib/pantry/communication/writing_socket.rb +46 -0
- data/lib/pantry/config.rb +182 -0
- data/lib/pantry/file_editor.rb +67 -0
- data/lib/pantry/logger.rb +78 -0
- data/lib/pantry/message.rb +134 -0
- data/lib/pantry/multi_command.rb +36 -0
- data/lib/pantry/server.rb +132 -0
- data/lib/pantry/test/acceptance.rb +83 -0
- data/lib/pantry/test/support/fake_fs.rb +31 -0
- data/lib/pantry/test/support/matchers.rb +13 -0
- data/lib/pantry/test/support/minitest.rb +13 -0
- data/lib/pantry/test/support/mock_ui.rb +23 -0
- data/lib/pantry/test/unit.rb +13 -0
- data/lib/pantry/ui.rb +68 -0
- data/lib/pantry/version.rb +3 -0
- data/pantry.gemspec +40 -0
- data/test/acceptance/cli/error_handling_test.rb +7 -0
- data/test/acceptance/cli/execute_command_on_clients_test.rb +32 -0
- data/test/acceptance/cli/request_info_from_server_test.rb +44 -0
- data/test/acceptance/communication/client_requests_info_from_server_test.rb +28 -0
- data/test/acceptance/communication/heartbeat_test.rb +19 -0
- data/test/acceptance/communication/pub_sub_communication_test.rb +53 -0
- data/test/acceptance/communication/security_test.rb +117 -0
- data/test/acceptance/communication/server_requests_info_from_client_test.rb +41 -0
- data/test/acceptance/test_helper.rb +25 -0
- data/test/fixtures/config.yml +22 -0
- data/test/fixtures/empty.yml +2 -0
- data/test/fixtures/file_to_upload +3 -0
- data/test/root_dir/.gitkeep +0 -0
- data/test/unit/cli_test.rb +173 -0
- data/test/unit/client_registry_test.rb +61 -0
- data/test/unit/client_test.rb +128 -0
- data/test/unit/command_handler_test.rb +79 -0
- data/test/unit/command_line_test.rb +5 -0
- data/test/unit/command_test.rb +206 -0
- data/test/unit/commands/create_client_test.rb +25 -0
- data/test/unit/commands/download_directory_test.rb +58 -0
- data/test/unit/commands/echo_test.rb +22 -0
- data/test/unit/commands/edit_application_test.rb +84 -0
- data/test/unit/commands/register_client_test.rb +41 -0
- data/test/unit/commands/status_test.rb +81 -0
- data/test/unit/commands/sync_directory_test.rb +75 -0
- data/test/unit/commands/update_application_test.rb +35 -0
- data/test/unit/commands/upload_file_test.rb +51 -0
- data/test/unit/communication/client_filter_test.rb +262 -0
- data/test/unit/communication/client_test.rb +99 -0
- data/test/unit/communication/file_service/receive_file_test.rb +214 -0
- data/test/unit/communication/file_service/send_file_test.rb +110 -0
- data/test/unit/communication/file_service_test.rb +56 -0
- data/test/unit/communication/publish_socket_test.rb +19 -0
- data/test/unit/communication/reading_socket_test.rb +110 -0
- data/test/unit/communication/receive_socket_test.rb +20 -0
- data/test/unit/communication/security/authentication_test.rb +97 -0
- data/test/unit/communication/security/curve_key_store_test.rb +110 -0
- data/test/unit/communication/security/curve_security_test.rb +44 -0
- data/test/unit/communication/security/null_security_test.rb +15 -0
- data/test/unit/communication/security_test.rb +49 -0
- data/test/unit/communication/send_socket_test.rb +19 -0
- data/test/unit/communication/serialize_message_test.rb +128 -0
- data/test/unit/communication/server_test.rb +106 -0
- data/test/unit/communication/subscribe_socket_test.rb +46 -0
- data/test/unit/communication/wait_list_test.rb +60 -0
- data/test/unit/communication/writing_socket_test.rb +46 -0
- data/test/unit/config_test.rb +150 -0
- data/test/unit/logger_test.rb +79 -0
- data/test/unit/message_test.rb +179 -0
- data/test/unit/multi_command_test.rb +45 -0
- data/test/unit/opt_parse_plus_test.rb +218 -0
- data/test/unit/pantry_test.rb +82 -0
- data/test/unit/server_test.rb +166 -0
- data/test/unit/test_helper.rb +25 -0
- data/test/unit/ui_test.rb +58 -0
- metadata +389 -13
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module Pantry
|
|
2
|
+
module Commands
|
|
3
|
+
|
|
4
|
+
# Base class for any Command that needs to sync a set of files in a directory
|
|
5
|
+
# from the Server down to the Client.
|
|
6
|
+
#
|
|
7
|
+
# Subclasses need to define where on the server the files live and where on
|
|
8
|
+
# the client the files will be written to. Both #server_directory and #client_directory
|
|
9
|
+
# are executed on the Client so #client is accessible for added information.
|
|
10
|
+
#
|
|
11
|
+
# This command expects simple directories with a small number of files that are
|
|
12
|
+
# themselves small in size, as this command reads every file into memory and sends
|
|
13
|
+
# that raw content back to the Client. If there are more substantial files to transfer
|
|
14
|
+
# use #send_file and #receive_file instead.
|
|
15
|
+
class SyncDirectory < Pantry::Command
|
|
16
|
+
|
|
17
|
+
def server_directory(local_root)
|
|
18
|
+
raise "Specify the read directory on the server"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def client_directory(local_root)
|
|
22
|
+
raise "Specify the write directory on the client"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def perform(message)
|
|
26
|
+
dir_contents = send_request!(
|
|
27
|
+
Pantry::Commands::DownloadDirectory.new(
|
|
28
|
+
server_directory(Pathname.new(""))
|
|
29
|
+
).to_message
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
write_to = client_directory(Pantry.root)
|
|
33
|
+
FileUtils.mkdir_p(write_to)
|
|
34
|
+
|
|
35
|
+
dir_contents.body.each do |(file_name, file_contents)|
|
|
36
|
+
file_path = write_to.join(file_name).cleanpath
|
|
37
|
+
FileUtils.mkdir_p(File.dirname(file_path))
|
|
38
|
+
|
|
39
|
+
File.open(file_path, "w+") do |file|
|
|
40
|
+
file.write(file_contents)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
true
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module Pantry
|
|
2
|
+
module Commands
|
|
3
|
+
|
|
4
|
+
# Upload and save new configuration for an Application
|
|
5
|
+
#
|
|
6
|
+
# See EditApplication for more information
|
|
7
|
+
class UpdateApplication < Pantry::Command
|
|
8
|
+
|
|
9
|
+
def initialize(application_name = nil, config_body = nil)
|
|
10
|
+
@application_name = application_name
|
|
11
|
+
@config_body = config_body
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def to_message
|
|
15
|
+
super.tap do |msg|
|
|
16
|
+
msg << @application_name
|
|
17
|
+
msg << @config_body
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def perform(message)
|
|
22
|
+
application_name = message.body[0]
|
|
23
|
+
config_body = message.body[1]
|
|
24
|
+
|
|
25
|
+
app_config_file = Pantry.root.join("applications", application_name, "config.yml")
|
|
26
|
+
FileUtils.mkdir_p(File.dirname(app_config_file))
|
|
27
|
+
|
|
28
|
+
begin
|
|
29
|
+
Psych.parse(config_body, "config.yml")
|
|
30
|
+
rescue => ex
|
|
31
|
+
# Invalid YAML, don't save!
|
|
32
|
+
return [false, ex.message]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
File.open(app_config_file, "w+") do |file|
|
|
36
|
+
file.write(config_body)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
true
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
module Pantry
|
|
2
|
+
module Commands
|
|
3
|
+
|
|
4
|
+
# Base class for any command that needs to upload a single file where
|
|
5
|
+
# that file is small enough for its contents to be passed around in plain messages.
|
|
6
|
+
# For larger files that shouldn't be pulled entirely into memory, please use
|
|
7
|
+
# #send_file and #receive_file instead.
|
|
8
|
+
#
|
|
9
|
+
# This class is not used directly. Subclass to define the CLI command pattern
|
|
10
|
+
# and the directory where the uploaded file will end up.
|
|
11
|
+
class UploadFile < Pantry::Command
|
|
12
|
+
|
|
13
|
+
attr_reader :file_to_upload
|
|
14
|
+
|
|
15
|
+
def initialize(file_to_upload = nil)
|
|
16
|
+
@file_to_upload = file_to_upload
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Specify the directory this file should be written to
|
|
20
|
+
# When applicable, +application+ is the application that should know about this file
|
|
21
|
+
def upload_directory(application)
|
|
22
|
+
raise "Must implement #upload_directory in subclass"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Specify any required options for this Command by long-name
|
|
26
|
+
# For example, to require the base APPLICATION option, return %i(application)
|
|
27
|
+
# Does not matter if the list is of strings or symbols.
|
|
28
|
+
def required_options
|
|
29
|
+
[]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def prepare_message(options)
|
|
33
|
+
required_options.each do |required|
|
|
34
|
+
unless options[required]
|
|
35
|
+
raise Pantry::MissingOption, "Required option #{required} is missing"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
super.tap do |message|
|
|
40
|
+
message << options
|
|
41
|
+
message << File.basename(@file_to_upload)
|
|
42
|
+
message << File.read(@file_to_upload)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def perform(message)
|
|
47
|
+
cmd_options = message.body[0]
|
|
48
|
+
file_name = message.body[1]
|
|
49
|
+
file_body = message.body[2]
|
|
50
|
+
|
|
51
|
+
upload_dir = upload_directory(cmd_options)
|
|
52
|
+
|
|
53
|
+
FileUtils.mkdir_p(upload_dir)
|
|
54
|
+
File.open(upload_dir.join(file_name), "w+") do |file|
|
|
55
|
+
file.write file_body
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
true
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def receive_server_response(response)
|
|
62
|
+
# Say nothing. Finishing is enough
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Pantry
|
|
2
|
+
|
|
3
|
+
# The Communication subsystem of Pantry is managed via 0MQ through the
|
|
4
|
+
# Celluloid::ZMQ library.
|
|
5
|
+
module Communication
|
|
6
|
+
Celluloid::ZMQ.init
|
|
7
|
+
|
|
8
|
+
# Configure a ZMQ socket with some common options
|
|
9
|
+
def self.configure_socket(socket)
|
|
10
|
+
# Ensure the socket doesn't spam us trying to reconnect
|
|
11
|
+
# after a disconnect or authentication failure
|
|
12
|
+
socket.set(::ZMQ::RECONNECT_IVL, 1_000)
|
|
13
|
+
socket.set(::ZMQ::RECONNECT_IVL_MAX, 30_000)
|
|
14
|
+
|
|
15
|
+
# Drop all messages on shutdown
|
|
16
|
+
socket.linger = 0
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
module Pantry
|
|
2
|
+
module Communication
|
|
3
|
+
|
|
4
|
+
# The communication layer of a Pantry::Client
|
|
5
|
+
# This class manages all of the ZeroMQ sockets and underlying
|
|
6
|
+
# communication systems, handling the sending and receiving of messages.
|
|
7
|
+
class Client
|
|
8
|
+
include Celluloid
|
|
9
|
+
|
|
10
|
+
def initialize(listener)
|
|
11
|
+
@listener = listener
|
|
12
|
+
@response_wait_list = Communication::WaitList.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Start up the networking layer, opening up sockets and getting
|
|
16
|
+
# ready for communication.
|
|
17
|
+
def run
|
|
18
|
+
@security = Communication::Security.new_client
|
|
19
|
+
|
|
20
|
+
@subscribe_socket = Communication::SubscribeSocket.new_link(
|
|
21
|
+
Pantry.config.server_host,
|
|
22
|
+
Pantry.config.pub_sub_port,
|
|
23
|
+
@security
|
|
24
|
+
)
|
|
25
|
+
@subscribe_socket.add_listener(self)
|
|
26
|
+
@subscribe_socket.filter_on(@listener.filter)
|
|
27
|
+
@subscribe_socket.open
|
|
28
|
+
|
|
29
|
+
@send_socket = Communication::SendSocket.new_link(
|
|
30
|
+
Pantry.config.server_host,
|
|
31
|
+
Pantry.config.receive_port,
|
|
32
|
+
@security
|
|
33
|
+
)
|
|
34
|
+
@send_socket.open
|
|
35
|
+
|
|
36
|
+
@file_service = Communication::FileService.new_link(
|
|
37
|
+
Pantry.config.server_host,
|
|
38
|
+
Pantry.config.file_service_port,
|
|
39
|
+
@security
|
|
40
|
+
)
|
|
41
|
+
@file_service.start_client
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Callback from the SubscribeSocket when a message is received.
|
|
45
|
+
def handle_message(message)
|
|
46
|
+
if @response_wait_list.waiting_for?(message)
|
|
47
|
+
@response_wait_list.received(message)
|
|
48
|
+
else
|
|
49
|
+
@listener.receive_message(message)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def send_request(message)
|
|
54
|
+
@response_wait_list.wait_for(message).tap do
|
|
55
|
+
send_message(message)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def send_message(message)
|
|
60
|
+
message.from = @listener
|
|
61
|
+
@send_socket.send_message(message)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def receive_file(file_size, file_checksum)
|
|
65
|
+
@file_service.receive_file(file_size, file_checksum)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def send_file(file_path, receiver_uuid, file_uuid)
|
|
69
|
+
@file_service.send_file(file_path, receiver_uuid, file_uuid)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
module Pantry
|
|
2
|
+
module Communication
|
|
3
|
+
|
|
4
|
+
# ClientFilter handles and manages building filters that map configuration values
|
|
5
|
+
# to 0MQ stream names. A message stream is a period-delimited string that works
|
|
6
|
+
# with 0MQ's Subscription prefix matcher, allowing Clients to choose which messages
|
|
7
|
+
# they want to receive. Streams are built to enable tiered delivery capability.
|
|
8
|
+
#
|
|
9
|
+
# For example, a Client with the application 'pantry' and roles 'app' and 'db' will
|
|
10
|
+
# subscribe to the following streams:
|
|
11
|
+
#
|
|
12
|
+
# pantry
|
|
13
|
+
# pantry.app
|
|
14
|
+
# pantry.db
|
|
15
|
+
#
|
|
16
|
+
# Similarly to differentiate between environments, the environment is added inbetween
|
|
17
|
+
# the application and the role:
|
|
18
|
+
#
|
|
19
|
+
# pantry
|
|
20
|
+
# pantry.production
|
|
21
|
+
# pantry.production.app
|
|
22
|
+
# pantry.production.db
|
|
23
|
+
#
|
|
24
|
+
# This class is also used to when sending messages, to choose which specific stream
|
|
25
|
+
# (e.g. the deepest buildable) to send a given message down.
|
|
26
|
+
#
|
|
27
|
+
# A client identity token can also be given via +identity+. If identity is provided
|
|
28
|
+
# then that stream will be chosen above all others. Use this to send a message to
|
|
29
|
+
# specific clients.
|
|
30
|
+
class ClientFilter
|
|
31
|
+
|
|
32
|
+
attr_reader :application, :environment, :roles, :identity
|
|
33
|
+
|
|
34
|
+
def initialize(application: nil, environment: nil, roles: [], identity: nil)
|
|
35
|
+
@application = application
|
|
36
|
+
@environment = environment
|
|
37
|
+
@roles = roles || []
|
|
38
|
+
@identity = identity
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# List out all communication streams this ClientFilter is configured to know about.
|
|
42
|
+
def streams
|
|
43
|
+
list = []
|
|
44
|
+
base_stream = []
|
|
45
|
+
|
|
46
|
+
if @identity
|
|
47
|
+
list << @identity
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
if @application
|
|
51
|
+
list << @application
|
|
52
|
+
base_stream = [@application]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
if @environment
|
|
56
|
+
list << [base_stream, @environment].flatten.compact.join(".")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
@roles.each do |role|
|
|
60
|
+
list << [base_stream, @environment, role].flatten.compact.join(".")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
list = list.flatten.compact
|
|
64
|
+
list.empty? ? [""] : list
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Return the most specific stream that matches this ClientFilter.
|
|
68
|
+
# +identity+ is chosen above all others.
|
|
69
|
+
def stream
|
|
70
|
+
if @identity
|
|
71
|
+
@identity
|
|
72
|
+
else
|
|
73
|
+
[@application, @environment, @roles.first].compact.join(".")
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def ==(other)
|
|
78
|
+
return false unless other
|
|
79
|
+
return false unless other.is_a?(ClientFilter)
|
|
80
|
+
|
|
81
|
+
self.application == other.application &&
|
|
82
|
+
self.environment == other.environment &&
|
|
83
|
+
self.roles == other.roles &&
|
|
84
|
+
self.identity == other.identity
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Will this filter match on the given stream?
|
|
88
|
+
def matches?(test_stream)
|
|
89
|
+
self.streams.any? do |stream|
|
|
90
|
+
stream.start_with?(test_stream)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# A filter includes another filter if the other filter matches.
|
|
95
|
+
# This does not look at identities.
|
|
96
|
+
def includes?(filter)
|
|
97
|
+
return true if self == filter
|
|
98
|
+
return true if streams == [""]
|
|
99
|
+
|
|
100
|
+
my_stream = Set.new(streams)
|
|
101
|
+
other_stream = Set.new(filter.streams)
|
|
102
|
+
|
|
103
|
+
my_stream.subset?(other_stream)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def to_hash
|
|
107
|
+
{
|
|
108
|
+
application: @application,
|
|
109
|
+
environment: @environment,
|
|
110
|
+
roles: @roles,
|
|
111
|
+
identity: @identity
|
|
112
|
+
}
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
module Pantry
|
|
2
|
+
module Communication
|
|
3
|
+
|
|
4
|
+
# FileService manages the sending and receiving of files that are too big
|
|
5
|
+
# to cleanly send as a plain ZeroMQ message.
|
|
6
|
+
# Every Client and Server has its own FileService handler which can manage
|
|
7
|
+
# both sending and receiving files from each other.
|
|
8
|
+
#
|
|
9
|
+
# Setting up a file transfer processes backwards from what may be expected.
|
|
10
|
+
# As the Receiver actually requests chunks from the Sender, a protocol that's
|
|
11
|
+
# heavily influenced by http://zguide.zeromq.org/page:all#Transferring-Files,
|
|
12
|
+
# a Receiver must be initiated first on the receiving end, which will then pass
|
|
13
|
+
# back the appropriate information (receiver_uuid and file upload UUID) a
|
|
14
|
+
# Sender needs to start up and run.
|
|
15
|
+
#
|
|
16
|
+
# From this the two parts complete the process automatically. A Receiver writes the
|
|
17
|
+
# data it receives in a tempfile, and must be configured with a completion block
|
|
18
|
+
# to move the uploaded file to its final location.
|
|
19
|
+
#
|
|
20
|
+
# To ensure this object has as little special-casing code as possible, the communication
|
|
21
|
+
# takes place in a ZeroMQ ROUTER <-> ROUTER topology.
|
|
22
|
+
class FileService
|
|
23
|
+
include Celluloid::ZMQ
|
|
24
|
+
finalizer :shutdown
|
|
25
|
+
|
|
26
|
+
attr_reader :identity
|
|
27
|
+
|
|
28
|
+
def initialize(server_host, port, security)
|
|
29
|
+
@host = server_host
|
|
30
|
+
@port = port
|
|
31
|
+
|
|
32
|
+
@socket = Celluloid::ZMQ::RouterSocket.new
|
|
33
|
+
@socket.set(::ZMQ::ROUTER_MANDATORY, 1)
|
|
34
|
+
@socket.identity = @identity = SecureRandom.uuid
|
|
35
|
+
Communication.configure_socket(@socket)
|
|
36
|
+
|
|
37
|
+
@security = security
|
|
38
|
+
@security.configure_socket(@socket)
|
|
39
|
+
|
|
40
|
+
@receiver = FileService::ReceiveFile.new_link(self)
|
|
41
|
+
@sender = FileService::SendFile.new_link(self)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def secure_with(security_handler)
|
|
45
|
+
@security = security_handler
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def start_server
|
|
49
|
+
@socket.bind("tcp://#{@host}:#{@port}")
|
|
50
|
+
run
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def start_client
|
|
54
|
+
@socket.connect("tcp://#{@host}:#{@port}")
|
|
55
|
+
run
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def shutdown
|
|
59
|
+
@running = false
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def run
|
|
63
|
+
@running = true
|
|
64
|
+
self.async.process_messages
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Inform the service that it will soon be receiving a file of the given
|
|
68
|
+
# size and checksum. Returns a UploadInfo struct with the information for
|
|
69
|
+
# the Sender.
|
|
70
|
+
def receive_file(size, checksum)
|
|
71
|
+
Pantry.logger.debug("[FileService] Receiving file of size #{size} and checksum #{checksum}")
|
|
72
|
+
@receiver.receive_file(size, checksum).tap do |info|
|
|
73
|
+
info.receiver_uuid = @socket.identity
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Inform the service that we want to start sending a file up to the receiver
|
|
78
|
+
# who's listening on the given UUID.
|
|
79
|
+
def send_file(file_path, receiver_uuid, file_uuid)
|
|
80
|
+
Pantry.logger.debug("[FileService] Sending file #{file_path} to #{receiver_uuid}")
|
|
81
|
+
@sender.send_file(file_path, receiver_uuid, file_uuid)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def send_message(identity, message)
|
|
85
|
+
@socket.write(
|
|
86
|
+
[
|
|
87
|
+
identity,
|
|
88
|
+
SerializeMessage.to_zeromq(message)
|
|
89
|
+
].flatten
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def receive_message(from_identity, message)
|
|
94
|
+
@sender.async.receive_message(from_identity, message)
|
|
95
|
+
@receiver.async.receive_message(from_identity, message)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
protected
|
|
99
|
+
|
|
100
|
+
def process_messages
|
|
101
|
+
while @running
|
|
102
|
+
process_next_message
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
@socket.close
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def process_next_message
|
|
109
|
+
next_message = []
|
|
110
|
+
|
|
111
|
+
from_identity = @socket.read
|
|
112
|
+
|
|
113
|
+
while @socket.more_parts?
|
|
114
|
+
next_message << @socket.read
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
async.receive_message(
|
|
118
|
+
from_identity, SerializeMessage.from_zeromq(next_message)
|
|
119
|
+
)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
end
|
|
125
|
+
end
|