fff4eaa2ba4ecbc22346a31e60b1be53b0441056
[webvac] / lib / webvac.rb
1 %w(
2         redic
3         magic
4         json
5         time
6         cgi
7 ).each &method(:require)
8
9 # The namespace for WebVac.  See the README.
10 module WebVac
11         # Config object, intended to be used as a singleton.
12         class Config
13                 # The default config options.  See the README.
14                 Defaults = {
15                         redis_url: "redis://localhost:6379/0",
16
17                         server_path_strip: "/media",
18                         server_path_prepend: "/media/block/fse",
19
20                         venti_server: 'localhost',
21
22                         plan9bin: '/opt/plan9/bin',
23
24                         mime_substitutions: {
25                                 'text/html' => 'text/plain',
26                         },
27                 }
28                 attr_accessor *Defaults.keys
29
30                 # The sorted list of places where we will look for config files
31                 # to load.
32                 ConfigPaths = [
33                         ENV['WEBVAC_CONFIG'],
34                         "./config/webvac.json",
35                         "#{ENV['HOME']}/.webvac.json",
36                         "/etc/webvac.json",
37                 ].compact
38
39                 # Reads/parses config and instantiates an object
40                 def self.load
41                         f = ConfigPaths.find { |f| File.readable?(f) }
42                         cfg = if f
43                                 JSON.parse File.read(f)
44                         else
45                                 {}
46                         end
47                         new cfg
48                 end
49
50                 # Takes a config, replaces the defaults with it.
51                 # Will throw exceptions if you give it a bad config, you should probably
52                 # just call Config.load.
53                 def initialize cfg
54                         Defaults.each { |k,v|
55                                 send("#{k}=", v)
56                         }
57                         cfg.each { |k,v|
58                                 send("#{k}=", v)
59                         }
60                 end
61
62                 def path_fixup path
63                         @_path_rx ||= /^#{Regexp.escape(server_path_strip)}/
64                         path.sub(@_path_rx, server_path_prepend)
65                 end
66         end
67
68         # Stateless-ish client for venti.
69         # I completely punted on implementing a venti client, so it just calls
70         # the vac/unvac binaries.  Does the job!
71         class Vac
72                 attr_reader :config
73
74                 # Takes an instance of Config.
75                 def initialize cfg
76                         @config = cfg
77                 end
78
79                 def save! fn
80                         contents = File.read(fn)
81                         pi, po = IO.pipe
82                         io = IO.popen(
83                                 {'venti' => config.venti_server},
84                                 ["#{config.plan9bin}/vac", '-i', File.basename(fn)],
85                                 in: pi
86                         ).tap { |io| Thread.new { Process.wait(io.pid) } }
87                         po.write contents
88                         po.close
89                         io.read.chomp.sub(/^vac:/, '')
90                 end
91
92                 def load! vac
93                         unless /^vac:[a-f0-9]{40}$/.match(vac)
94                                 raise ArgumentError, "#{vac.inspect} not a vac score?"
95                         end
96                         IO.popen(
97                                 {'venti' => config.venti_server},
98                                 ["#{config.plan9bin}/unvac", '-c', vac]
99                         ).tap { |io| Thread.new { Process.wait(io.pid) } }.read
100                 end
101         end
102
103         # Sits in front of Redis (just Redis right now), and handles the mapping
104         # of vac hashes to pathnames, as well as the metadata (in JSON and in the
105         # form of HTTP headers, which allows HEAD requests to be cheap).  Also does
106         # some of the bookkeeping necessary for that, like the interaction with
107         # libmagic.
108         #
109         # Relatively threadsafe, but maintains one Redis connection per active
110         # thread (created on demand).
111         class Table
112                 attr_reader :config
113
114                 # Takes an instance of Config.
115                 def initialize cfg
116                         @config = cfg
117                 end
118
119                 # Takes a filename, returns the filename's metadata.  Stateless-ish.
120                 def fn2md f
121                         s = File.stat(f)
122                         m = {
123                                 'Content-Type' => Magic.guess_file_mime_type(f),
124                                 'Content-Length' => s.size.to_s,
125                                 'Last-Modified' => s.mtime.rfc822,
126                         } rescue nil
127                 end
128
129                 def meta_save! fn, sc
130                         md = fn2md(fn)
131                         return unless md
132                         redis.call 'HSET', 'score2md', sc, md.to_json
133                 end
134
135                 def metadata score
136                         # Overall, doesn't really matter if this fails.
137                         JSON.parse(
138                                 redis.call('HGET', 'score2md', score.sub(/^vac:/, ''))
139                         ) rescue nil
140                 end
141
142                 def rec_score! fn, sc
143                         redis.call 'HSET', 'path2score', fn, sc
144                 end
145
146                 def redis
147                         Thread.current[:webvac_redis] ||= Redic.new(config.redis_url)
148                 end
149
150                 def path2score p
151                         r = redis.call 'HGET', 'path2score', p
152                         return "vac:#{r}" if r
153                 end
154
155                 def guess_mime contents
156                         Magic.guess_string_mime_type(contents)
157                 end
158         end
159 end