fix *List id and description in discovery document
[arvados.git] / services / keep / keep.rb
1 #!/usr/bin/env ruby
2
3 require 'sinatra/base'
4 require 'digest/md5'
5 require 'digest/sha1'
6
7 class Keep < Sinatra::Base
8   configure do
9     mime_type :binary, 'application/octet-stream'
10     enable :logging
11     set :port, (ENV['PORT'] || '25107').to_i
12     set :bind, (ENV['IP'] || '0.0.0.0')
13   end
14
15   def verify_hash(data, hash)
16     if hash.length == 32
17       Digest::MD5.hexdigest(data) == hash && hash
18     elsif hash.length == 40
19       Digest::SHA1.hexdigest(data) == hash && hash
20     else
21       false
22     end
23   end
24
25   def self.debuglevel
26     if ENV['DEBUG'] and ENV['DEBUG'].match /^-?\d+/
27       ENV['DEBUG'].to_i
28     else
29       0
30     end
31   end
32
33   def self.debuglog(loglevel, msg)
34     if debuglevel >= loglevel
35       $stderr.puts "[keepd/#{$$} #{Time.now}] #{msg}"
36     end
37   end
38   def debuglog(*args)
39     self.class.debuglog *args
40   end
41
42   def keepdirs
43     @@keepdirs
44   end
45
46   def self.keepdirs
47     return @@keepdirs if defined? @@keepdirs
48     # Configure backing store directories
49     keepdirs = []
50     rootdir = (ENV['KEEP_ROOT'] || '/').sub /\/$/, ''
51     `mount`.split("\n").each do |mountline|
52       dev, on_txt, mountpoint, type_txt, fstype, opts = mountline.split
53       if on_txt == 'on' and type_txt == 'type'
54         debuglog 2, "dir #{mountpoint} is mounted"
55         if mountpoint[0..(rootdir.length)] == rootdir + '/'
56           debuglog 2, "dir #{mountpoint} is in #{rootdir}/"
57           keepdir = "#{mountpoint.sub /\/$/, ''}/keep"
58           if File.exists? "#{keepdir}/."
59             keepdirs << { :root => "#{keepdir}", :readonly => false }
60             if opts.gsub(/[\(\)]/, '').split(',').index('ro')
61               keepdirs[-1][:readonly] = true
62             end
63             debuglog 0, "keepdir #{keepdirs[-1].inspect}"
64           end
65         end
66       end
67     end
68     @@keepdirs = keepdirs
69   end
70   self.keepdirs
71
72   def find_backfile(hash, opts)
73     subdir = hash[0..2]
74     keepdirs.each do |keepdir|
75       backfile = "#{keepdir[:root]}/#{subdir}/#{hash}"
76       if File.exists? backfile
77         data = nil
78         File.open("#{keepdir[:root]}/lock", "a+") do |f|
79           if f.flock File::LOCK_EX
80             data = File.read backfile
81           end
82         end
83         if data and (!opts[:verify_hash] or verify_hash data, hash)
84           return [backfile, data]
85         end
86       end
87     end
88     nil
89   end
90
91   get '/:locator' do |locator|
92     regs = locator.match /^([0-9a-f]{32,})/
93     if regs
94       hash = regs[1]
95       backfile, data = find_backfile hash, :verify_hash => false
96       if data
97         content_type :binary
98         body data
99       else
100         status 404
101         body 'not found'
102       end
103     else
104       pass
105     end
106   end
107
108   put '/:locator' do |locator|
109     data = request.body.read
110     hash = verify_hash(data, locator)
111     if not hash
112       status 422
113       body "Checksum mismatch"
114       return
115     end
116     backfile, havedata = find_backfile hash, :verify_hash => true
117     if havedata
118       status 200
119       body 'OK'
120     else
121       wrote = nil
122       subdir = hash[0..2]
123       keepdirs.each do |keepdir|
124         next if keepdir[:readonly]
125         backdir = "#{keepdir[:root]}/#{subdir}"
126         if !File.exists? backdir
127           begin
128             Dir.mkdir backdir
129           rescue
130           end
131         end
132         backfile = "#{keepdir[:root]}/#{subdir}/#{hash}"
133         File.open("#{keepdir[:root]}/lock", "a+") do |lf|
134           if lf.flock File::LOCK_EX
135             File.open(backfile + ".tmp", "a+") do |wf|
136               if wf.flock File::LOCK_EX
137                 wf.seek 0, File::SEEK_SET
138                 wf.truncate 0
139                 wrote = wf.write data
140               end
141               if wrote == data.length
142                 File.rename backfile+".tmp", backfile
143                 break
144               else
145                 File.unlink backfile+".tmp"
146               end
147             end
148           end
149         end
150       end
151       if wrote == data.length
152         status 200
153         body 'OK'
154       else
155         status 500
156         body 'Fail'
157       end
158     end
159   end
160
161   if app_file == $0
162     run! do |server|
163       if ENV['SSL_CERT'] and ENV['SSL_KEY']
164         ssl_options = {
165           :cert_chain_file => ENV['SSL_CERT'],
166           :private_key_file => ENV['SSL_KEY'],
167           :verify_peer => false
168         }
169         server.ssl = true
170         server.ssl_options = ssl_options
171       end
172     end
173   end
174 end