jchris-couchrest 0.8.3 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
data/bin/couchdir ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ unless ARGV.length >= 2
4
+ puts "usage: couchdir path/to/directory db-name [docid]"
5
+ exit
6
+ end
7
+
8
+ require 'rubygems'
9
+ require 'couchrest'
10
+
11
+ dirname = ARGV[0]
12
+ dbname = ARGV[1]
13
+ docid = ARGV[2]
14
+
15
+ puts "Create attachments for the files in #{dirname} in database #{dbname}."
16
+
17
+ fm = CouchRest::FileManager.new(dbname)
18
+ fm.loud = true
19
+ puts "Pushing views from directory #{dirname} to database #{fm.db}"
20
+ fm.push_directory(dirname, docid)
data/bin/couchview ADDED
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ commands = %w{push generate}
4
+
5
+ command = ARGV[0]
6
+
7
+ if !commands.include?(command)
8
+ puts <<-USAGE
9
+ Couchview has two modes: push and generate. Run couchview push or couchview generate for usage documentation.
10
+ USAGE
11
+ exit
12
+ end
13
+
14
+ if ARGV.length == 1
15
+ case command
16
+ when "generate"
17
+ puts <<-GEN
18
+ Usage: couchview generate directory design1 design2 design3 ...
19
+
20
+ Couchview will create directories and example views for the design documents you specify.
21
+
22
+ GEN
23
+ when "push"
24
+ puts <<-PUSH
25
+ == Pushing views with Couchview ==
26
+
27
+ Usage: couchview push directory dbname
28
+
29
+ Couchview expects a specific filesystem layout for your CouchDB views (see
30
+ example below). It also supports advanced features like inlining of library
31
+ code (so you can keep DRY) as well as avoiding unnecessary document
32
+ modification.
33
+
34
+ Couchview also solves a problem with CouchDB's view API, which only provides
35
+ access to the final reduce side of any views which have both a map and a
36
+ reduce function defined. The intermediate map results are often useful for
37
+ development and production. CouchDB is smart enough to reuse map indexes for
38
+ functions duplicated across views within the same design document.
39
+
40
+ For views with a reduce function defined, Couchview creates both a reduce view
41
+ and a map-only view, so that you can browse and query the map side as well as
42
+ the reduction, with no performance penalty.
43
+
44
+ == Example ==
45
+
46
+ couchview push foo-project/bar-views baz-database
47
+
48
+ This will push the views defined in foo-project/bar-views into a database
49
+ called baz-database. Couchview expects the views to be defined in files with
50
+ names like:
51
+
52
+ foo-project/bar-views/my-design/viewname-map.js
53
+ foo-project/bar-views/my-design/viewname-reduce.js
54
+ foo-project/bar-views/my-design/noreduce-map.js
55
+
56
+ Pushed to => http://localhost:5984/baz-database/_design/my-design
57
+
58
+ And the design document:
59
+ {
60
+ "views" : {
61
+ "viewname-map" : {
62
+ "map" : "### contents of view-name-map.js ###"
63
+ },
64
+ "viewname-reduce" : {
65
+ "map" : "### contents of view-name-map.js ###",
66
+ "reduce" : "### contents of view-name-reduce.js ###"
67
+ },
68
+ "noreduce-map" : {
69
+ "map" : "### contents of noreduce-map.js ###"
70
+ }
71
+ }
72
+ }
73
+
74
+ Couchview will create a design document for each subdirectory of the views
75
+ directory specified on the command line.
76
+
77
+ == Library Inlining ==
78
+
79
+ Couchview can optionally inline library code into your views so you only have
80
+ to maintain it in one place. It looks for any files named lib.* in your
81
+ design-doc directory (for doc specific libs) and in the parent views directory
82
+ (for project global libs). These libraries are only inserted into views which
83
+ include the text
84
+
85
+ //include-lib
86
+
87
+ or
88
+
89
+ #include-lib
90
+
91
+ Couchview is a result of scratching my own itch. I'd be happy to make it more
92
+ general, so please contact me at jchris@grabb.it if you'd like to see anything
93
+ added or changed.
94
+ PUSH
95
+ end
96
+ exit
97
+ end
98
+
99
+ require 'rubygems'
100
+ require 'couchrest'
101
+
102
+ if command == 'push'
103
+ dirname = ARGV[1]
104
+ dbname = ARGV[2]
105
+ fm = CouchRest::FileManager.new(dbname)
106
+ fm.loud = true
107
+ puts "Pushing views from directory #{dirname} to database #{fm.db}"
108
+ fm.push_views(dirname)
109
+ elsif command == 'generate'
110
+ puts "Under construction ;)"
111
+ end
data/lib/couch_rest.rb ADDED
@@ -0,0 +1,62 @@
1
+ class CouchRest
2
+ attr_accessor :uri
3
+ def initialize server = 'http://localhost:5984'
4
+ @uri = server
5
+ end
6
+
7
+ # list all databases on the server
8
+ def databases
9
+ CouchRest.get "#{@uri}/_all_dbs"
10
+ end
11
+
12
+ def database name
13
+ CouchRest::Database.new(@uri, name)
14
+ end
15
+
16
+ # get the welcome message
17
+ def info
18
+ CouchRest.get "#{@uri}/"
19
+ end
20
+
21
+ # restart the couchdb instance
22
+ def restart!
23
+ CouchRest.post "#{@uri}/_restart"
24
+ end
25
+
26
+ # create a database
27
+ def create_db name
28
+ CouchRest.put "#{@uri}/#{name}"
29
+ database name
30
+ end
31
+
32
+ class << self
33
+ def put uri, doc = nil
34
+ payload = doc.to_json if doc
35
+ JSON.parse(RestClient.put(uri, payload))
36
+ end
37
+
38
+ def get uri
39
+ JSON.parse(RestClient.get(uri), :max_nesting => false)
40
+ end
41
+
42
+ def post uri, doc = nil
43
+ payload = doc.to_json if doc
44
+ JSON.parse(RestClient.post(uri, payload))
45
+ end
46
+
47
+ def delete uri
48
+ JSON.parse(RestClient.delete(uri))
49
+ end
50
+
51
+ def paramify_url url, params = nil
52
+ if params
53
+ query = params.collect do |k,v|
54
+ v = v.to_json if %w{key startkey endkey}.include?(k.to_s)
55
+ "#{k}=#{CGI.escape(v.to_s)}"
56
+ end.join("&")
57
+ url = "#{url}?#{query}"
58
+ end
59
+ url
60
+ end
61
+ end
62
+ end
data/lib/couchrest.rb CHANGED
@@ -2,68 +2,9 @@ require "rubygems"
2
2
  require 'json'
3
3
  require 'rest_client'
4
4
 
5
+ require File.dirname(__FILE__) + '/couch_rest'
5
6
  require File.dirname(__FILE__) + '/database'
6
7
  require File.dirname(__FILE__) + '/pager'
8
+ require File.dirname(__FILE__) + '/file_manager'
7
9
 
8
- class CouchRest
9
- attr_accessor :uri
10
- def initialize server = 'http://localhost:5984'
11
- @uri = server
12
- end
13
-
14
- # list all databases on the server
15
- def databases
16
- CouchRest.get "#{@uri}/_all_dbs"
17
- end
18
-
19
- def database name
20
- CouchRest::Database.new(@uri, name)
21
- end
22
-
23
- # get the welcome message
24
- def info
25
- CouchRest.get "#{@uri}/"
26
- end
27
10
 
28
- # restart the couchdb instance
29
- def restart!
30
- CouchRest.post "#{@uri}/_restart"
31
- end
32
-
33
- # create a database
34
- def create_db name
35
- CouchRest.put "#{@uri}/#{name}"
36
- database name
37
- end
38
-
39
- class << self
40
- def put uri, doc = nil
41
- payload = doc.to_json if doc
42
- JSON.parse(RestClient.put(uri, payload))
43
- end
44
-
45
- def get uri
46
- JSON.parse(RestClient.get(uri), :max_nesting => false)
47
- end
48
-
49
- def post uri, doc = nil
50
- payload = doc.to_json if doc
51
- JSON.parse(RestClient.post(uri, payload))
52
- end
53
-
54
- def delete uri
55
- JSON.parse(RestClient.delete(uri))
56
- end
57
-
58
- def paramify_url url, params = nil
59
- if params
60
- query = params.collect do |k,v|
61
- v = v.to_json if %w{key startkey endkey}.include?(k.to_s)
62
- "#{k}=#{CGI.escape(v.to_s)}"
63
- end.join("&")
64
- url = "#{url}?#{query}"
65
- end
66
- url
67
- end
68
- end
69
- end
data/lib/database.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  require 'cgi'
2
2
  require "base64"
3
- require File.dirname(__FILE__) + '/couchrest'
4
3
 
5
4
  class CouchRest
6
5
  class Database
@@ -11,6 +10,10 @@ class CouchRest
11
10
  @root = "#{host}/#{name}"
12
11
  end
13
12
 
13
+ def to_s
14
+ @root
15
+ end
16
+
14
17
  def documents params = nil
15
18
  url = CouchRest.paramify_url "#{@root}/_all_docs", params
16
19
  CouchRest.get url
@@ -0,0 +1,396 @@
1
+ require 'digest/md5'
2
+
3
+ class CouchRest
4
+ class FileManager
5
+ attr_reader :db
6
+ attr_accessor :loud
7
+
8
+ LANGS = {"rb" => "ruby", "js" => "javascript"}
9
+ MIMES = {
10
+ "html" => "text/html",
11
+ "htm" => "text/html",
12
+ "png" => "image/png",
13
+ "css" => "text/css",
14
+ "js" => "test/javascript"
15
+ }
16
+ def initialize(dbname, host="http://localhost:5984")
17
+ @db = CouchRest.new(host).database(dbname)
18
+ end
19
+
20
+ def push_directory(push_dir, docid=nil)
21
+ docid ||= push_dir.split('/').reverse.find{|part|!part.empty?}
22
+
23
+ pushfiles = Dir["#{push_dir}/**/*.*"].collect do |f|
24
+ {f.split("#{push_dir}/").last => open(f).read}
25
+ end
26
+
27
+ return if pushfiles.empty?
28
+
29
+ @attachments = {}
30
+ @signatures = {}
31
+ pushfiles.each do |file|
32
+ name = file.keys.first
33
+ value = file.values.first
34
+ @signatures[name] = md5(value)
35
+
36
+ @attachments[name] = {
37
+ "data" => value,
38
+ "content_type" => MIMES[name.split('.').last]
39
+ }
40
+ end
41
+
42
+ doc = @db.get(docid) rescue nil
43
+
44
+ unless doc
45
+ say "creating #{docid}"
46
+ @db.save({"_id" => docid, "_attachments" => @attachments, "signatures" => @signatures})
47
+ return
48
+ end
49
+
50
+ # remove deleted docs
51
+ to_be_removed = doc["signatures"].keys.select do |d|
52
+ !pushfiles.collect{|p| p.keys.first}.include?(d)
53
+ end
54
+
55
+ to_be_removed.each do |p|
56
+ say "deleting #{p}"
57
+ doc["signatures"].delete(p)
58
+ doc["_attachments"].delete(p)
59
+ end
60
+
61
+ # update existing docs:
62
+ doc["signatures"].each do |path, sig|
63
+ if (@signatures[path] == sig)
64
+ say "no change to #{path}. skipping..."
65
+ else
66
+ say "replacing #{path}"
67
+ doc["signatures"][path] = md5(@attachments[path]["data"])
68
+ doc["_attachments"][path].delete("stub")
69
+ doc["_attachments"][path].delete("length")
70
+ doc["_attachments"][path]["data"] = @attachments[path]["data"]
71
+ doc["_attachments"][path].merge!({"data" => @attachments[path]["data"]} )
72
+
73
+ end
74
+ end
75
+
76
+ # add in new files
77
+ new_files = pushfiles.select{|d| !doc["signatures"].keys.include?( d.keys.first) }
78
+
79
+ new_files.each do |f|
80
+ say "creating #{f}"
81
+ path = f.keys.first
82
+ content = f.values.first
83
+ doc["signatures"][path] = md5(content)
84
+
85
+ doc["_attachments"][path] = {
86
+ "data" => content,
87
+ "content_type" => @content_types[path.split('.').last]
88
+ }
89
+ end
90
+
91
+ begin
92
+ @db.save(doc)
93
+ rescue Exception => e
94
+ say e.message
95
+ end
96
+ end
97
+
98
+ def push_views(view_dir)
99
+ designs = {}
100
+
101
+ Dir["#{view_dir}/**/*.*"].each do |design_doc|
102
+ design_doc_parts = design_doc.split('/')
103
+ next if /^lib\..*$/.match design_doc_parts.last
104
+ pre_normalized_view_name = design_doc_parts.last.split("-")
105
+ view_name = pre_normalized_view_name[0..pre_normalized_view_name.length-2].join("-")
106
+
107
+ folder = design_doc_parts[-2]
108
+
109
+ designs[folder] ||= {}
110
+ designs[folder]["views"] ||= {}
111
+ design_lang = design_doc_parts.last.split(".").last
112
+ designs[folder]["language"] ||= LANGS[design_lang]
113
+
114
+ libs = ""
115
+ Dir["#{view_dir}/lib.#{design_lang}"].collect do |global_lib|
116
+ libs << open(global_lib).read
117
+ libs << "\n"
118
+ end
119
+ Dir["#{view_dir}/#{folder}/lib.#{design_lang}"].collect do |global_lib|
120
+ libs << open(global_lib).read
121
+ libs << "\n"
122
+ end
123
+ if design_doc_parts.last =~ /-map/
124
+ designs[folder]["views"]["#{view_name}-map"] ||= {}
125
+
126
+ designs[folder]["views"]["#{view_name}-map"]["map"] = read(design_doc, libs)
127
+
128
+ designs[folder]["views"]["#{view_name}-reduce"] ||= {}
129
+ designs[folder]["views"]["#{view_name}-reduce"]["map"] = read(design_doc, libs)
130
+ end
131
+
132
+ if design_doc_parts.last =~ /-reduce/
133
+ designs[folder]["views"]["#{view_name}-reduce"] ||= {}
134
+
135
+ designs[folder]["views"]["#{view_name}-reduce"]["reduce"] = read(design_doc, libs)
136
+ end
137
+ end
138
+
139
+ # cleanup empty maps and reduces
140
+ designs.each do |name, props|
141
+ props["views"].each do |view, funcs|
142
+ next unless view.include?("reduce")
143
+ props["views"].delete(view) unless funcs.keys.include?("reduce")
144
+ end
145
+ end
146
+
147
+ designs.each do |k,v|
148
+ create_or_update("_design/#{k}", v)
149
+ end
150
+
151
+ designs
152
+ end
153
+
154
+
155
+ private
156
+
157
+ def say words
158
+ puts words if @loud
159
+ end
160
+
161
+ def md5 string
162
+ Digest::MD5.hexdigest(string)
163
+ end
164
+
165
+ def read(file, libs=nil)
166
+ st = open(file).read
167
+ st.sub!(/(\/\/|#)include-lib/,libs) if libs
168
+ st
169
+ end
170
+
171
+ def create_or_update(id, fields)
172
+ existing = @db.get(id) rescue nil
173
+
174
+ if existing
175
+ updated = fields.merge({"_id" => id, "_rev" => existing["_rev"]})
176
+ if existing != updated
177
+ say "replacing #{id}"
178
+ db.save(updated)
179
+ else
180
+ say "skipping #{id}"
181
+ end
182
+ else
183
+ say "creating #{id}"
184
+ db.save(fields.merge({"_id" => id}))
185
+ end
186
+
187
+ end
188
+ end
189
+ end
190
+
191
+ __END__
192
+
193
+
194
+
195
+
196
+ # parse the file structure to load the public files, controllers, and views into a hash with the right shape for coucdb
197
+ couch = {}
198
+
199
+ couch["public"] = Dir["#{File.expand_path(File.dirname("."))}/public/**/*.*"].collect do |f|
200
+ {f.split("public/").last => open(f).read}
201
+ end
202
+
203
+ couch["controllers"] = {}
204
+ Dir["#{File.expand_path(File.dirname("."))}/app/controllers/**/*.*"].collect do |c|
205
+ path_parts = c.split("/")
206
+
207
+ controller_name = path_parts[path_parts.length - 2]
208
+ action_name = path_parts[path_parts.length - 1].split(".").first
209
+
210
+ couch["controllers"][controller_name] ||= {"actions" => {}}
211
+ couch["controllers"][controller_name]["actions"][action_name] = open(c).read
212
+
213
+ end
214
+
215
+ couch["designs"] = {}
216
+ Dir["#{File.expand_path(File.dirname("."))}/app/views/**/*.*"].collect do |design_doc|
217
+ design_doc_parts = design_doc.split('/')
218
+ pre_normalized_view_name = design_doc_parts.last.split("-")
219
+ view_name = pre_normalized_view_name[0..pre_normalized_view_name.length-2].join("-")
220
+
221
+ folder = design_doc.split("app/views").last.split("/")[1]
222
+
223
+ couch["designs"][folder] ||= {}
224
+ couch["designs"][folder]["views"] ||= {}
225
+ couch["designs"][folder]["language"] ||= LANGS[design_doc_parts.last.split(".").last]
226
+
227
+ if design_doc_parts.last =~ /-map/
228
+ couch["designs"][folder]["views"]["#{view_name}-map"] ||= {}
229
+
230
+ couch["designs"][folder]["views"]["#{view_name}-map"]["map"] = open(design_doc).read
231
+
232
+ couch["designs"][folder]["views"]["#{view_name}-reduce"] ||= {}
233
+ couch["designs"][folder]["views"]["#{view_name}-reduce"]["map"] = open(design_doc).read
234
+ end
235
+
236
+ if design_doc_parts.last =~ /-reduce/
237
+ couch["designs"][folder]["views"]["#{view_name}-reduce"] ||= {}
238
+
239
+ couch["designs"][folder]["views"]["#{view_name}-reduce"]["reduce"] = open(design_doc).read
240
+ end
241
+ end
242
+
243
+ # cleanup empty maps and reduces
244
+ couch["designs"].each do |name, props|
245
+ props["views"].delete("#{name}-reduce") unless props["views"]["#{name}-reduce"].keys.include?("reduce")
246
+ end
247
+
248
+ # parsing done, begin posting
249
+
250
+ # connect to couchdb
251
+ cr = CouchRest.new("http://localhost:5984")
252
+ @db = cr.database(DBNAME)
253
+
254
+ def create_or_update(id, fields)
255
+ existing = get(id)
256
+
257
+ if existing
258
+ updated = fields.merge({"_id" => id, "_rev" => existing["_rev"]})
259
+ else
260
+ puts "saving #{id}"
261
+ save(fields.merge({"_id" => id}))
262
+ end
263
+
264
+ if existing == updated
265
+ puts "no change to #{id}. skipping..."
266
+ else
267
+ puts "replacing #{id}"
268
+ save(updated)
269
+ end
270
+
271
+ end
272
+
273
+ def get(id)
274
+ doc = handle_errors do
275
+ @db.get(id)
276
+ end
277
+ end
278
+
279
+ def save(doc)
280
+ handle_errors do
281
+ @db.save(doc)
282
+ end
283
+ end
284
+
285
+ def handle_errors(&block)
286
+ begin
287
+ yield
288
+ rescue Exception => e
289
+ # puts e.message
290
+ nil
291
+ end
292
+ end
293
+
294
+
295
+ if todo.include? "views"
296
+ puts "posting views into CouchDB"
297
+ couch["designs"].each do |k,v|
298
+ create_or_update("_design/#{k}", v)
299
+ end
300
+ puts
301
+ end
302
+
303
+ if todo.include? "controllers"
304
+ puts "posting controllers into CouchDB"
305
+ couch["controllers"].each do |k,v|
306
+ create_or_update("controller/#{k}", v)
307
+ end
308
+ puts
309
+ end
310
+
311
+
312
+ if todo.include? "public"
313
+ puts "posting public docs into CouchDB"
314
+
315
+ if couch["public"].empty?
316
+ puts "no docs in public"; exit
317
+ end
318
+
319
+ @content_types = {
320
+ "html" => "text/html",
321
+ "htm" => "text/html",
322
+ "png" => "image/png",
323
+ "css" => "text/css",
324
+ "js" => "test/javascript"
325
+ }
326
+
327
+ def md5 string
328
+ Digest::MD5.hexdigest(string)
329
+ end
330
+
331
+ @attachments = {}
332
+ @signatures = {}
333
+ couch["public"].each do |doc|
334
+ @signatures[doc.keys.first] = md5(doc.values.first)
335
+
336
+ @attachments[doc.keys.first] = {
337
+ "data" => doc.values.first,
338
+ "content_type" => @content_types[doc.keys.first.split('.').last]
339
+ }
340
+ end
341
+
342
+ doc = get("public")
343
+
344
+ unless doc
345
+ puts "creating public"
346
+ @db.save({"_id" => "public", "_attachments" => @attachments, "signatures" => @signatures})
347
+ exit
348
+ end
349
+
350
+ # remove deleted docs
351
+ to_be_removed = doc["signatures"].keys.select{|d| !couch["public"].collect{|p| p.keys.first}.include?(d) }
352
+
353
+ to_be_removed.each do |p|
354
+ puts "deleting #{p}"
355
+ doc["signatures"].delete(p)
356
+ doc["_attachments"].delete(p)
357
+ end
358
+
359
+ # update existing docs:
360
+ doc["signatures"].each do |path, sig|
361
+ if (@signatures[path] == sig)
362
+ puts "no change to #{path}. skipping..."
363
+ else
364
+ puts "replacing #{path}"
365
+ doc["signatures"][path] = md5(@attachments[path]["data"])
366
+ doc["_attachments"][path].delete("stub")
367
+ doc["_attachments"][path].delete("length")
368
+ doc["_attachments"][path]["data"] = @attachments[path]["data"]
369
+ doc["_attachments"][path].merge!({"data" => @attachments[path]["data"]} )
370
+
371
+ end
372
+ end
373
+
374
+ # add in new files
375
+ new_files = couch["public"].select{|d| !doc["signatures"].keys.include?( d.keys.first) }
376
+
377
+ new_files.each do |f|
378
+ puts "creating #{f}"
379
+ path = f.keys.first
380
+ content = f.values.first
381
+ doc["signatures"][path] = md5(content)
382
+
383
+ doc["_attachments"][path] = {
384
+ "data" => content,
385
+ "content_type" => @content_types[path.split('.').last]
386
+ }
387
+ end
388
+
389
+ begin
390
+ @db.save(doc)
391
+ rescue Exception => e
392
+ puts e.message
393
+ end
394
+
395
+ puts
396
+ end
data/lib/pager.rb CHANGED
@@ -1,14 +1,3 @@
1
- # paginate though 'gcharts/mp3-trk-dom-map' view and save
2
-
3
- # get 1000 records
4
- # truncate so that the key of the last record is not included in the page
5
- # that key will be the first of the next page
6
- # (if the last key equals the first key, up the page size)
7
- # group the results by key
8
- # yield the group
9
-
10
- require File.dirname(__FILE__) + '/couchrest'
11
-
12
1
  module Enumerable
13
2
  def group_by
14
3
  inject({}) do |grouped, element|
@@ -16,19 +16,6 @@ describe CouchRest do
16
16
  end
17
17
  end
18
18
 
19
- describe "tested against the current CouchDB svn revision" do
20
- it "should be up to date" do
21
- v = @cr.info["version"]
22
- if /incubating/.match(v)
23
- v.should include('0.8.0')
24
- else
25
- vi = v.split(/a/).pop.to_i
26
- vi.should be >= 661484 # versions older than this will likely fail many specs
27
- vi.should be <= 663797 # versions newer than this haven't been tried
28
- end
29
- end
30
- end
31
-
32
19
  describe "getting info" do
33
20
  it "should list databases" do
34
21
  @cr.databases.should be_an_instance_of(Array)
@@ -0,0 +1,116 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe CouchRest::FileManager do
4
+ before(:all) do
5
+ @cr = CouchRest.new(COUCHHOST)
6
+ @db = @cr.database(TESTDB)
7
+ @db.delete! rescue nil
8
+ @db = @cr.create_db(TESTDB) rescue nil
9
+ end
10
+ it "should initialize" do
11
+ @fm = CouchRest::FileManager.new(TESTDB)
12
+ @fm.should_not be_nil
13
+ end
14
+ it "should require a db name" do
15
+ lambda{CouchRest::FileManager.new}.should raise_error
16
+ end
17
+ it "should accept a db name" do
18
+ @fm = CouchRest::FileManager.new(TESTDB, 'http://localhost')
19
+ @fm.db.name.should == TESTDB
20
+ end
21
+ it "should default to localhost couchdb" do
22
+ @fm = CouchRest::FileManager.new(TESTDB)
23
+ @fm.db.host.should == 'http://localhost:5984'
24
+ end
25
+ end
26
+
27
+ describe CouchRest::FileManager, "pushing views" do
28
+ before(:all) do
29
+ @cr = CouchRest.new(COUCHHOST)
30
+ @db = @cr.database(TESTDB)
31
+ @db.delete! rescue nil
32
+ @db = @cr.create_db(TESTDB) rescue nil
33
+
34
+ @fm = CouchRest::FileManager.new(TESTDB, COUCHHOST)
35
+ @view_dir = File.dirname(__FILE__) + '/fixtures/views'
36
+ ds = @fm.push_views(@view_dir)
37
+ @design = @db.get("_design/test_view")
38
+ end
39
+ it "should create a design document for each folder" do
40
+ @design["views"].should_not be_nil
41
+ end
42
+ it "should push a map and reduce view" do
43
+ @design["views"]["test-map"].should_not be_nil
44
+ @design["views"]["test-reduce"].should_not be_nil
45
+ end
46
+ it "should push a map only view" do
47
+ @design["views"]["only-map"].should_not be_nil
48
+ @design["views"]["only-reduce"].should be_nil
49
+ end
50
+ it "should include library files" do
51
+ @design["views"]["only-map"]["map"].should include("globalLib")
52
+ @design["views"]["only-map"]["map"].should include("justThisView")
53
+ end
54
+ it "should not create extra design docs" do
55
+ docs = @db.documents(:startkey => '_design', :endkey => '_design/ZZZZZZ')
56
+ docs['total_rows'].should == 1
57
+ end
58
+ end
59
+
60
+ describe CouchRest::FileManager, "pushing a directory with id" do
61
+ before(:all) do
62
+ @cr = CouchRest.new(COUCHHOST)
63
+ @db = @cr.database(TESTDB)
64
+ @db.delete! rescue nil
65
+ @db = @cr.create_db(TESTDB) rescue nil
66
+
67
+ @fm = CouchRest::FileManager.new(TESTDB, COUCHHOST)
68
+ @push_dir = File.dirname(__FILE__) + '/fixtures/attachments'
69
+ ds = @fm.push_directory(@push_dir, 'attached')
70
+ end
71
+ it "should create a document for the folder" do
72
+ @db.get("attached")
73
+ end
74
+ it "should make attachments" do
75
+ doc = @db.get("attached")
76
+ doc["_attachments"]["test.html"].should_not be_nil
77
+ end
78
+ it "should set the content type" do
79
+ doc = @db.get("attached")
80
+ doc["_attachments"]["test.html"]["content_type"].should == "text/html"
81
+ end
82
+ end
83
+
84
+ describe CouchRest::FileManager, "pushing a directory without id" do
85
+ before(:all) do
86
+ @cr = CouchRest.new(COUCHHOST)
87
+ @db = @cr.database(TESTDB)
88
+ @db.delete! rescue nil
89
+ @db = @cr.create_db(TESTDB) rescue nil
90
+
91
+ @fm = CouchRest::FileManager.new(TESTDB, COUCHHOST)
92
+ @push_dir = File.dirname(__FILE__) + '/fixtures/attachments'
93
+ ds = @fm.push_directory(@push_dir)
94
+ end
95
+ it "should use the dirname" do
96
+ doc = @db.get("attachments")
97
+ doc["_attachments"]["test.html"].should_not be_nil
98
+ end
99
+ end
100
+
101
+ describe CouchRest::FileManager, "pushing a directory/ without id" do
102
+ before(:all) do
103
+ @cr = CouchRest.new(COUCHHOST)
104
+ @db = @cr.database(TESTDB)
105
+ @db.delete! rescue nil
106
+ @db = @cr.create_db(TESTDB) rescue nil
107
+
108
+ @fm = CouchRest::FileManager.new(TESTDB, COUCHHOST)
109
+ @push_dir = File.dirname(__FILE__) + '/fixtures/attachments/'
110
+ ds = @fm.push_directory(@push_dir)
111
+ end
112
+ it "should use the dirname" do
113
+ doc = @db.get("attachments")
114
+ doc["_attachments"]["test.html"].should_not be_nil
115
+ end
116
+ end
metadata CHANGED
@@ -1,15 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jchris-couchrest
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.3
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - J. Chris Anderson
8
+ - Greg Borenstein
8
9
  autorequire:
9
10
  bindir: bin
10
11
  cert_chain: []
11
12
 
12
- date: 2008-06-20 00:00:00 -07:00
13
+ date: 2008-08-03 00:00:00 -07:00
13
14
  default_executable:
14
15
  dependencies:
15
16
  - !ruby/object:Gem::Dependency
@@ -32,23 +33,27 @@ dependencies:
32
33
  version:
33
34
  description: CouchRest provides a simple interface on top of CouchDB's RESTful HTTP API, as well as including some utility scripts for managing views and attachments.
34
35
  email: jchris@grabb.it
35
- executables: []
36
-
36
+ executables:
37
+ - couchview
38
+ - couchdir
37
39
  extensions: []
38
40
 
39
41
  extra_rdoc_files: []
40
42
 
41
43
  files:
42
44
  - lib/couchrest.rb
45
+ - lib/couch_rest.rb
43
46
  - lib/database.rb
44
47
  - lib/pager.rb
48
+ - lib/file_manager.rb
45
49
  - Rakefile
46
50
  - README
47
- - script/couchdir
48
- - script/couchview
51
+ - bin/couchdir
52
+ - bin/couchview
49
53
  - spec/couchrest_spec.rb
50
54
  - spec/database_spec.rb
51
55
  - spec/pager_spec.rb
56
+ - spec/file_manager_spec.rb
52
57
  - spec/spec_helper.rb
53
58
  has_rdoc: false
54
59
  homepage: http://github.com/jchris/couchrest
data/script/couchdir DELETED
@@ -1,62 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- unless ARGV.length >= 2
4
- puts "usage: couchdir path/to/directory db-name"
5
- exit
6
- end
7
-
8
- dirname = ARGV[0].sub(/\/$/,'')
9
- dbname = ARGV[1]
10
-
11
-
12
-
13
- puts "Shoving #{dirname} into #{dbname}."
14
-
15
- require File.expand_path(File.dirname(__FILE__)) + '/../couchrest'
16
- require 'fileutils'
17
-
18
- cr = CouchRest.new("http://localhost:5984")
19
- @db = cr.database(dbname)
20
-
21
- @content_types = {
22
- "html" => "text/html",
23
- "htm" => "text/html",
24
- "png" => "image/png",
25
- "css" => "text/css"
26
- }
27
-
28
- files = Dir.glob(File.join(dirname,"**","*"))
29
- attachments = {}
30
- files.each do |filename|
31
- content = open(filename).read
32
- aname = filename.split('/')
33
- aname.shift
34
- aname = aname.join('/')
35
- attachments[aname] = {
36
- "data" => content,
37
- "content_type" => @content_types[aname.split('.').last]
38
- }
39
- end
40
-
41
- puts attachments.keys.inspect
42
-
43
- begin
44
- doc = @db.get(dirname)
45
- rescue RestClient::Request::RequestFailed
46
- doc = nil
47
- end
48
-
49
- # puts "get: #{doc.inspect}"
50
-
51
- if doc
52
- doc["_attachments"] = attachments
53
- else
54
- doc = {
55
- "_id" => dirname,
56
- "_attachments" => attachments
57
- }
58
- end
59
-
60
- # puts "saving: #{doc.inspect}"
61
- @db.save(doc)
62
- puts "saved"
data/script/couchview DELETED
@@ -1,162 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- commands = %w{pull push}
4
-
5
- command = ARGV[0]
6
-
7
- if !commands.include?(command)
8
- puts <<-USAGE
9
- Usage: couchview (pull|push) my-database-name
10
- For help on pull and push run script/views (pull|push) without a database name.
11
- USAGE
12
- exit
13
- end
14
-
15
- if ARGV.length == 1
16
- case command
17
- when "pull"
18
- puts <<-PULL
19
- couchview pull my-database-name
20
-
21
- I will automagically create a "views" directory in your current working directory if none exists.
22
- Then I copy the design documents and views into a directory structure like:
23
-
24
- ./views/my-design-doc/view-name-map.js
25
- ./views/my-design-doc/view-name-reduce.js
26
-
27
- If your view names don't end in "map" or "reduce" I'll add those suffixes as a pull. On push I'll put them in new locations corresponding to these new names (overwriting the old design documents). I'm opinionated, but if these conventions don't work for you, the source code is right here.
28
-
29
- PULL
30
- when "push"
31
- puts <<-PUSH
32
- couchview push my-database-name
33
-
34
- I'll push all the files in your views directory to the specified database. Because CouchDB caches the results of view calculation by function content, there's no performance penalty for duplicating the map function twice, which I'll do if you have a reduce function. This makes it possible to browse the results of just the map, which can be useful for both queries and debugging.
35
-
36
- ./views/my-design-doc/view-name-map.js
37
- ./views/my-design-doc/view-name-reduce.js
38
-
39
- Pushed to =>
40
-
41
- http://localhost:5984/my-database-name/_design/my-design-doc
42
- {
43
- "views" : {
44
- "view-name-map" : {
45
- "map" : "### contents of view-name-map.js ###"
46
- },
47
- "view-name-reduce" : {
48
- "map" : "### contents of view-name-map.js ###",
49
- "reduce" : "### contents of view-name-reduce.js ###"
50
- },
51
- }
52
- }
53
- PUSH
54
- end
55
- exit
56
- end
57
-
58
- dbname = ARGV[1]
59
- dirname = ARGV[2] || "views"
60
-
61
- puts "Running #{command} on #{dbname} from directory #{dirname}."
62
-
63
- require File.expand_path(File.dirname(__FILE__)) + '/../couchrest'
64
- require 'fileutils'
65
-
66
- module Enumerable
67
- def group_by
68
- inject({}) do |groups, element|
69
- (groups[yield(element)] ||= []) << element
70
- groups
71
- end
72
- end if RUBY_VERSION < '1.9'
73
- end
74
-
75
- # connect to couchdb
76
- cr = CouchRest.new("http://localhost:5984")
77
- db = cr.database(dbname)
78
-
79
- def readjs(file, libs=nil)
80
- st = open(file).read
81
- st.sub!(/\/\/include-lib/,libs) if libs
82
- st
83
- end
84
-
85
- case command
86
- when "push" # files to views
87
- views = {}
88
- viewfiles = Dir.glob(File.join(dirname,"**","*.js")) # todo support non-js views
89
- libfiles = viewfiles.select{|f|/lib\.js/.match(f)}
90
- libs = open(libfiles[0]).read if libfiles[0]
91
- all = (viewfiles-libfiles).collect do |file|
92
- fileparts = file.split('/')
93
- filename = /(\w.*)-(\w.*)\.js/.match file.split('/').pop
94
- design = fileparts[1]
95
- view = filename[1]
96
- func = filename[2]
97
- path = file
98
- [design,view,func,path]
99
- end
100
- designs = all.group_by{|f|f[0]}
101
- designs.each do |design,parts|
102
- # puts "replace _design/#{design}? (enter to proceed, 'n' to skip)"
103
- # rep = $stdin.gets.chomp
104
- # next if rep == 'n'
105
- dviews = {}
106
- parts.group_by{|p|p[1]}.each do |view,fs|
107
- fs.each do |f|
108
- dviews["#{view}-reduce"] ||= {}
109
- dviews["#{view}-reduce"][f[2]] = readjs(f.last,libs)
110
- end
111
- dviews["#{view}-map"] = {'map' => dviews["#{view}-reduce"]['map']}
112
- dviews.delete("#{view}-reduce") unless dviews["#{view}-reduce"]["reduce"]
113
- end
114
- # save them to the db
115
- begin
116
- view = db.get("_design/#{design}")
117
- rescue RestClient::Request::RequestFailed
118
- view = nil
119
- end
120
- if (view && view['views'] == dviews)
121
- puts "no change to _design/#{design}. skipping..."
122
- else
123
- puts "replacing _design/#{design}"
124
- db.delete(view) rescue nil
125
- db.save({
126
- "_id" => "_design/#{design}",
127
- :views => dviews
128
- })
129
- end
130
- end
131
-
132
- when "pull" # views to files
133
- ds = db.documents(:startkey => '_design/', :endkey => '_design/ZZZZZZZZZ')
134
- ds['rows'].collect{|r|r['id']}.each do |id|
135
- puts directory = id.split('/').last
136
- FileUtils.mkdir_p(File.join("views",directory))
137
- views = db.get(id)['views']
138
-
139
- vgroups = views.keys.group_by{|k|k.sub(/\-(map|reduce)$/,'')}
140
- vgroups.each do|g,vs|
141
- mapname = vs.find {|v|views[v]["map"]}
142
- if mapname
143
- # save map
144
- mapfunc = views[mapname]["map"]
145
- mapfile = File.join(dirname,directory,"#{g}-map.js") # todo support non-js views
146
- File.open(mapfile,'w') do |f|
147
- f.write mapfunc
148
- end
149
- end
150
-
151
- reducename = vs.find {|v|views[v]["reduce"]}
152
- if reducename
153
- # save reduce
154
- reducefunc = views[reducename]["reduce"]
155
- reducefile = File.join(dirname,directory,"#{g}-reduce.js") # todo support non-js views
156
- File.open(reducefile,'w') do |f|
157
- f.write reducefunc
158
- end
159
- end
160
- end
161
- end
162
- end