pantry 0.0.0 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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,53 @@
|
|
1
|
+
module Pantry
|
2
|
+
|
3
|
+
# Manages and processes commands as requested from the Client or the Server.
|
4
|
+
# Given a mapping of available commands, maps the incoming message to the appropriate
|
5
|
+
# command handler and returns the response. Returns nil if no command found.
|
6
|
+
class CommandHandler
|
7
|
+
|
8
|
+
def initialize(server_or_client, commands_to_register = [])
|
9
|
+
@handlers = {}
|
10
|
+
@server_or_client = server_or_client
|
11
|
+
|
12
|
+
commands_to_register.each do |command_class|
|
13
|
+
add_command(command_class)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Install a Command class as a message handler for this process.
|
18
|
+
# The Message's +type+ for this kind of message is simply the name of the class
|
19
|
+
# without any scope information. E.g. Echo not Pantry::Command::Echo.
|
20
|
+
def add_command(command_class)
|
21
|
+
@handlers[command_class.message_type] = build_command_proc(command_class)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Does this CommandHandler know how to handle the given command?
|
25
|
+
def can_handle?(message)
|
26
|
+
!@handlers[message.type].nil?
|
27
|
+
end
|
28
|
+
|
29
|
+
# Given a message, figure out which handler should be triggered and get things rolling
|
30
|
+
def process(message)
|
31
|
+
if handler = @handlers[message.type]
|
32
|
+
Pantry.logger.debug("[#{@server_or_client.identity}] Running handler on #{message.inspect}")
|
33
|
+
handler.call(message)
|
34
|
+
else
|
35
|
+
Pantry.logger.warn(
|
36
|
+
"[#{@server_or_client.identity}] No known handler for message type #{message.type}"
|
37
|
+
)
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
protected
|
43
|
+
|
44
|
+
def build_command_proc(command_class)
|
45
|
+
proc do |message|
|
46
|
+
command_obj = command_class.new
|
47
|
+
command_obj.server_or_client = @server_or_client
|
48
|
+
command_obj.perform(message)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
module Pantry
|
2
|
+
|
3
|
+
class CommandLine
|
4
|
+
|
5
|
+
# The top-level set of CLI options and flags Pantry respects
|
6
|
+
BASE_OPTIONS = proc {
|
7
|
+
banner "Usage: #{File.basename($0)} [options] [command [command options]]"
|
8
|
+
option "-h", "--host HOSTNAME", String, "Hostname of the Server to connect to"
|
9
|
+
option "--curve-key-file FILE", String, "Name of the file in .pantry holding Curve keys.",
|
10
|
+
"Specifying this option will turn on Curve encryption."
|
11
|
+
|
12
|
+
option "-a", "--application APPLICATION", String, "Filter Clients by a specific APPLICATION"
|
13
|
+
option "-e", "--environment ENVIRONMENT", String, "Filter Clients by a specific ENVIRONMENT"
|
14
|
+
option "-r", "--roles ROLE1,ROLE2", Array, "Filter Clients by given ROLES"
|
15
|
+
option "-v", "--verbose", "Verbose output (INFO)"
|
16
|
+
option "-d", "--debug", "Even more Verbose output (DEBUG)"
|
17
|
+
option "-V", "--version", "Print out Pantry's version"
|
18
|
+
}
|
19
|
+
|
20
|
+
def initialize(command_line)
|
21
|
+
@command_line = command_line
|
22
|
+
@known_commands = {}
|
23
|
+
|
24
|
+
@commands = find_all_cli_enabled_commands
|
25
|
+
end
|
26
|
+
|
27
|
+
# Parse the full command line. Returns a hash containing the options found
|
28
|
+
# as well as what is still left on the command line.
|
29
|
+
# If the command line is empty, will default to --help.
|
30
|
+
#
|
31
|
+
# Returns [nil, nil] if help was requested or there was a problem.
|
32
|
+
def parse!
|
33
|
+
@command_line = merge_command_line_with_defaults(@command_line)
|
34
|
+
parser = build_parser(@commands)
|
35
|
+
|
36
|
+
begin
|
37
|
+
if @command_line.empty?
|
38
|
+
@command_line << "--help"
|
39
|
+
end
|
40
|
+
|
41
|
+
@parsed_options = parser.parse!(@command_line)
|
42
|
+
|
43
|
+
if @parsed_options['help']
|
44
|
+
# Help printed already
|
45
|
+
return [nil, nil]
|
46
|
+
end
|
47
|
+
|
48
|
+
[@parsed_options, @command_line]
|
49
|
+
rescue => ex
|
50
|
+
puts ex, ""
|
51
|
+
puts parser.help
|
52
|
+
[nil, nil]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns details of the command found during parsing.
|
57
|
+
# Returns a hash with the keys +banner+ and +class+,
|
58
|
+
# or returns nil if no matching command was found
|
59
|
+
def triggered_command
|
60
|
+
[
|
61
|
+
@commands[@parsed_options.command_found],
|
62
|
+
@parsed_options[@parsed_options.command_found]
|
63
|
+
]
|
64
|
+
end
|
65
|
+
|
66
|
+
protected
|
67
|
+
|
68
|
+
def find_all_cli_enabled_commands
|
69
|
+
commands = {}
|
70
|
+
Pantry.all_commands.each do |command_class|
|
71
|
+
if command_class.command_name
|
72
|
+
# Hmm duplicated from OptParsePlus
|
73
|
+
base_command_name = command_class.command_name.split(/\s/).first
|
74
|
+
commands[base_command_name] = {
|
75
|
+
banner: command_class.command_name,
|
76
|
+
class: command_class
|
77
|
+
}
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
commands
|
82
|
+
end
|
83
|
+
|
84
|
+
def merge_command_line_with_defaults(command_line)
|
85
|
+
[read_defaults_file, command_line].flatten
|
86
|
+
end
|
87
|
+
|
88
|
+
def read_defaults_file
|
89
|
+
dot_pantry_config = Pantry.root.join("config")
|
90
|
+
|
91
|
+
if File.exist?(dot_pantry_config)
|
92
|
+
# ARGV is an array of the command line seperated by white-space.
|
93
|
+
# Make sure what we read from .pantry returns the same
|
94
|
+
File.readlines(dot_pantry_config).map { |line|
|
95
|
+
line.strip.split(/\s/)
|
96
|
+
}.flatten
|
97
|
+
else
|
98
|
+
[]
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def build_parser(cli_commands)
|
103
|
+
parser = OptParsePlus.new
|
104
|
+
parser.add_options(&BASE_OPTIONS)
|
105
|
+
|
106
|
+
cli_commands.each do |base_command_name, command_info|
|
107
|
+
parser.add_command(command_info[:banner], &command_info[:class].command_config)
|
108
|
+
end
|
109
|
+
|
110
|
+
parser
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Pantry
|
2
|
+
module Commands
|
3
|
+
|
4
|
+
# Ask the server to generate a new set of keys
|
5
|
+
# Prints a yaml file that contains the required keys for a client to properly
|
6
|
+
# conenct and authenticate to the server
|
7
|
+
class CreateClient < Pantry::Command
|
8
|
+
command "client:create" do
|
9
|
+
description "Generate a new public/private encryption keypair for a client."
|
10
|
+
end
|
11
|
+
|
12
|
+
def perform(message)
|
13
|
+
server.create_client
|
14
|
+
end
|
15
|
+
|
16
|
+
def receive_server_response(message)
|
17
|
+
keys = message.body[0]
|
18
|
+
Pantry.ui.say("New Client Credentials")
|
19
|
+
Pantry.ui.say("Store this in the Client's Pantry.root/security/curve/client_keys.yml")
|
20
|
+
Pantry.ui.say(YAML.dump({
|
21
|
+
"server_public_key" => keys[:server_public_key],
|
22
|
+
"public_key" => keys[:public_key],
|
23
|
+
"private_key" => keys[:private_key]
|
24
|
+
}))
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Pantry
|
2
|
+
module Commands
|
3
|
+
|
4
|
+
# Download all content inside of the given directory.
|
5
|
+
#
|
6
|
+
# This command expects simple directories with a small number of files that are
|
7
|
+
# themselves small in size, as this command reads every file into memory and sends
|
8
|
+
# that raw content back to the Client. If there are more substantial files to transfer
|
9
|
+
# use #send_file and #receive_file instead.
|
10
|
+
class DownloadDirectory < Pantry::Command
|
11
|
+
|
12
|
+
def initialize(directory = nil)
|
13
|
+
@directory = directory
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_message
|
17
|
+
super.tap do |message|
|
18
|
+
message << @directory.to_s
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def perform(message)
|
23
|
+
directory = Pantry.root.join(message.body[0])
|
24
|
+
|
25
|
+
Dir[directory.join("**", "*")].map do |file|
|
26
|
+
next if File.directory?(file)
|
27
|
+
[Pathname.new(file).relative_path_from(directory).to_s,
|
28
|
+
File.read(file)]
|
29
|
+
end.compact
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Pantry
|
2
|
+
module Commands
|
3
|
+
|
4
|
+
# Simple Echo command, returns the body of the Message given.
|
5
|
+
class Echo < Command
|
6
|
+
|
7
|
+
command "echo MESSAGE" do
|
8
|
+
description "Test Client communication with a simple Echo request"
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(string_to_echo = "")
|
12
|
+
@string_to_echo = string_to_echo
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_message
|
16
|
+
message = super
|
17
|
+
message << @string_to_echo
|
18
|
+
message
|
19
|
+
end
|
20
|
+
|
21
|
+
def perform(message)
|
22
|
+
message.body[0]
|
23
|
+
end
|
24
|
+
|
25
|
+
def receive_client_response(response)
|
26
|
+
Pantry.ui.say("#{response.from} echo's #{response.body[0].inspect}")
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Pantry
|
2
|
+
module Commands
|
3
|
+
|
4
|
+
# Edit the configuration of the requested Application.
|
5
|
+
#
|
6
|
+
# Application configuration is stored on the Server under
|
7
|
+
# Pantry.root/applications/[app name]/config.yml and is where all
|
8
|
+
# configuration lives for how Pantry manages this application.
|
9
|
+
class EditApplication < Pantry::Command
|
10
|
+
|
11
|
+
command "edit" do
|
12
|
+
description "Edit an application's configuration with the text editor specified in $EDITOR.
|
13
|
+
Requires an Application."
|
14
|
+
end
|
15
|
+
|
16
|
+
def prepare_message(options)
|
17
|
+
@application = options[:application]
|
18
|
+
raise Pantry::MissingOption, 'Missing required option "application"' unless @application
|
19
|
+
|
20
|
+
# Let the EDITOR check run before we do any communication
|
21
|
+
@editor = Pantry::FileEditor.new
|
22
|
+
|
23
|
+
super.tap do |msg|
|
24
|
+
msg << @application
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Read or create a new config file for the given application
|
29
|
+
# and return the contents of this config file, which will always be
|
30
|
+
# a YAML document
|
31
|
+
def perform(message)
|
32
|
+
application = message.body[0]
|
33
|
+
|
34
|
+
config_file = Pantry.root.join("applications", application, "config.yml")
|
35
|
+
FileUtils.mkdir_p(File.dirname(config_file))
|
36
|
+
|
37
|
+
if File.exists?(config_file)
|
38
|
+
[File.read(config_file)]
|
39
|
+
else
|
40
|
+
[{"name" => application}.to_yaml]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def receive_server_response(message)
|
45
|
+
current_config = message.body[0]
|
46
|
+
new_config = @editor.edit(current_config, :yaml)
|
47
|
+
|
48
|
+
if new_config != current_config
|
49
|
+
send_request!(
|
50
|
+
Pantry::Commands::UpdateApplication.new(
|
51
|
+
@application, new_config
|
52
|
+
).to_message
|
53
|
+
)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Pantry
|
2
|
+
module Commands
|
3
|
+
|
4
|
+
class RegisterClient < Command
|
5
|
+
|
6
|
+
def initialize(client_info = nil)
|
7
|
+
@client_info = client_info
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_message
|
11
|
+
message = super
|
12
|
+
message << {
|
13
|
+
application: @client_info.application,
|
14
|
+
environment: @client_info.environment,
|
15
|
+
roles: @client_info.roles
|
16
|
+
}
|
17
|
+
message
|
18
|
+
end
|
19
|
+
|
20
|
+
# Take note that a Client has connected and registered itself
|
21
|
+
# with this Server.
|
22
|
+
def perform(message)
|
23
|
+
details = message.body[0]
|
24
|
+
|
25
|
+
@client_info = Pantry::ClientInfo.new(
|
26
|
+
identity: message.from,
|
27
|
+
application: details[:application],
|
28
|
+
environment: details[:environment],
|
29
|
+
roles: details[:roles]
|
30
|
+
)
|
31
|
+
|
32
|
+
self.server.register_client(@client_info)
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Pantry
|
2
|
+
module Commands
|
3
|
+
|
4
|
+
class Status < Command
|
5
|
+
|
6
|
+
command "status" do
|
7
|
+
description "List all Clients that match the options"
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_accessor :client_filter
|
11
|
+
|
12
|
+
def prepare_message(options)
|
13
|
+
@client_filter = Pantry::Communication::ClientFilter.new(
|
14
|
+
application: options[:application],
|
15
|
+
environment: options[:environment],
|
16
|
+
roles: options[:roles]
|
17
|
+
)
|
18
|
+
super
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_message
|
22
|
+
message = super
|
23
|
+
message << @client_filter.to_hash
|
24
|
+
message
|
25
|
+
end
|
26
|
+
|
27
|
+
# Return information about all connected Clients that match the given filter
|
28
|
+
def perform(message)
|
29
|
+
@client_filter = Pantry::Communication::ClientFilter.new(**(message.body[0] || {}))
|
30
|
+
self.server.client_registry.all_matching(@client_filter) do |client, record|
|
31
|
+
{
|
32
|
+
identity: client.identity,
|
33
|
+
application: client.application,
|
34
|
+
environment: client.environment,
|
35
|
+
roles: client.roles,
|
36
|
+
last_checked_in: record.last_checked_in_at
|
37
|
+
}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def receive_server_response(message)
|
42
|
+
output =
|
43
|
+
clients = message.body.map do |client|
|
44
|
+
[
|
45
|
+
time_ago_in_words(client[:last_checked_in]),
|
46
|
+
client[:identity],
|
47
|
+
"|",
|
48
|
+
client[:application],
|
49
|
+
client[:environment],
|
50
|
+
[client[:roles]].flatten.join(",")
|
51
|
+
].compact.join(" ")
|
52
|
+
end
|
53
|
+
|
54
|
+
Pantry.ui.list(output)
|
55
|
+
end
|
56
|
+
|
57
|
+
protected
|
58
|
+
|
59
|
+
def time_ago_in_words(time)
|
60
|
+
now = DateTime.now.to_time
|
61
|
+
checked_in = DateTime.parse(time).to_time
|
62
|
+
|
63
|
+
seconds_since = (now - checked_in).to_i
|
64
|
+
case seconds_since
|
65
|
+
when 0..(2*60)
|
66
|
+
Pantry.ui.color("A minute ago", :green)
|
67
|
+
when (2*60+1)..(59*60)
|
68
|
+
Pantry.ui.color("#{seconds_since / 60} minutes ago", :green)
|
69
|
+
else
|
70
|
+
hours_since = seconds_since / 60 / 60
|
71
|
+
hours_key = hours_since > 1 ? "hours" : "hour"
|
72
|
+
Pantry.ui.color("#{hours_since} #{hours_key} ago", :red)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|