mog: ensure the tempfile is killed at death
[ruby-mogilefs-client.git] / bin / mog
blobf17427ce7be6539e15803d3d4f94f6d7ba9ff683
1 #!/usr/bin/env ruby
2 require 'mogilefs'
3 require 'optparse'
5 trap('INT') { exit 130 }
6 trap('PIPE') { exit 0 }
8 # this is to be compatible with config files used by the Perl tools
9 def parse_config_file!(path, overwrite = false)
10 dest = {}
11 File.open(path).each_line do |line|
12 line.strip!
13 if /^(domain|class)\s*=\s*(\S+)/.match(line)
14 dest[$1.to_sym] = $2
15 elsif m = /^(?:trackers|hosts)\s*=\s*(.*)/.match(line)
16 dest[:hosts] = $1.split(/\s*,\s*/)
17 else
18 STDERR.puts "Ignored configuration line: #{line}" unless /^#/.match(line)
19 end
20 end
21 dest
22 end
24 # parse the default config file if one exists
25 def_file = File.expand_path("~/.mogilefs-client.conf")
26 def_cfg = File.exist?(def_file) ? parse_config_file!(def_file) : {}
28 # parse the command-line first, these options take precedence over all else
29 cli_cfg = {}
30 config_file = nil
31 ls_l = false
32 ls_h = false
33 ARGV.options do |x|
34 x.banner = "Usage: #{$0} [options] <command> [<arguments>]"
35 x.separator ''
37 x.on('-c', '--config=/path/to/config',
38 'config file to load') { |file| config_file = file }
40 x.on('-t', '--trackers=host1[,host2]', '--hosts=host1[,host2]', Array,
41 'hostnames/IP addresses of trackers') do |trackers|
42 cli_cfg[:hosts] = trackers
43 end
45 x.on('-C', '--class=s', 'class') { |klass| cli_cfg[:class] = klass }
46 x.on('-d', '--domain=s', 'domain') { |domain| cli_cfg[:domain] = domain }
47 x.on('-l', "long listing format (`ls' command)") { ls_l = true }
48 x.on('-h', '--human-readable',
49 "print sizes in human-readable format (`ls' command)") { ls_h = true }
51 x.separator ''
52 x.on('--help', 'Show this help message.') { puts x; exit }
53 x.parse!
54 end
56 # parse the config file specified at the command-line
57 file_cfg = config_file ? parse_config_file!(config_file, true) : {}
59 # read environment variables, too. This Ruby API favors the term
60 # "hosts", however upstream MogileFS teminology favors "trackers" instead.
61 # Favor the term more consistent with what the MogileFS inventors used.
62 env_cfg = {}
63 if ENV["MOG_TRACKERS"]
64 env_cfg[:hosts] = ENV["MOG_TRACKERS"].split(/\s*,\s*/)
65 end
66 if ENV["MOG_HOSTS"] && env_cfg[:hosts].empty?
67 env_cfg[:hosts] = ENV["MOG_HOSTS"].split(/\s*,\s*/)
68 end
69 env_cfg[:domain] = ENV["MOG_DOMAIN"] if ENV["MOG_DOMAIN"]
70 env_cfg[:class] = ENV["MOG_CLASS"] if ENV["MOG_CLASS"]
72 # merge the configs, favoring them in order specified:
73 cfg = {}.merge(def_cfg).merge(env_cfg).merge(file_cfg).merge(cli_cfg)
75 # error-checking
76 err = []
77 err << "trackers must be specified" if cfg[:hosts].nil? || cfg[:hosts].empty?
78 err << "domain must be specified" unless cfg[:domain]
79 if err.any?
80 STDERR.puts "Errors:\n #{err.join("\n ")}"
81 STDERR.puts ARGV.options
82 exit 1
83 end
85 unless cmd = ARGV.shift
86 STDERR.puts ARGV.options
87 exit 1
88 end
90 include MogileFS::Util
91 mg = MogileFS::MogileFS.new(cfg)
93 begin
94 case cmd
95 when 'cp'
96 filename = ARGV.shift or raise ArgumentError, '<filename> <key>'
97 key = ARGV.shift or raise ArgumentError, '<filename> <key>'
98 ARGV.shift and raise ArgumentError, '<filename> <key>'
99 cfg[:class] or raise ArgumentError, 'E: --class must be specified'
100 mg.store_file(key, cfg[:class], filename)
101 when 'cat'
102 ARGV.empty? and raise ArgumentError, '<key1> [<key2> ...]'
103 ARGV.each { |key| mg.get_file_data(key) { |fp| sysrwloop(fp, STDOUT) } }
104 when 'ls'
105 prefixes = ARGV.empty? ? [ nil ] : ARGV
106 prefixes.each do |prefix|
107 mg.each_key(prefix) do |key|
108 if ls_l
109 path_nr = "% 2d" % mg.get_paths(key).size
110 size = mg.size(key)
111 if ls_h && size > 1024
112 suff = ''
113 %w(K M G).each do |s|
114 size /= 1024.0
115 suff = s
116 break if size <= 1024
118 size = sprintf("%.1f%s", size, suff)
119 else
120 size = size.to_s
122 size = (' ' * (12 - size.length)) << size # right justify
123 puts [ path_nr, size, key ].pack("A4 A16 A32")
124 else
125 puts key
129 when 'rm'
130 ARGV.empty? and raise ArgumentError, '<key1> [<key2>]'
131 ARGV.each { |key| mg.delete(key) }
132 when 'mv'
133 from = ARGV.shift or raise ArgumentError, '<from> <to>'
134 to = ARGV.shift or raise ArgumentError, '<from> <to>'
135 ARGV.shift and raise ArgumentError, '<from> <to>'
136 mg.rename(from, to)
137 when 'stat' # this outputs a RFC822-like format
138 ARGV.empty? and raise ArgumentError, '<key1> [<key2>]'
139 ARGV.each_with_index do |key, i|
140 if size = mg.size(key)
141 puts "Key: #{key}"
142 puts "Size: #{size}"
143 mg.get_paths(key).each_with_index do |path,i|
144 puts "URL-#{i}: #{path}"
146 puts ""
147 else
148 STDERR.puts "No such key: #{key}"
151 when 'tee'
152 require 'tempfile'
153 key = ARGV.shift or raise ArgumentError, '<key>'
154 ARGV.shift and raise ArgumentError, '<key>'
155 cfg[:class] or raise ArgumentError, 'E: --class must be specified'
156 buf = ''
157 tmp = Tempfile.new('mog-tee') # TODO: explore Transfer-Encoding:chunked :)
158 at_exit { tmp.unlink }
159 begin
160 sysrwloop(STDIN, tmp)
161 mg.store_file(key, cfg[:class], tmp.path)
162 ensure
163 tmp.close
165 else
166 raise ArgumentError, "Unknown command: #{cmd}"
168 rescue ArgumentError => err
169 STDERR.puts "Usage: #{$0} #{cmd} #{err.message}"
170 exit 1
172 exit 0