jchris / couchrest

A RESTful CouchDB client based on Heroku's RestClient and Couch.js - you want the version at http://github.com/couchrest/couchrest

This URL has Read+Write access

couchrest / lib / couchrest / core / database.rb
420168be » jchris 2008-03-19 escape get and put ... stil... 1 require 'cgi'
bf1acd83 » jchris 2008-06-07 document attachments now su... 2 require "base64"
420168be » jchris 2008-03-19 escape get and put ... stil... 3
f5fdc8b9 » jchris 2008-09-11 made CouchRest a module 4 module CouchRest
32c01fad » jchris 2008-03-18 working specy stuff i hope 5 class Database
84e2bf94 » Matt Aimonetti 2009-01-28 slight change of API, CR::D... 6 attr_reader :server, :host, :name, :root, :uri
d8d5645e » thewordnerd 2008-12-14 Make bulk saving more flexi... 7 attr_accessor :bulk_save_cache_limit
9e7738fd » jchris 2008-09-07 added streamer class 8
254eb201 » jchris 2008-10-14 view blocks flow 9 # Create a CouchRest::Database adapter for the supplied CouchRest::Server
10 # and database name.
11 #
b3b58ffa » jchris 2008-09-29 polished documentation 12 # ==== Parameters
13 # server<CouchRest::Server>:: database host
14 # name<String>:: database name
15 #
84e2bf94 » Matt Aimonetti 2009-01-28 slight change of API, CR::D... 16 def initialize(server, name)
32c01fad » jchris 2008-03-18 working specy stuff i hope 17 @name = name
54f26017 » jchris 2008-09-07 CouchRest no longer uses PO... 18 @server = server
19 @host = server.uri
c18567f8 » Matt Aimonetti 2009-06-07 differentiated attachment's... 20 @uri = "/#{name.gsub('/','%2F')}"
21 @root = host + uri
54a0afdf » jchris 2008-10-13 added block yields to db.view 22 @streamer = Streamer.new(self)
d8d5645e » thewordnerd 2008-12-14 Make bulk saving more flexi... 23 @bulk_save_cache = []
813f673d » Matt Aimonetti 2009-03-05 increased the bulk cache li... 24 @bulk_save_cache_limit = 500 # must be smaller than the uuid count
32c01fad » jchris 2008-03-18 working specy stuff i hope 25 end
26
b3b58ffa » jchris 2008-09-29 polished documentation 27 # returns the database's uri
88f83f07 » jchris 2008-08-03 moved scripts to bin 28 def to_s
c18567f8 » Matt Aimonetti 2009-06-07 differentiated attachment's... 29 @root
88f83f07 » jchris 2008-08-03 moved scripts to bin 30 end
31
26c4db7f » jchris 2008-09-29 documenting CouchRest::Model 32 # GET the database info from CouchDB
5d56f196 » jchris 2008-09-07 added easy method for ensur... 33 def info
c18567f8 » Matt Aimonetti 2009-06-07 differentiated attachment's... 34 CouchRest.get @root
5d56f196 » jchris 2008-09-07 added easy method for ensur... 35 end
36
26c4db7f » jchris 2008-09-29 documenting CouchRest::Model 37 # Query the <tt>_all_docs</tt> view. Accepts all the same arguments as view.
84e2bf94 » Matt Aimonetti 2009-01-28 slight change of API, CR::D... 38 def documents(params = {})
e2f71638 » jchris 2008-10-08 multi-key support for views... 39 keys = params.delete(:keys)
c18567f8 » Matt Aimonetti 2009-06-07 differentiated attachment's... 40 url = CouchRest.paramify_url "#{@root}/_all_docs", params
e2f71638 » jchris 2008-10-08 multi-key support for views... 41 if keys
42 CouchRest.post(url, {:keys => keys})
43 else
44 CouchRest.get url
45 end
097ab935 » jchris 2008-03-19 getting into view land 46 end
47
8363aa62 » jchris 2009-05-08 added bulk_load macro 48 # load a set of documents by passing an array of ids
49 def get_bulk(ids)
50 documents(:keys => ids, :include_docs => true)
51 end
92b77a96 » Matt Aimonetti 2009-05-13 fixed bulk_load/get_bulk an... 52 alias :bulk_load :get_bulk
8363aa62 » jchris 2009-05-08 added bulk_load macro 53
254eb201 » jchris 2008-10-14 view blocks flow 54 # POST a temporary view function to CouchDB for querying. This is not
55 # recommended, as you don't get any performance benefit from CouchDB's
56 # materialized views. Can be quite slow on large databases.
84e2bf94 » Matt Aimonetti 2009-01-28 slight change of API, CR::D... 57 def slow_view(funcs, params = {})
e2f71638 » jchris 2008-10-08 multi-key support for views... 58 keys = params.delete(:keys)
59 funcs = funcs.merge({:keys => keys}) if keys
c18567f8 » Matt Aimonetti 2009-06-07 differentiated attachment's... 60 url = CouchRest.paramify_url "#{@root}/_temp_view", params
b2a29d9e » Matt Aimonetti 2009-07-14 started extracting the http... 61 JSON.parse(HttpAbstraction.post(url, funcs.to_json, {"Content-Type" => 'application/json'}))
6995d478 » jchris 2008-03-19 temp view 62 end
e15581b1 » mattly 2009-01-04 fix temp_view -> slow_view ... 63
64 # backwards compatibility is a plus
65 alias :temp_view :slow_view
6995d478 » jchris 2008-03-19 temp view 66
254eb201 » jchris 2008-10-14 view blocks flow 67 # Query a CouchDB view as defined by a <tt>_design</tt> document. Accepts
68 # paramaters as described in http://wiki.apache.org/couchdb/HttpViewApi
84e2bf94 » Matt Aimonetti 2009-01-28 slight change of API, CR::D... 69 def view(name, params = {}, &block)
e2f71638 » jchris 2008-10-08 multi-key support for views... 70 keys = params.delete(:keys)
7b03c7ba » jchris 2009-03-09 fix for design doc url changes Comment 71 name = name.split('/') # I think this will always be length == 2, but maybe not...
72 dname = name.shift
73 vname = name.join('/')
c18567f8 » Matt Aimonetti 2009-06-07 differentiated attachment's... 74 url = CouchRest.paramify_url "#{@root}/_design/#{dname}/_view/#{vname}", params
e2f71638 » jchris 2008-10-08 multi-key support for views... 75 if keys
76 CouchRest.post(url, {:keys => keys})
77 else
54a0afdf » jchris 2008-10-13 added block yields to db.view 78 if block_given?
7b03c7ba » jchris 2009-03-09 fix for design doc url changes Comment 79 @streamer.view("_design/#{dname}/_view/#{vname}", params, &block)
54a0afdf » jchris 2008-10-13 added block yields to db.view 80 else
81 CouchRest.get url
82 end
e2f71638 » jchris 2008-10-08 multi-key support for views... 83 end
097ab935 » jchris 2008-03-19 getting into view land 84 end
915905ca » jchris 2008-07-04 upgraded couchrest for 080 85
26c4db7f » jchris 2008-09-29 documenting CouchRest::Model 86 # GET a document from CouchDB, by id. Returns a Ruby Hash.
8964a9b2 » jchris 2009-03-14 created upgrade helper 87 def get(id, params = {})
bca68cf1 » jchris 2009-01-12 design doc ids fixed throug... 88 slug = escape_docid(id)
c18567f8 » Matt Aimonetti 2009-06-07 differentiated attachment's... 89 url = CouchRest.paramify_url("#{@root}/#{slug}", params)
fbc21aac » jchris 2009-03-14 updater is simpler now that... 90 result = CouchRest.get(url)
91 return result unless result.is_a?(Hash)
92 doc = if /^_design/ =~ result["_id"]
93 Design.new(result)
0769c269 » jchris 2008-11-08 on the road toward design docs 94 else
fbc21aac » jchris 2009-03-14 updater is simpler now that... 95 Document.new(result)
0769c269 » jchris 2008-11-08 on the road toward design docs 96 end
97 doc.database = self
98 doc
99f25bbb » jchris 2008-03-19 added document retreival (GET) 99 end
097ab935 » jchris 2008-03-19 getting into view land 100
26c4db7f » jchris 2008-09-29 documenting CouchRest::Model 101 # GET an attachment directly from CouchDB
60c57796 » jchris 2009-02-02 all specs pass; refined att... 102 def fetch_attachment(doc, name)
c18567f8 » Matt Aimonetti 2009-06-07 differentiated attachment's... 103 uri = url_for_attachment(doc, name)
b2a29d9e » Matt Aimonetti 2009-07-14 started extracting the http... 104 HttpAbstraction.get uri
bf1acd83 » jchris 2008-06-07 document attachments now su... 105 end
106
2b7e49c9 » jchris 2008-09-30 put attachments 107 # PUT an attachment directly to CouchDB
84e2bf94 » Matt Aimonetti 2009-01-28 slight change of API, CR::D... 108 def put_attachment(doc, name, file, options = {})
109 docid = escape_docid(doc['_id'])
110 name = CGI.escape(name)
c18567f8 » Matt Aimonetti 2009-06-07 differentiated attachment's... 111 uri = url_for_attachment(doc, name)
b2a29d9e » Matt Aimonetti 2009-07-14 started extracting the http... 112 JSON.parse(HttpAbstraction.put(uri, file, options))
2b7e49c9 » jchris 2008-09-30 put attachments 113 end
902e1bed » jchris 2008-09-30 moved specs so the autotest... 114
b915f7f7 » mattly 2009-02-02 - Added Database#delete_att... 115 # DELETE an attachment directly from CouchDB
5270fde0 » Matt Aimonetti 2009-07-29 added an option to force th... 116 def delete_attachment(doc, name, force=false)
c18567f8 » Matt Aimonetti 2009-06-07 differentiated attachment's... 117 uri = url_for_attachment(doc, name)
60c57796 » jchris 2009-02-02 all specs pass; refined att... 118 # this needs a rev
5270fde0 » Matt Aimonetti 2009-07-29 added an option to force th... 119 begin
120 JSON.parse(HttpAbstraction.delete(uri))
121 rescue Exception => error
122 if force
123 # get over a 409
124 doc = get(doc['_id'])
125 uri = url_for_attachment(doc, name)
126 JSON.parse(HttpAbstraction.delete(uri))
127 else
128 error
129 end
130 end
b915f7f7 » mattly 2009-02-02 - Added Database#delete_att... 131 end
62ea68df » Igal Koshevoy 2009-11-15 Improved CouchRest::Databas... 132
254eb201 » jchris 2008-10-14 view blocks flow 133 # Save a document to CouchDB. This will use the <tt>_id</tt> field from
134 # the document as the id for PUT, or request a new UUID from CouchDB, if
135 # no <tt>_id</tt> is present on the document. IDs are attached to
136 # documents on the client side because POST has the curious property of
137 # being automatically retried by proxies in the event of network
138 # segmentation and lost responses.
d8d5645e » thewordnerd 2008-12-14 Make bulk saving more flexi... 139 #
140 # If <tt>bulk</tt> is true (false by default) the document is cached for bulk-saving later.
141 # Bulk saving happens automatically when #bulk_save_cache limit is exceded, or on the next non bulk save.
62ea68df » Igal Koshevoy 2009-11-15 Improved CouchRest::Databas... 142 #
143 # If <tt>batch</tt> is true (false by default) the document is saved in
144 # batch mode, "used to achieve higher throughput at the cost of lower
145 # guarantees. When [...] sent using this option, it is not immediately
146 # written to disk. Instead it is stored in memory on a per-user basis for a
147 # second or so (or the number of docs in memory reaches a certain point).
148 # After the threshold has passed, the docs are committed to disk. Instead
149 # of waiting for the doc to be written to disk before responding, CouchDB
150 # sends an HTTP 202 Accepted response immediately. batch=ok is not suitable
151 # for crucial data, but it ideal for applications like logging which can
152 # accept the risk that a small proportion of updates could be lost due to a
153 # crash."
154 def save_doc(doc, bulk = false, batch = false)
bf1acd83 » jchris 2008-06-07 document attachments now su... 155 if doc['_attachments']
156 doc['_attachments'] = encode_attachments(doc['_attachments'])
157 end
d8d5645e » thewordnerd 2008-12-14 Make bulk saving more flexi... 158 if bulk
159 @bulk_save_cache << doc
b5d6baaf » Julien Sanchez 2009-10-12 Save on Document & Extended... 160 bulk_save if @bulk_save_cache.length >= @bulk_save_cache_limit
84382d8a » thewordnerd 2008-12-15 Removed model create/update... 161 return {"ok" => true} # Compatibility with Document#save
d8d5645e » thewordnerd 2008-12-14 Make bulk saving more flexi... 162 elsif !bulk && @bulk_save_cache.length > 0
163 bulk_save
164 end
0769c269 » jchris 2008-11-08 on the road toward design docs 165 result = if doc['_id']
e48a6c88 » Matt Aimonetti 2009-05-26 fixed all the specs so we a... 166 slug = escape_docid(doc['_id'])
167 begin
62ea68df » Igal Koshevoy 2009-11-15 Improved CouchRest::Databas... 168 uri = "#{@root}/#{slug}"
169 uri << "?batch=ok" if batch
170 CouchRest.put uri, doc
b2a29d9e » Matt Aimonetti 2009-07-14 started extracting the http... 171 rescue HttpAbstraction::ResourceNotFound
e48a6c88 » Matt Aimonetti 2009-05-26 fixed all the specs so we a... 172 p "resource not found when saving even tho an id was passed"
173 slug = doc['_id'] = @server.next_uuid
c18567f8 » Matt Aimonetti 2009-06-07 differentiated attachment's... 174 CouchRest.put "#{@root}/#{slug}", doc
e48a6c88 » Matt Aimonetti 2009-05-26 fixed all the specs so we a... 175 end
097ab935 » jchris 2008-03-19 getting into view land 176 else
9e4d5c0e » jchris 2008-09-13 backwards compatibility for... 177 begin
178 slug = doc['_id'] = @server.next_uuid
c18567f8 » Matt Aimonetti 2009-06-07 differentiated attachment's... 179 CouchRest.put "#{@root}/#{slug}", doc
9e4d5c0e » jchris 2008-09-13 backwards compatibility for... 180 rescue #old version of couchdb
c18567f8 » Matt Aimonetti 2009-06-07 differentiated attachment's... 181 CouchRest.post @root, doc
9e4d5c0e » jchris 2008-09-13 backwards compatibility for... 182 end
097ab935 » jchris 2008-03-19 getting into view land 183 end
0769c269 » jchris 2008-11-08 on the road toward design docs 184 if result['ok']
185 doc['_id'] = result['id']
186 doc['_rev'] = result['rev']
187 doc.database = self if doc.respond_to?(:database=)
188 end
189 result
097ab935 » jchris 2008-03-19 getting into view land 190 end
191
d0d5eec1 » Igal Koshevoy 2009-11-15 Added CouchRest::Database#b... 192 # Save a document to CouchDB in bulk mode. See #save_doc's +bulk+ argument.
193 def bulk_save_doc(doc)
194 save_doc(doc, true)
195 end
196
197 # Save a document to CouchDB in batch mode. See #save_doc's +batch+ argument.
198 def batch_save_doc(doc)
199 save_doc(doc, false, true)
200 end
84e2bf94 » Matt Aimonetti 2009-01-28 slight change of API, CR::D... 201
254eb201 » jchris 2008-10-14 view blocks flow 202 # POST an array of documents to CouchDB. If any of the documents are
203 # missing ids, supply one from the uuid cache.
d8d5645e » thewordnerd 2008-12-14 Make bulk saving more flexi... 204 #
205 # If called with no arguments, bulk saves the cache of documents to be bulk saved.
8f24d7d5 » jchris 2009-01-23 bulk_save has an option to ... 206 def bulk_save(docs = nil, use_uuids = true)
d8d5645e » thewordnerd 2008-12-14 Make bulk saving more flexi... 207 if docs.nil?
208 docs = @bulk_save_cache
209 @bulk_save_cache = []
210 end
8f24d7d5 » jchris 2009-01-23 bulk_save has an option to ... 211 if (use_uuids)
212 ids, noids = docs.partition{|d|d['_id']}
213 uuid_count = [noids.length, @server.uuid_batch_count].max
214 noids.each do |doc|
215 nextid = @server.next_uuid(uuid_count) rescue nil
216 doc['_id'] = nextid if nextid
217 end
54f26017 » jchris 2008-09-07 CouchRest no longer uses PO... 218 end
c18567f8 » Matt Aimonetti 2009-06-07 differentiated attachment's... 219 CouchRest.post "#{@root}/_bulk_docs", {:docs => docs}
7f144586 » jchris 2008-03-19 bulk save 220 end
c4cce183 » Matt Aimonetti 2009-02-17 added database.bulk_delete ... 221 alias :bulk_delete :bulk_save
7f144586 » jchris 2008-03-19 bulk save 222
254eb201 » jchris 2008-10-14 view blocks flow 223 # DELETE the document from CouchDB that has the given <tt>_id</tt> and
224 # <tt>_rev</tt>.
36945d5a » AntonyBlakey 2009-01-09 Add bulk save deferal optio... 225 #
226 # If <tt>bulk</tt> is true (false by default) the deletion is recorded for bulk-saving (bulk-deletion :) later.
227 # Bulk saving happens automatically when #bulk_save_cache limit is exceded, or on the next non bulk save.
84e2bf94 » Matt Aimonetti 2009-01-28 slight change of API, CR::D... 228 def delete_doc(doc, bulk = false)
6b57357f » jchris 2009-01-12 merge deferred-delete 229 raise ArgumentError, "_id and _rev required for deleting" unless doc['_id'] && doc['_rev']
36945d5a » AntonyBlakey 2009-01-09 Add bulk save deferal optio... 230 if bulk
231 @bulk_save_cache << { '_id' => doc['_id'], '_rev' => doc['_rev'], '_deleted' => true }
232 return bulk_save if @bulk_save_cache.length >= @bulk_save_cache_limit
233 return { "ok" => true } # Mimic the non-deferred version
234 end
bca68cf1 » jchris 2009-01-12 design doc ids fixed throug... 235 slug = escape_docid(doc['_id'])
c18567f8 » Matt Aimonetti 2009-06-07 differentiated attachment's... 236 CouchRest.delete "#{@root}/#{slug}?rev=#{doc['_rev']}"
84e2bf94 » Matt Aimonetti 2009-01-28 slight change of API, CR::D... 237 end
238
9faa9daa » mattly 2009-01-05 support for couchdb's suppo... 239 # COPY an existing document to a new id. If the destination id currently exists, a rev must be provided.
240 # <tt>dest</tt> can take one of two forms if overwriting: "id_to_overwrite?rev=revision" or the actual doc
241 # hash with a '_rev' key
84e2bf94 » Matt Aimonetti 2009-01-28 slight change of API, CR::D... 242 def copy_doc(doc, dest)
9faa9daa » mattly 2009-01-05 support for couchdb's suppo... 243 raise ArgumentError, "_id is required for copying" unless doc['_id']
bca68cf1 » jchris 2009-01-12 design doc ids fixed throug... 244 slug = escape_docid(doc['_id'])
9faa9daa » mattly 2009-01-05 support for couchdb's suppo... 245 destination = if dest.respond_to?(:has_key?) && dest['_id'] && dest['_rev']
246 "#{dest['_id']}?rev=#{dest['_rev']}"
247 else
248 dest
249 end
c18567f8 » Matt Aimonetti 2009-06-07 differentiated attachment's... 250 CouchRest.copy "#{@root}/#{slug}", destination
84e2bf94 » Matt Aimonetti 2009-01-28 slight change of API, CR::D... 251 end
252
dd7f1098 » thewordnerd 2008-12-14 Add support for database co... 253 # Compact the database, removing old document revisions and optimizing space use.
254 def compact!
c18567f8 » Matt Aimonetti 2009-06-07 differentiated attachment's... 255 CouchRest.post "#{@root}/_compact"
84e2bf94 » Matt Aimonetti 2009-01-28 slight change of API, CR::D... 256 end
257
258 # Create the database
259 def create!
260 bool = server.create_db(@name) rescue false
261 bool && true
262 end
263
264 # Delete and re create the database
265 def recreate!
266 delete!
267 create!
b2a29d9e » Matt Aimonetti 2009-07-14 started extracting the http... 268 rescue HttpAbstraction::ResourceNotFound
84e2bf94 » Matt Aimonetti 2009-01-28 slight change of API, CR::D... 269 ensure
270 create!
dd7f1098 » thewordnerd 2008-12-14 Add support for database co... 271 end
571cd257 » mattly 2009-01-31 database replication method... 272
273 # Replicates via "pulling" from another database to this database. Makes no attempt to deal with conflicts.
274 def replicate_from other_db
275 raise ArgumentError, "must provide a CouchReset::Database" unless other_db.kind_of?(CouchRest::Database)
276 CouchRest.post "#{@host}/_replicate", :source => other_db.root, :target => name
277 end
278
279 # Replicates via "pushing" to another database. Makes no attempt to deal with conflicts.
280 def replicate_to other_db
281 raise ArgumentError, "must provide a CouchReset::Database" unless other_db.kind_of?(CouchRest::Database)
282 CouchRest.post "#{@host}/_replicate", :target => other_db.root, :source => name
283 end
284
254eb201 » jchris 2008-10-14 view blocks flow 285 # DELETE the database itself. This is not undoable and could be rather
286 # catastrophic. Use with care!
32c01fad » jchris 2008-03-18 working specy stuff i hope 287 def delete!
c35c3515 » Matt Aimonetti 2009-05-27 added an automated way to m... 288 clear_extended_doc_fresh_cache
c18567f8 » Matt Aimonetti 2009-06-07 differentiated attachment's... 289 CouchRest.delete @root
32c01fad » jchris 2008-03-18 working specy stuff i hope 290 end
54f26017 » jchris 2008-09-07 CouchRest no longer uses PO... 291
bf1acd83 » jchris 2008-06-07 document attachments now su... 292 private
b915f7f7 » mattly 2009-02-02 - Added Database#delete_att... 293
c35c3515 » Matt Aimonetti 2009-05-27 added an automated way to m... 294 def clear_extended_doc_fresh_cache
295c0f05 » Matt Aimonetti 2009-05-27 fixed the design doc cache ... 295 ::CouchRest::ExtendedDocument.subclasses.each{|klass| klass.design_doc_fresh = false if klass.respond_to?(:design_doc_fresh=) }
c35c3515 » Matt Aimonetti 2009-05-27 added an automated way to m... 296 end
c18567f8 » Matt Aimonetti 2009-06-07 differentiated attachment's... 297
fe489f2d » Matt Aimonetti 2009-02-24 removed CouchRest::Model, a... 298 def uri_for_attachment(doc, name)
60c57796 » jchris 2009-02-02 all specs pass; refined att... 299 if doc.is_a?(String)
300 puts "CouchRest::Database#fetch_attachment will eventually require a doc as the first argument, not a doc.id"
301 docid = doc
302 rev = nil
303 else
304 docid = doc['_id']
305 rev = doc['_rev']
306 end
307 docid = escape_docid(docid)
b915f7f7 » mattly 2009-02-02 - Added Database#delete_att... 308 name = CGI.escape(name)
60c57796 » jchris 2009-02-02 all specs pass; refined att... 309 rev = "?rev=#{doc['_rev']}" if rev
c18567f8 » Matt Aimonetti 2009-06-07 differentiated attachment's... 310 "/#{docid}/#{name}#{rev}"
311 end
312
313 def url_for_attachment(doc, name)
314 @root + uri_for_attachment(doc, name)
b915f7f7 » mattly 2009-02-02 - Added Database#delete_att... 315 end
316
bca68cf1 » jchris 2009-01-12 design doc ids fixed throug... 317 def escape_docid id
318 /^_design\/(.*)/ =~ id ? "_design/#{CGI.escape($1)}" : CGI.escape(id)
d1f8970c » jchris 2009-01-12 fixed ddoc names on get 319 end
320
84e2bf94 » Matt Aimonetti 2009-01-28 slight change of API, CR::D... 321 def encode_attachments(attachments)
bf1acd83 » jchris 2008-06-07 document attachments now su... 322 attachments.each do |k,v|
9954d914 » mattly 2008-06-12 PUT attachments as stubs do... 323 next if v['stub']
881b18b0 » jchris 2008-06-07 better attachment api 324 v['data'] = base64(v['data'])
bf1acd83 » jchris 2008-06-07 document attachments now su... 325 end
881b18b0 » jchris 2008-06-07 better attachment api 326 attachments
bf1acd83 » jchris 2008-06-07 document attachments now su... 327 end
54f26017 » jchris 2008-09-07 CouchRest no longer uses PO... 328
84e2bf94 » Matt Aimonetti 2009-01-28 slight change of API, CR::D... 329 def base64(data)
bf1acd83 » jchris 2008-06-07 document attachments now su... 330 Base64.encode64(data).gsub(/\s/,'')
331 end
32c01fad » jchris 2008-03-18 working specy stuff i hope 332 end
19e3e45c » jchris 2008-06-20 almost ready for gem versio... 333 end