├── .bnsignore ├── .gitignore ├── Gemfile ├── Gemfile.lock ├── History.txt ├── README.rdoc ├── Rakefile ├── VERSION ├── bin └── couch-docs ├── couch_docs.gemspec ├── fixtures ├── _design │ ├── __lib │ │ └── foo.js │ ├── a │ │ ├── b │ │ │ ├── c.js │ │ │ └── d.js │ │ └── e.json │ ├── j │ │ └── q.json │ └── x │ │ └── z.js ├── bar.json ├── baz_with_attachments.json ├── baz_with_attachments │ └── spacer.gif └── foo.json ├── lib ├── couch_docs.rb └── couch_docs │ ├── command_line.rb │ ├── design_directory.rb │ ├── document_directory.rb │ ├── store.rb │ └── version.rb ├── spec ├── couch_docs │ ├── command_line_spec.rb │ ├── design_directory_spec.rb │ ├── document_directory_spec.rb │ └── store_spec.rb ├── couch_docs_spec.rb ├── spec.opts └── spec_helper.rb └── test └── test_couch_docs.rb /.bnsignore: -------------------------------------------------------------------------------- 1 | # The list of files that should be ignored by Mr Bones. 2 | # Lines that start with '#' are comments. 3 | # 4 | # A .gitignore file can be used instead by setting it as the ignore 5 | # file in your Rakefile: 6 | # 7 | # PROJ.ignore_file = '.gitignore' 8 | # 9 | # For a project with a C extension, the following would be a good set of 10 | # exclude patterns (uncomment them if you want to use them): 11 | # *.[oa] 12 | # *~ 13 | announcement.txt 14 | coverage 15 | doc 16 | pkg 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pkg/* 2 | *.gem 3 | .bundle 4 | .rvmrc 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in couch_docs.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | couch_docs (1.3.2) 5 | directory_watcher (~> 1.3.0) 6 | json (~> 1.8.0) 7 | mime-types (~> 1.16) 8 | rest-client (~> 1.6.0) 9 | 10 | GEM 11 | remote: http://rubygems.org/ 12 | specs: 13 | directory_watcher (1.3.2) 14 | json (1.8.1) 15 | mime-types (1.25.1) 16 | rest-client (1.6.7) 17 | mime-types (>= 1.16) 18 | rspec (1.3.1) 19 | 20 | PLATFORMS 21 | ruby 22 | 23 | DEPENDENCIES 24 | couch_docs! 25 | rspec (~> 1.3.0) 26 | -------------------------------------------------------------------------------- /History.txt: -------------------------------------------------------------------------------- 1 | == 1.3.1 / 2011-01-02 2 | 3 | * Fix destructive pushes. 4 | 5 | 6 | == 1.3.0 / 2011-01-01 7 | 8 | * Attachment support 9 | 10 | * Attachments for the foo document would be stored in the foo sub-directory (the foo document itself is stored as foo.json). 11 | 12 | * Rudimentary mime-type support. 13 | 14 | * Works with dumping and pushing. 15 | 16 | * Minor bug fixes. 17 | 18 | == 1.2.1 / 2010-04-21 19 | 20 | * Update README to reflect changes in 1.2. No code changes. 21 | 22 | == 1.2.0 / 2010-03-29 23 | 24 | * Directory watcher only applies individual changes, not entire 25 | directory when individual changes are made. 26 | 27 | == 1.1.1 / 2010-03-15 28 | 29 | * Require RestClient 1.1. 30 | 31 | == 1.1.0 / 2010-03-13 32 | 33 | * Better command line experience. 34 | * Default to current directory. 35 | * Print help without args / better format (optparse). 36 | * Support the !code macro from couchapp. 37 | * Support a flag (-d) to only work on design docs. 38 | * Can create the DB if it doesn't already exist 39 | * Command line can be used to watch for local changes to be pushed immediately to the CouchDB server. 40 | 41 | == 1.0.0 / 2009-08-09 42 | 43 | * Update the couch-docs script to be able to dump a CouchDB database 44 | to a local directory as well as uploading a local directory into a 45 | CouchDB database. 46 | 47 | * CouchDB revision numbers are stripped when dumping (to prevent 48 | conflicts when re-loading) 49 | 50 | * Attachments are dumped as well. 51 | 52 | == 0.9.0 / 2009-08-08 53 | 54 | * Import from couch_design_docs (name change to reflect increased functionality) 55 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = NAME 2 | 3 | couch_docs - Manage CouchDB views and documents from the filesystem. 4 | 5 | Author: Chris Strom 6 | 7 | http://github.com/eee-c/couch_docs 8 | 9 | (formerly couch_design_docs) 10 | 11 | = DESCRIPTION 12 | 13 | I have two primary use cases: uploading fixture data for integration testing and editing CouchDB documents in my favorite editor. It could also be useful as a cheap back-up mechanism, though it likely would not work well with large datasets (see PROBLEMS/FUTURE). 14 | 15 | == Fixture Strategy 16 | 17 | Before an integration test, I create a new CouchDB database with a randomly generated name. I use couch_docs to upload fixture data to that database: 18 | 19 | CouchDocs.put_dir(DB_URL, DIRECTORY) 20 | 21 | Then, I allow the test run to proceed normally. At the end of the run, I teardown the test database. 22 | 23 | Since CouchDB database creation / destruction is so cheap, this allows for quite fast integration tests -- even including necessary design documents. 24 | 25 | == Editing with Emacs 26 | 27 | I much prefer editing JSON documents in Emacs (lesser editors should work) over using the Futon web interface built into CouchDB. To initiate this, I watch the directory containing the JSON document(s): 28 | 29 | couch-docs push -w localhost:5984/db 30 | 31 | The -w option uses directory watcher to watch for any changes on the file system. When a change occurs, the updated document is loaded to CouchDB immediately. This is very useful for rapid editing/prototyping of documents that are subsequently used in another medium (e.g. for a blog post). 32 | 33 | = FEATURES 34 | 35 | * Upload JSON documents stored on the filesystem into a CouchDB 36 | database. 37 | 38 | * Map .js files stored on the filesystem 39 | (e.g. _design/recipes/count_by_month/map.js) into CouchDB 40 | design documents. 41 | 42 | * Support for the !code macro from couchapp (useful for 43 | DRYing up map/reduce views as well as list/show documents. 44 | 45 | * Dump documents stored in CouchDB to the filesystem. 46 | 47 | * Attachments are stored as real files (not inline, mime64 encoded attributes on the JSON document) 48 | 49 | * De-resolution of !code macros. 50 | 51 | * Command line script (couch-docs) to push / dump CouchDB database. 52 | 53 | * Multiple options including a directory watcher for uploading 54 | directory changes 55 | 56 | = SYNOPSIS 57 | 58 | From the command line: 59 | 60 | # For dumping the contents of a CouchDB database to the filesystem 61 | couch-docs dump http://localhost:5984/db 62 | 63 | # For loading documents from the filesystem into CouchDB 64 | couch-docs push http://localhost:5984/db 65 | 66 | 67 | In code: 68 | 69 | DB_URL = "http://localhost:5984/db" 70 | DIRECTORY = "/repos/db/couchdb/" 71 | 72 | # /repos/db/couchdb/_design/lucene/transform.js 73 | # /repos/db/couchdb/foo.json 74 | 75 | CouchDocs.put_dir(DB_URL, DIRECTORY) 76 | 77 | # => lucene design document with a "transform" function containing 78 | # the contents of transform.js 79 | # - AND - 80 | # a document named "foo" with the JSON contents from the foo.json 81 | # file 82 | 83 | CouchDocs.dump(DB_URL, "/repos/db/bak") 84 | 85 | # => JSON dump of every document at DB_URL 86 | 87 | = REQUIREMENTS 88 | 89 | * CouchDB 90 | * JSON 91 | * RestClient 92 | * DirectoryWatcher 93 | 94 | = INSTALL 95 | 96 | * sudo gem install couch_docs 97 | 98 | = PROBLEMS/FUTURE 99 | 100 | * Does not honor CouchDB document revisions. Instead it deletes previous revisions and uploads an entirely new one. 101 | 102 | * A progress bar would be helpful. 103 | 104 | * Unit testing of view javascript would be very nice. 105 | 106 | * Will almost certainly not work for large datasets (>10k documents). 107 | 108 | = LICENSE 109 | 110 | (The MIT License) 111 | 112 | Copyright (c) 2010 113 | 114 | Permission is hereby granted, free of charge, to any person obtaining 115 | a copy of this software and associated documentation files (the 116 | 'Software'), to deal in the Software without restriction, including 117 | without limitation the rights to use, copy, modify, merge, publish, 118 | distribute, sublicense, and/or sell copies of the Software, and to 119 | permit persons to whom the Software is furnished to do so, subject to 120 | the following conditions: 121 | 122 | The above copyright notice and this permission notice shall be 123 | included in all copies or substantial portions of the Software. 124 | 125 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 126 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 127 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 128 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 129 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 130 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 131 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 132 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler::GemHelper.install_tasks 3 | 4 | require 'spec/rake/spectask' 5 | Spec::Rake::SpecTask.new(:spec) do |spec| 6 | spec.libs << 'lib' << 'spec' 7 | spec.spec_files = FileList['spec/**/*_spec.rb'] 8 | end 9 | 10 | task :default => :spec 11 | 12 | require 'rdoc/task' 13 | Rake::RDocTask.new do |rdoc| 14 | version = File.exist?('VERSION') ? File.read('VERSION') : "" 15 | 16 | rdoc.rdoc_dir = 'rdoc' 17 | rdoc.title = "couch_docs #{version}" 18 | rdoc.rdoc_files.include('README*') 19 | rdoc.rdoc_files.include('lib/**/*.rb') 20 | end 21 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.3.2 2 | -------------------------------------------------------------------------------- /bin/couch-docs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'rubygems' 4 | 5 | require File.expand_path( 6 | File.join(File.dirname(__FILE__), %w[.. lib couch_docs])) 7 | 8 | # Put your code here 9 | 10 | CouchDocs::CommandLine.run ARGV 11 | 12 | # EOF 13 | -------------------------------------------------------------------------------- /couch_docs.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "couch_docs/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "couch_docs" 7 | s.version = CouchDocs::VERSION 8 | s.platform = Gem::Platform::RUBY 9 | s.authors = ["Chris Strom"] 10 | s.email = ["chris@eeecooks.com"] 11 | s.homepage = "http://github.com/eee-c/couch_docs" 12 | s.summary = %q{Manage CouchDB views and documents} 13 | s.description = %q{Manage CouchDB views and documents.} 14 | 15 | s.rubyforge_project = "couch_docs" 16 | 17 | s.files = `git ls-files`.split("\n") 18 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 19 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 20 | s.require_paths = ["lib"] 21 | 22 | s.add_development_dependency "rspec", ["~> 1.3.0"] 23 | 24 | s.add_runtime_dependency(%q, ["~> 1.6.0"]) 25 | s.add_runtime_dependency(%q, ["~> 1.8.0"]) 26 | s.add_runtime_dependency(%q, ["~> 1.3.0"]) 27 | s.add_runtime_dependency(%q, ["~> 1.16"]) 28 | end 29 | -------------------------------------------------------------------------------- /fixtures/_design/__lib/foo.js: -------------------------------------------------------------------------------- 1 | function foo () { return "foo"; } 2 | -------------------------------------------------------------------------------- /fixtures/_design/a/b/c.js: -------------------------------------------------------------------------------- 1 | function(doc) { return true; } -------------------------------------------------------------------------------- /fixtures/_design/a/b/d.js: -------------------------------------------------------------------------------- 1 | function(doc) { return true; } -------------------------------------------------------------------------------- /fixtures/_design/a/e.json: -------------------------------------------------------------------------------- 1 | [{"one": "2"}] 2 | -------------------------------------------------------------------------------- /fixtures/_design/j/q.json: -------------------------------------------------------------------------------- 1 | ["!code foo.js"] 2 | -------------------------------------------------------------------------------- /fixtures/_design/x/z.js: -------------------------------------------------------------------------------- 1 | // !code foo.js 2 | function bar () { return "bar"; } 3 | -------------------------------------------------------------------------------- /fixtures/bar.json: -------------------------------------------------------------------------------- 1 | {"bar":"2"} -------------------------------------------------------------------------------- /fixtures/baz_with_attachments.json: -------------------------------------------------------------------------------- 1 | {"baz":"3"} 2 | -------------------------------------------------------------------------------- /fixtures/baz_with_attachments/spacer.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eee-c/couch_docs/bb97eb57e4a6110172e7cf34ecc723deb89bcecb/fixtures/baz_with_attachments/spacer.gif -------------------------------------------------------------------------------- /fixtures/foo.json: -------------------------------------------------------------------------------- 1 | {"foo":"1"} -------------------------------------------------------------------------------- /lib/couch_docs.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | 3 | module CouchDocs 4 | 5 | # :stopdoc: 6 | LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR 7 | PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR 8 | # :startdoc: 9 | 10 | # Returns the version string for the library. 11 | # 12 | def self.version 13 | VERSION 14 | end 15 | 16 | # For a CouchDB database described by db_uri and a 17 | # directory, dir containing design documents, creates 18 | # design documents in the CouchDB database 19 | # 20 | def self.put_dir(db_uri, dir) 21 | self.put_design_dir(db_uri, "#{dir}/_design") 22 | self.put_document_dir(db_uri, dir) 23 | end 24 | 25 | # Alias for put_dir 26 | def self.upload_dir(db_uri, dir) 27 | self.put_dir(db_uri, dir) 28 | end 29 | 30 | # Upload design documents from dir to the CouchDB database 31 | # located at db_uri 32 | # 33 | def self.put_design_dir(db_uri, dir) 34 | store = Store.new(db_uri) 35 | dir = DesignDirectory.new(dir) 36 | store.put_design_documents(dir.to_hash) 37 | end 38 | 39 | # Upload documents from dir to the CouchDB database 40 | # located at db_uri 41 | # 42 | def self.put_document_dir(db_uri, dir) 43 | dir = DocumentDirectory.new(dir) 44 | dir.each_document do |name, contents| 45 | Store.put!("#{db_uri}/#{name}", contents) 46 | end 47 | end 48 | 49 | 50 | # Upload a document located at file_path to the CouchDB database 51 | # located at db_uri 52 | # 53 | def self.put_file(db_uri, file_path) 54 | contents = JSON.parse(File.read(file_path)) 55 | name = File.basename(file_path, ".json") 56 | Store.put!("#{db_uri}/#{name}", contents) 57 | end 58 | 59 | # Dump all documents located at db_uri into the directory 60 | # dir 61 | # 62 | def self.dump(db_uri, dir, only=nil) 63 | null_dir = OpenStruct.new(:store_document => nil) 64 | 65 | doc_dir = (only == :design) ? 66 | null_dir : DocumentDirectory.new(dir) 67 | design_dir = (only == :doc) ? 68 | null_dir : DesignDirectory.new(dir) 69 | 70 | store = Store.new(db_uri, :only => only) 71 | store.map.each do |doc| 72 | doc.delete('_rev') 73 | (doc['_id'] =~ /^_design/ ? design_dir : doc_dir). 74 | store_document(doc) 75 | end 76 | end 77 | 78 | # Create or recreate the database located at db_uri 79 | def self.destructive_database_create(db_uri) 80 | Store.put!(db_uri, "") 81 | end 82 | 83 | # Returns the library path for the module. If any arguments are given, 84 | # they will be joined to the end of the libray path using 85 | # File.join. 86 | # 87 | def self.libpath( *args ) 88 | args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten) 89 | end 90 | 91 | # Returns the lpath for the module. If any arguments are given, 92 | # they will be joined to the end of the path using 93 | # File.join. 94 | # 95 | def self.path( *args ) 96 | args.empty? ? PATH : ::File.join(PATH, args.flatten) 97 | end 98 | 99 | # Utility method used to require all files ending in .rb that lie in the 100 | # directory below this file that has the same name as the filename passed 101 | # in. Optionally, a specific _directory_ name can be passed in such that 102 | # the _filename_ does not have to be equivalent to the directory. 103 | # 104 | def self.require_all_libs_relative_to( fname, dir = nil ) 105 | dir ||= ::File.basename(fname, '.*') 106 | search_me = ::File.expand_path( 107 | ::File.join(::File.dirname(fname), dir, '**', '*.rb')) 108 | 109 | Dir.glob(search_me).sort.each {|rb| require rb} 110 | end 111 | 112 | end # module CouchDocs 113 | 114 | CouchDocs.require_all_libs_relative_to(__FILE__) 115 | 116 | # EOF 117 | -------------------------------------------------------------------------------- /lib/couch_docs/command_line.rb: -------------------------------------------------------------------------------- 1 | require 'optparse' 2 | require 'pp' 3 | 4 | require 'rubygems' 5 | require 'directory_watcher' 6 | 7 | module CouchDocs 8 | class CommandLine 9 | COMMANDS = %w{push dump} 10 | DEPRECATED_COMMANDS = %w{load} 11 | 12 | def self.run(*args) 13 | CommandLine.new(*args).run 14 | end 15 | 16 | attr_reader :command, :options 17 | 18 | def initialize(args) 19 | parse_options(args) 20 | end 21 | 22 | def run 23 | case command 24 | when "dump" 25 | CouchDocs.dump(@options[:couchdb_url], 26 | @options[:target_dir], 27 | @options[:dump]) 28 | when "push" 29 | if @options[:destructive] 30 | CouchDocs.destructive_database_create(options[:couchdb_url]) 31 | end 32 | 33 | dw = DirectoryWatcher.new @options[:target_dir] 34 | dw.glob = '**/*' 35 | dw.interval = 2.0 36 | 37 | dw.add_observer do |*args| 38 | puts "Updating documents on CouchDB Server..." 39 | directory_watcher_update(args) 40 | end 41 | 42 | if @options[:watch] 43 | dw.start 44 | 45 | begin 46 | sleep 30 while active? 47 | rescue Interrupt 48 | dw.stop 49 | puts 50 | end 51 | else 52 | dw.run_once 53 | end 54 | when "load" # DEPRECATED 55 | CouchDocs.put_dir(@options[:target_dir], 56 | @options[:couchdb_url]) 57 | end 58 | end 59 | 60 | def parse_options(args) 61 | @options = { :target_dir => "." } 62 | 63 | options_parser = OptionParser.new do |opts| 64 | opts.banner = "Usage: couch-docs push|dump [OPTIONS] couchdb_url [target_dir]" 65 | 66 | opts.separator "" 67 | opts.separator "If a target_dir is not specified, the current working directory will be used." 68 | 69 | opts.separator "" 70 | opts.separator "Push options:" 71 | 72 | opts.on("-R", "--destructive", 73 | "Drop the couchdb_uri (if it exists) and create a new database") do 74 | @options[:destructive] = true 75 | end 76 | 77 | # TODO: bulk_docs in 1.2 78 | # opts.on("-b", "--bulk [BATCH_SIZE=1000]", Integer, 79 | # "Use bulk insert when pushing new documents") do |batch_size| 80 | # @options[:bulk] = true 81 | # @options[:batch_size] = batch_size || 1000 82 | # end 83 | 84 | opts.on("-w", "--watch", "Watch the directory for changes, uploading when detected") do 85 | @options[:watch] = true 86 | end 87 | 88 | opts.separator "" 89 | opts.separator "Dump options:" 90 | 91 | opts.on("-d", "--design", "Only dump design documents") do 92 | @options[:dump] = :design 93 | end 94 | opts.on("-D", "--data", "Only dump data documents") do 95 | @options[:dump] = :doc 96 | end 97 | 98 | opts.separator "" 99 | opts.separator "Common options:" 100 | 101 | opts.on_tail("-v", "--version", "Show version") do 102 | puts File.basename($0) + " " + CouchDocs::VERSION 103 | exit 104 | end 105 | 106 | # No argument, shows at tail. This will print an options summary. 107 | # Try it and see! 108 | opts.on_tail("-h", "--help", "Show this message") do 109 | puts opts 110 | exit 111 | end 112 | end 113 | 114 | begin 115 | options_parser.parse!(args) 116 | additional_help = "#{options_parser.banner}\n\nTry --help for more options." 117 | unless (COMMANDS+DEPRECATED_COMMANDS).include? args.first 118 | puts "invalid command: \"#{args.first}\". Must be one of #{COMMANDS.join(', ')}.\n\n" 119 | puts additional_help 120 | exit 121 | end 122 | @command = args.shift 123 | unless args.first 124 | puts "Missing required couchdb_uri argument.\n\n" 125 | puts additional_help 126 | exit 127 | end 128 | @options[:couchdb_url] = args.shift 129 | @options[:target_dir] = args.shift if (args.size >= 1) 130 | 131 | rescue OptionParser::InvalidOption => e 132 | raise e 133 | end 134 | end 135 | 136 | def directory_watcher_update(args) 137 | if initial_add? args 138 | CouchDocs.put_dir(@options[:couchdb_url], 139 | @options[:target_dir]) 140 | else 141 | if design_doc_update? args 142 | CouchDocs.put_design_dir(@options[:couchdb_url], 143 | "#{@options[:target_dir]}/_design") 144 | end 145 | documents(args).each do |update| 146 | CouchDocs.put_file(@options[:couchdb_url], 147 | update.path) 148 | end 149 | end 150 | rescue RestClient::ResourceNotFound => e 151 | $stderr.puts "\n" 152 | $stderr.puts e.message 153 | $stderr.puts "Does the database exist? Try using the -R option." 154 | $stderr.puts "\n" 155 | rescue Exception => e 156 | $stderr.puts "\n" 157 | $stderr.puts e.message 158 | $stderr.puts e.backtrace 159 | end 160 | 161 | def initial_add?(args) 162 | args.all? { |f| f.type == :added } 163 | end 164 | 165 | def design_doc_update?(args) 166 | args.any? { |f| f.path =~ /_design/ } 167 | end 168 | 169 | def documents(args) 170 | args.reject { |f| f.path =~ /_design/ } 171 | end 172 | 173 | private 174 | def active?; true end 175 | end 176 | end 177 | -------------------------------------------------------------------------------- /lib/couch_docs/design_directory.rb: -------------------------------------------------------------------------------- 1 | class Hash 2 | def deep_merge(other) 3 | self.merge(other) do |key, oldval, newval| 4 | oldval.deep_merge(newval) 5 | end 6 | end 7 | end 8 | 9 | module CouchDocs 10 | class DesignDirectory 11 | 12 | attr_accessor :couch_view_dir 13 | 14 | def self.a_to_hash(a) 15 | key = a.first 16 | if (a.length > 2) 17 | { key => a_to_hash(a[1,a.length]) } 18 | else 19 | { key => a.last } 20 | end 21 | end 22 | 23 | def initialize(path) 24 | Dir.new(path) # Just checkin' 25 | @couch_view_dir = path 26 | end 27 | 28 | # Load 29 | 30 | def to_hash 31 | Dir["#{couch_view_dir}/**/*.{js,json}"].inject({}) do |memo, filename| 32 | DesignDirectory. 33 | a_to_hash(expand_file(filename)). 34 | deep_merge(memo) 35 | end 36 | end 37 | 38 | def expand_file(filename) 39 | if filename =~ /\.js$/ 40 | name_value_pair = [ 41 | File.basename(filename, '.js'), 42 | read_js_value(filename) 43 | ] 44 | elsif filename =~ /\.json$/ 45 | name_value_pair = [ 46 | File.basename(filename, '.json'), 47 | read_json_value(filename) 48 | ] 49 | end 50 | 51 | name_value_pair[0].gsub!(/%2F/, '/') 52 | 53 | File.dirname(filename). 54 | gsub(/#{couch_view_dir}\/?/, ''). 55 | split(/\//) + name_value_pair 56 | end 57 | 58 | def read_json_value(filename) 59 | JSON.parse(File.new(filename).read) 60 | end 61 | 62 | def read_js_value(filename) 63 | File. 64 | readlines(filename). 65 | map { |line| process_code_macro(line) }. 66 | join 67 | end 68 | 69 | def process_code_macro(line) 70 | if line =~ %r{\s*//\s*!code\s*(\S+)\s*} 71 | "// !begin code #{$1}\n" + 72 | read_from_lib($1) + 73 | "// !end code #{$1}\n" 74 | else 75 | line 76 | end 77 | end 78 | 79 | def read_from_lib(path) 80 | File.read("#{couch_view_dir}/__lib/#{path}") 81 | end 82 | 83 | # Store 84 | 85 | def store_document(doc) 86 | id = doc['_id'] 87 | self.save_js(nil, id, doc) 88 | end 89 | 90 | def save_js(rel_path, key, value) 91 | if value.is_a? Hash 92 | save_js_hash(rel_path, key, value) 93 | else 94 | save_js_value(rel_path, key, value) 95 | end 96 | end 97 | 98 | def remove_code_macros(js) 99 | js =~ %r{// !begin code ([.\w]+)$}m 100 | lib = $1 101 | if lib and js =~ %r{// !end code #{lib}$}m 102 | remove_code_macros(js.sub(%r{// !begin code #{lib}.+// !end code #{lib}}m, "// !code #{lib}")) 103 | else 104 | js 105 | end 106 | end 107 | 108 | private 109 | def save_js_hash(rel_path, id, hash) 110 | hash.each_pair do |k, v| 111 | next if k == '_id' 112 | self.save_js([rel_path, id].compact.join('/'), k, v) 113 | end 114 | end 115 | 116 | def save_js_value(rel_path, id, value) 117 | ext = value.is_a?(String) ? "js" : "json" 118 | value = value.is_a?(String) ? remove_code_macros(value) : value.to_json 119 | 120 | 121 | path = couch_view_dir + '/' + rel_path 122 | FileUtils.mkdir_p(path) 123 | 124 | file = File.new("#{path}/#{id.gsub(/\//, '%2F')}.#{ext}", "w+") 125 | file.write(value) 126 | file.close 127 | end 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /lib/couch_docs/document_directory.rb: -------------------------------------------------------------------------------- 1 | require 'base64' 2 | 3 | module CouchDocs 4 | class DocumentDirectory 5 | 6 | attr_accessor :couch_doc_dir 7 | 8 | def initialize(path) 9 | Dir.new(path) 10 | @couch_doc_dir = path 11 | end 12 | 13 | def each_document 14 | Dir["#{couch_doc_dir}/*.json"].each do |filename| 15 | id = File.basename(filename, '.json') 16 | json = JSON.parse(File.new(filename).read) 17 | 18 | if File.directory? "#{couch_doc_dir}/#{id}" 19 | json["_attachments"] ||= { } 20 | Dir["#{couch_doc_dir}/#{id}/*"].each do |attachment| 21 | next unless File.file? attachment 22 | attachment_name = File.basename(attachment) 23 | json["_attachments"][attachment_name] = file_as_attachment(attachment) 24 | end 25 | end 26 | 27 | yield [ id, json ] 28 | end 29 | end 30 | 31 | def store_document(doc) 32 | file = File.new("#{couch_doc_dir}/#{doc['_id']}.json", "w+") 33 | store_attachments(doc['_id'], doc.delete('_attachments')) 34 | file.write(doc.to_json) 35 | file.close 36 | end 37 | 38 | def file_as_attachment(file) 39 | type = mime_type(file) 40 | data = File.read(file) 41 | 42 | attachment = { 43 | "data" => Base64.encode64(data).gsub(/\n/, '') 44 | } 45 | if type 46 | attachment.merge!({"content_type" => type}) 47 | end 48 | 49 | attachment 50 | end 51 | 52 | def store_attachments(id, attachments) 53 | return unless attachments 54 | 55 | make_attachment_dir(id) 56 | attachments.each do |filename, opts| 57 | save_attachment(id, filename, opts['data']) 58 | end 59 | end 60 | 61 | def make_attachment_dir(id) 62 | FileUtils.mkdir_p "#{couch_doc_dir}/#{id}" 63 | end 64 | 65 | def save_attachment(id, filename, data) 66 | file = File.new "#{couch_doc_dir}/#{id}/#{filename}", "w" 67 | file.write Base64.decode64(data) 68 | file.close 69 | end 70 | 71 | private 72 | def mime_type(file) 73 | type = MIME::Types.type_for(file).first.to_s 74 | (type && type != '') ? type : nil 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/couch_docs/store.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'restclient' 3 | require 'json' 4 | 5 | module CouchDocs 6 | class Store 7 | include Enumerable 8 | 9 | attr_accessor :url, :design_docs_only 10 | 11 | # Initialize a CouchDB store object. Requires a URL for the 12 | # target CouchDB database. 13 | # 14 | def initialize(url, options={}) 15 | @url = url 16 | @design_docs_only = (options[:only] == :design) 17 | end 18 | 19 | # Loads all supplied design documents in the current store. 20 | # Given a hash h, the keys being the CouchDB document 21 | # name and values of design documents 22 | # 23 | def put_design_documents(h) 24 | h.each_pair do |document_name, doc| 25 | Store.put!("#{url}/_design/#{document_name}", doc) 26 | end 27 | end 28 | 29 | # Create or replace the document located at path with the 30 | # Hash document doc 31 | # 32 | def self.put!(path, doc) 33 | self.put(path, doc) 34 | rescue RestClient::RequestFailed 35 | self.delete_and_put(path, doc) 36 | end 37 | 38 | def self.delete_and_put(path, doc) 39 | self.delete(path) 40 | self.put(path, doc) 41 | end 42 | 43 | def self.put(path, doc) 44 | RestClient.put path, 45 | doc.to_json, 46 | :content_type => 'application/json' 47 | end 48 | 49 | def self.post(path, doc) 50 | RestClient.post path, 51 | doc.to_json, 52 | :content_type => 'application/json' 53 | end 54 | 55 | def self.delete(path) 56 | # retrieve existing to obtain the revision 57 | old = self.get(path) 58 | url = old['_rev'] ? path + "?rev=#{old['_rev']}" : path 59 | RestClient.delete(url) 60 | end 61 | 62 | def self.get(path) 63 | JSON.parse(RestClient.get(path)) 64 | end 65 | 66 | def each 67 | all_url = "#{url}/_all_docs" + 68 | (design_docs_only ? '?startkey=%22_design%22&endkey=%22_design0%22' : "") 69 | Store.get(all_url)['rows'].each do |rec| 70 | yield Store.get("#{url}/#{rec['id']}?attachments=true") 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/couch_docs/version.rb: -------------------------------------------------------------------------------- 1 | module CouchDocs 2 | VERSION = "1.3.2" 3 | end 4 | -------------------------------------------------------------------------------- /spec/couch_docs/command_line_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper')) 2 | 3 | describe CouchDocs::CommandLine do 4 | it "should be able to run a single instance of a command line" do 5 | CouchDocs::CommandLine. 6 | should_receive(:new). 7 | with('foo', 'bar'). 8 | and_return(mock("Command Line").as_null_object) 9 | 10 | CouchDocs::CommandLine.run('foo', 'bar') 11 | end 12 | 13 | it "should run the command line instance" do 14 | command_line = mock("Command Line").as_null_object 15 | command_line. 16 | should_receive(:run) 17 | 18 | CouchDocs::CommandLine.stub!(:new).and_return(command_line) 19 | 20 | CouchDocs::CommandLine.run('foo', 'bar') 21 | end 22 | 23 | context "an instance that dumps a CouchDB database" do 24 | it "should dump CouchDB documents from uri to dir when run" do 25 | @it = CouchDocs::CommandLine.new(['dump', 'uri', 'dir']) 26 | 27 | CouchDocs. 28 | should_receive(:dump). 29 | with("uri", "dir", nil) 30 | 31 | @it.run 32 | end 33 | 34 | it "should be able to dump only design documents" do 35 | @it = CouchDocs::CommandLine.new(['dump', 'uri', 'dir', '-d']) 36 | 37 | CouchDocs. 38 | should_receive(:dump). 39 | with("uri", "dir", :design) 40 | 41 | @it.run 42 | end 43 | 44 | it "should be able to dump only regular documents" do 45 | @it = CouchDocs::CommandLine.new(['dump', 'uri', 'dir', '-D']) 46 | 47 | CouchDocs. 48 | should_receive(:dump). 49 | with("uri", "dir", :doc) 50 | 51 | @it.run 52 | end 53 | 54 | it "should be an initial add if everything is an add" do 55 | @it = CouchDocs::CommandLine.new(['push', 'uri']) 56 | args = [mock(:type => :added), 57 | mock(:type => :added)] 58 | @it.should be_initial_add(args) 59 | end 60 | 61 | it "should not be an initial add if something is not an add" do 62 | @it = CouchDocs::CommandLine.new(['push', 'uri']) 63 | args = [mock(:type => :foo), 64 | mock(:type => :added)] 65 | @it.should_not be_initial_add(args) 66 | end 67 | 68 | it "should be a design docs update if something changes in _design" do 69 | @it = CouchDocs::CommandLine.new(['push', 'uri']) 70 | args = [mock(:path => "foo"), 71 | mock(:path => "_design")] 72 | @it.should be_design_doc_update(args) 73 | end 74 | 75 | it "should know document updates" do 76 | @it = CouchDocs::CommandLine.new(['push', 'uri']) 77 | doc_update = mock(:path => "foo") 78 | args = [doc_update, 79 | mock(:path => "_design")] 80 | 81 | @it. 82 | documents(args). 83 | should == [doc_update] 84 | end 85 | 86 | 87 | context "updates on the filesystem" do 88 | before(:each) do 89 | @args = mock("args") 90 | @it = CouchDocs::CommandLine.new(%w(push uri dir)) 91 | end 92 | it "should only update design docs if only local design docs have changed" do 93 | CouchDocs. 94 | should_receive(:put_dir) 95 | 96 | @it.stub!(:initial_add?).and_return(true) 97 | @it.directory_watcher_update(@args) 98 | end 99 | context "not an inital add" do 100 | before(:each) do 101 | @it.stub!(:initial_add?).and_return(false) 102 | @it.stub!(:design_doc_update?).and_return(false) 103 | @it.stub!(:documents).and_return([]) 104 | CouchDocs.stub!(:put_design_dir) 105 | end 106 | it "should update design docs if there are design document updates" do 107 | CouchDocs. 108 | should_receive(:put_design_dir) 109 | 110 | @it.stub!(:design_doc_update?).and_return(true) 111 | @it.directory_watcher_update(@args) 112 | end 113 | it "should update documents (if any)" do 114 | file_mock = mock("File", :path => "/foo") 115 | @it.stub!(:documents).and_return([file_mock]) 116 | 117 | CouchDocs. 118 | should_receive(:put_file). 119 | with("uri", "/foo") 120 | 121 | @it.directory_watcher_update(@args) 122 | end 123 | end 124 | end 125 | end 126 | 127 | context "pushing" do 128 | before(:each) do 129 | CouchDocs.stub!(:put_dir) 130 | 131 | @dw = mock("Directory Watcher").as_null_object 132 | DirectoryWatcher.stub!(:new).and_return(@dw) 133 | end 134 | 135 | it "should know watch" do 136 | @it = CouchDocs::CommandLine.new(%w(push uri dir -w)) 137 | @it.options[:watch].should be_true 138 | end 139 | 140 | it "should run once normally" do 141 | @dw.should_receive(:run_once) 142 | 143 | @it = CouchDocs::CommandLine.new(%w(push uri dir)) 144 | @it.run 145 | end 146 | 147 | it "should start a watcher with -w" do 148 | @dw.should_receive(:start) 149 | 150 | @it = CouchDocs::CommandLine.new(%w(push uri dir -w)) 151 | @it.stub!(:active?).and_return(false) 152 | @it.run 153 | end 154 | end 155 | 156 | context "an instance that uploads to a CouchDB database" do 157 | before(:each) do 158 | @it = CouchDocs::CommandLine.new(['load', 'dir', 'uri']) 159 | end 160 | 161 | it "should load CouchDB documents from dir to uri when run" do 162 | CouchDocs. 163 | should_receive(:put_dir). 164 | with("uri", "dir") 165 | 166 | @it.run 167 | end 168 | end 169 | 170 | end 171 | -------------------------------------------------------------------------------- /spec/couch_docs/design_directory_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper')) 2 | 3 | describe CouchDocs::DesignDirectory do 4 | it "should require a root directory for instantiation" do 5 | lambda { CouchDocs::DesignDirectory.new }. 6 | should raise_error 7 | 8 | lambda { CouchDocs::DesignDirectory.new("foo") }. 9 | should raise_error 10 | 11 | lambda { CouchDocs::DesignDirectory.new("fixtures/_design")}. 12 | should_not raise_error 13 | end 14 | 15 | it "should convert arrays into deep hashes" do 16 | CouchDocs::DesignDirectory. 17 | a_to_hash(%w{a b c d}). 18 | should == { 19 | 'a' => { 20 | 'b' => { 21 | 'c' => 'd' 22 | } 23 | } 24 | } 25 | end 26 | 27 | context "a valid directory" do 28 | before(:each) do 29 | @it = CouchDocs::DesignDirectory.new("fixtures/_design") 30 | end 31 | 32 | it "should list dirs, basename and contents of a js file" do 33 | @it.expand_file("fixtures/_design/a/b/c.js"). 34 | should == ['a', 'b', 'c', 'function(doc) { return true; }'] 35 | end 36 | 37 | it "should list dirs, basename and contents of a json file" do 38 | @it.expand_file("fixtures/_design/a/e.json"). 39 | should == ['a', 'e', [{"one" => "2"}]] 40 | end 41 | 42 | it "should assemble all documents into a single docs structure" do 43 | @it.to_hash['a']. 44 | should == { 45 | 'b' => { 46 | 'c' => 'function(doc) { return true; }', 47 | 'd' => 'function(doc) { return true; }' 48 | }, 49 | 'e' => [{"one" => "2"}] 50 | } 51 | end 52 | 53 | it "should process code macros when assembling" do 54 | @it.to_hash['x']. 55 | should == { 56 | 'z' => 57 | "// !begin code foo.js\n" + 58 | "function foo () { return \"foo\"; }\n" + 59 | "// !end code foo.js\n" + 60 | "function bar () { return \"bar\"; }\n" 61 | } 62 | end 63 | 64 | it "should ignore macro escape sequence when reading JSON" do 65 | @it.to_hash['j']. 66 | should == {'q' => ["!code foo.js"]} 67 | end 68 | 69 | it "should work with absolute !code paths" 70 | 71 | it "should replace !code macros with the contents of the referenced file in lib" do 72 | @it.stub!(:read_from_lib).and_return("awesome javascript") 73 | 74 | @it. 75 | process_code_macro(" // !code foo/bar.js "). 76 | should =~ /awesome javascript/ 77 | end 78 | 79 | it "should not affect normal lines when processing macros" do 80 | @it. 81 | process_code_macro(" var foo = 'bar'; "). 82 | should == " var foo = 'bar'; " 83 | end 84 | 85 | it "should find files with relative paths in __lib" do 86 | File. 87 | should_receive(:read). 88 | with("fixtures/_design/__lib/foo.js") 89 | 90 | @it.read_from_lib("foo.js") 91 | end 92 | 93 | end 94 | 95 | context "saving a JSON attribute" do 96 | before(:each) do 97 | @it = CouchDocs::DesignDirectory.new("/tmp") 98 | 99 | FileUtils.stub!(:mkdir_p) 100 | @file = mock("File").as_null_object 101 | File.stub!(:new).and_return(@file) 102 | end 103 | 104 | it "should not mangle json valued attributes" do 105 | @file. 106 | should_receive(:write). 107 | with(%{["bar","baz"]}) 108 | 109 | @it.save_js(nil, "_design/foo", { "foo" => ["bar","baz"] }) 110 | end 111 | 112 | it "should save in a .json file" do 113 | File. 114 | should_receive(:new). 115 | with("/tmp/_design/foo/foo.json", "w+"). 116 | and_return(@file) 117 | 118 | @it.save_js(nil, "_design/foo", { "foo" => ["bar","baz"] }) 119 | end 120 | end 121 | 122 | context "saving a JS attribute" do 123 | before(:each) do 124 | @it = CouchDocs::DesignDirectory.new("/tmp") 125 | 126 | FileUtils.stub!(:mkdir_p) 127 | @file = mock("File").as_null_object 128 | File.stub!(:new).and_return(@file) 129 | end 130 | 131 | it "should not store _id" do 132 | File. 133 | should_not_receive(:new). 134 | with("/tmp/_design/foo/_id.js", "w+") 135 | 136 | @it.save_js(nil, "_design/foo", { "_id" => "_design/foo"}) 137 | end 138 | 139 | it "should create map the design document attribute to the filesystem" do 140 | FileUtils. 141 | should_receive(:mkdir_p). 142 | with("/tmp/_design/foo") 143 | 144 | @it.save_js("_design/foo", "bar", "json") 145 | end 146 | 147 | it "should store the attribute to the filesystem" do 148 | File. 149 | should_receive(:new). 150 | with("/tmp/_design/foo/bar.js", "w+") 151 | 152 | @it.save_js("_design/foo", "bar", "json") 153 | end 154 | 155 | it "should store hash values to the filesystem" do 156 | File. 157 | should_receive(:new). 158 | with("/tmp/_design/foo/bar/baz.js", "w+") 159 | 160 | @it.save_js("_design/foo", "bar", { "baz" => "json" }) 161 | end 162 | 163 | it "should store the attribute to the filesystem" do 164 | @file. 165 | should_receive(:write). 166 | with("json") 167 | 168 | @it.save_js("_design/foo", "bar", "json") 169 | end 170 | 171 | it "should store the attributes with slashes to the filesystem" do 172 | File. 173 | should_receive(:new). 174 | with("/tmp/_design/foo/bar%2Fbaz.js", "w+") 175 | 176 | @it.save_js("_design/foo", "bar/baz", "json") 177 | end 178 | 179 | it "should strip lib code when dumping" do 180 | js = <<_JS 181 | // !begin code foo.js 182 | function foo () { return 'foo'; } 183 | // !end code foo.js 184 | // !begin code bar.js 185 | function bar () { return 'bar'; } 186 | // !end code bar.js 187 | function baz () { return 'baz'; } 188 | _JS 189 | 190 | @it. 191 | remove_code_macros(js). 192 | should == "// !code foo.js\n" + 193 | "// !code bar.js\n" + 194 | "function baz () { return 'baz'; }\n" 195 | end 196 | end 197 | end 198 | -------------------------------------------------------------------------------- /spec/couch_docs/document_directory_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper')) 2 | 3 | describe CouchDocs::DocumentDirectory do 4 | it "should require a root directory for instantiation" do 5 | lambda { CouchDocs::DocumentDirectory.new }. 6 | should raise_error 7 | 8 | lambda { CouchDocs::DocumentDirectory.new("foo") }. 9 | should raise_error 10 | 11 | lambda { CouchDocs::DocumentDirectory.new("fixtures")}. 12 | should_not raise_error 13 | end 14 | 15 | context "a valid directory" do 16 | before(:each) do 17 | @it = CouchDocs::DocumentDirectory.new("fixtures") 18 | end 19 | 20 | it "should be able to iterate over the documents" do 21 | everything = [] 22 | @it.each_document do |name, contents| 23 | everything << [name, contents] 24 | end 25 | everything. 26 | should include ['bar', {"bar" => "2"}] 27 | 28 | everything. 29 | should include ['foo', {"foo" => "1"}] 30 | end 31 | 32 | it "should be able to store a document" do 33 | file = mock("File", :write => 42, :close => true) 34 | File. 35 | should_receive(:new). 36 | with("fixtures/foo.json", "w+"). 37 | and_return(file) 38 | 39 | @it.store_document({'_id' => 'foo'}) 40 | end 41 | 42 | it "should be able to save a document as JSON" do 43 | file = mock("File", :close => true) 44 | File.stub!(:new).and_return(file) 45 | 46 | file.should_receive(:write).with(%Q|{"_id":"foo"}|) 47 | 48 | @it.store_document({'_id' => 'foo'}) 49 | end 50 | 51 | context "pushing attachments to CouchDB" do 52 | before(:each) do 53 | @spacer_b64 = "R0lGODlhAQABAPcAAAAAAIAAAACAAICAAAAAgIAAgACAgICAgMDAwP8AAAD/AP//AAAA//8A/wD//////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMwAAZgAAmQAAzAAA/wAzAAAzMwAzZgAzmQAzzAAz/wBmAABmMwBmZgBmmQBmzABm/wCZAACZMwCZZgCZmQCZzACZ/wDMAADMMwDMZgDMmQDMzADM/wD/AAD/MwD/ZgD/mQD/zAD//zMAADMAMzMAZjMAmTMAzDMA/zMzADMzMzMzZjMzmTMzzDMz/zNmADNmMzNmZjNmmTNmzDNm/zOZADOZMzOZZjOZmTOZzDOZ/zPMADPMMzPMZjPMmTPMzDPM/zP/ADP/MzP/ZjP/mTP/zDP//2YAAGYAM2YAZmYAmWYAzGYA/2YzAGYzM2YzZmYzmWYzzGYz/2ZmAGZmM2ZmZmZmmWZmzGZm/2aZAGaZM2aZZmaZmWaZzGaZ/2bMAGbMM2bMZmbMmWbMzGbM/2b/AGb/M2b/Zmb/mWb/zGb//5kAAJkAM5kAZpkAmZkAzJkA/5kzAJkzM5kzZpkzmZkzzJkz/5lmAJlmM5lmZplmmZlmzJlm/5mZAJmZM5mZZpmZmZmZzJmZ/5nMAJnMM5nMZpnMmZnMzJnM/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwAM8wAZswAmcwAzMwA/8wzAMwzM8wzZswzmcwzzMwz/8xmAMxmM8xmZsxmmcxmzMxm/8yZAMyZM8yZZsyZmcyZzMyZ/8zMAMzMM8zMZszMmczMzMzM/8z/AMz/M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8Amf8AzP8A//8zAP8zM/8zZv8zmf8zzP8z//9mAP9mM/9mZv9mmf9mzP9m//+ZAP+ZM/+ZZv+Zmf+ZzP+Z///MAP/MM//MZv/Mmf/MzP/M////AP//M///Zv//mf//zP///yH5BAEAABAALAAAAAABAAEAAAgEALkFBAA7" 54 | end 55 | 56 | it "should connect attachments by sub-directory name (foo.json => foo/)" do 57 | @it.stub!(:mime_type) 58 | 59 | everything = [] 60 | @it.each_document do |name, contents| 61 | everything << [name, contents] 62 | end 63 | 64 | everything. 65 | should include(['baz_with_attachments', 66 | {'baz' => '3', 67 | "_attachments" => { "spacer.gif" => {"data" => @spacer_b64} } }]) 68 | end 69 | it "should mime 64 encode attachments" do 70 | # covered above 71 | end 72 | it "should ignore non-file attachments" do 73 | # covered above 74 | end 75 | 76 | context "infering mime type" do 77 | before(:each) do 78 | File.stub!(:read).and_return("asdf") 79 | Base64.stub!(:encode64).and_return("asdf") 80 | end 81 | 82 | it "should guess gif mime type" do 83 | @it.file_as_attachment("spacer.gif"). 84 | should == { 85 | "data" => "asdf", 86 | "content_type" => "image/gif" 87 | } 88 | end 89 | 90 | it "should guess jpeg mime type" do 91 | @it.file_as_attachment("spacer.jpg"). 92 | should == { 93 | "data" => "asdf", 94 | "content_type" => "image/jpeg" 95 | } 96 | end 97 | 98 | it "should guess png mime type" do 99 | @it.file_as_attachment("spacer.png"). 100 | should == { 101 | "data" => "asdf", 102 | "content_type" => "image/png" 103 | } 104 | end 105 | 106 | it "should default to no mime type" do 107 | @it.file_as_attachment("spacer.foo"). 108 | should == { 109 | "data" => "asdf" 110 | } 111 | end 112 | end 113 | 114 | it "should give precedence to filesystem attachments" do 115 | @it.stub!(:mime_type) 116 | 117 | JSON.stub!(:parse). 118 | and_return({ "baz" => "3", 119 | "_attachments" => { 120 | "spacer.gif" => "asdf", 121 | "baz.jpg" => "asdf" 122 | } 123 | }) 124 | 125 | everything = [] 126 | @it.each_document do |name, contents| 127 | everything << [name, contents] 128 | end 129 | 130 | everything. 131 | should include(['baz_with_attachments', 132 | {'baz' => '3', 133 | "_attachments" => { "spacer.gif" => {"data" => @spacer_b64}, "baz.jpg" => "asdf" } }]) 134 | end 135 | 136 | end 137 | context "dump attachments from CouchDB" do 138 | before(:each) do 139 | FileUtils.stub!(:mkdir_p) 140 | end 141 | 142 | it "should store attachments" do 143 | file = mock("File").as_null_object 144 | File.stub!(:new).and_return(file) 145 | 146 | @it. 147 | should_receive(:store_attachments). 148 | with('foo', 'bar') 149 | 150 | @it.store_document({'_id' => 'foo', 151 | '_attachments' => 'bar'}) 152 | end 153 | 154 | context "storing attachments" do 155 | before(:each) do 156 | @attachments = { 'foo.txt' => { 'data' => 'attachment data' } } 157 | end 158 | 159 | it "should make a directory to hold the attachments" do 160 | @it. 161 | should_receive(:make_attachment_dir). 162 | with('foo') 163 | 164 | @it.stub!(:save_attachment) 165 | @it.store_attachments('foo', @attachments) 166 | end 167 | 168 | it "should create a sub-directory with document ID" do 169 | FileUtils. 170 | should_receive(:mkdir_p). 171 | with("fixtures/foo") 172 | 173 | @it.stub!(:save_attachment) 174 | @it.make_attachment_dir('foo') 175 | end 176 | 177 | it "should save attachments to the filesystem" do 178 | @it. 179 | should_receive(:save_attachment). 180 | with('foo', 'foo.txt', 'attachment data') 181 | 182 | @it.stub!(:save_attachment) 183 | @it.store_attachments('foo', @attachments) 184 | end 185 | 186 | it "should dump with native encoding (non-mime64)" do 187 | file = mock("File").as_null_object 188 | File.stub!(:new).and_return(file) 189 | 190 | file. 191 | should_receive(:write) 192 | 193 | @it.save_attachment('foo', 'foo.txt', 'ZGF0YQ==') 194 | end 195 | end 196 | 197 | it "should not include the attachments attribute" do 198 | file = mock("File", :close => true) 199 | File.stub!(:new).and_return(file) 200 | 201 | file. 202 | should_receive(:write). 203 | with('{"_id":"foo"}') 204 | 205 | @it.stub!(:store_attachments) 206 | 207 | @it.store_document({'_id' => 'foo', 208 | '_attachments' => 'foo'}) 209 | end 210 | end 211 | end 212 | end 213 | -------------------------------------------------------------------------------- /spec/couch_docs/store_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper')) 2 | 3 | describe CouchDocs::Store do 4 | it "should require a CouchDB URL Root for instantiation" do 5 | lambda { CouchDocs::Store.new }. 6 | should raise_error 7 | 8 | lambda { CouchDocs::Store.new("uri") }. 9 | should_not raise_error 10 | end 11 | 12 | context "a valid store" do 13 | before(:each) do 14 | @it = CouchDocs::Store.new("uri") 15 | 16 | @hash = { 17 | 'a' => { 18 | 'b' => { 19 | 'c' => 'function(doc) { return true; }' 20 | } 21 | } 22 | } 23 | end 24 | 25 | it "should be able to put a new document" do 26 | CouchDocs::Store. 27 | should_receive(:put). 28 | with("uri", { }) 29 | 30 | CouchDocs::Store.put!("uri", { }) 31 | end 32 | 33 | it "should delete existing docs if first put fails" do 34 | CouchDocs::Store. 35 | stub!(:put). 36 | and_raise(RestClient::RequestFailed) 37 | 38 | CouchDocs::Store. 39 | should_receive(:delete_and_put). 40 | with("uri", { }) 41 | 42 | CouchDocs::Store.put!("uri", { }) 43 | end 44 | 45 | it "should be able to delete and put" do 46 | CouchDocs::Store. 47 | should_receive(:delete). 48 | with("uri") 49 | 50 | CouchDocs::Store. 51 | should_receive(:put). 52 | with("uri", { }) 53 | 54 | CouchDocs::Store.delete_and_put("uri", { }) 55 | end 56 | 57 | it "should be able to load a hash into design docs" do 58 | RestClient. 59 | should_receive(:put). 60 | with("uri/_design/a", 61 | '{"b":{"c":"function(doc) { return true; }"}}', 62 | :content_type => 'application/json') 63 | @it.put_design_documents(@hash) 64 | end 65 | 66 | it "should be able to retrieve an existing document" do 67 | RestClient. 68 | stub!(:get). 69 | and_return('{"_rev":"1234"}') 70 | 71 | CouchDocs::Store.get("uri").should == { '_rev' => "1234" } 72 | end 73 | 74 | it "should be able to delete an existing document" do 75 | CouchDocs::Store.stub!(:get).and_return({ '_rev' => '1234' }) 76 | 77 | RestClient. 78 | should_receive(:delete). 79 | with("uri?rev=1234") 80 | 81 | CouchDocs::Store.delete("uri") 82 | end 83 | 84 | it "deletes without revision if none is present (e.g. database delete)" do 85 | CouchDocs::Store.stub!(:get).and_return({ }) 86 | 87 | RestClient. 88 | should_receive(:delete). 89 | with("uri") 90 | 91 | CouchDocs::Store.delete("uri") 92 | end 93 | 94 | it "should be able to load each document" do 95 | CouchDocs::Store.stub!(:get). 96 | with("uri/_all_docs"). 97 | and_return({ "total_rows" => 2, 98 | "offset" => 0, 99 | "rows" => [{"id"=>"1", "value"=>{}, "key"=>"1"}, 100 | {"id"=>"2", "value"=>{}, "key"=>"2"}]}) 101 | 102 | CouchDocs::Store.stub!(:get).with("uri/1?attachments=true") 103 | CouchDocs::Store.should_receive(:get).with("uri/2?attachments=true") 104 | 105 | @it.each { } 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /spec/couch_docs_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe CouchDocs do 4 | it "should be able to create (or delete/create) a DB" do 5 | CouchDocs::Store. 6 | should_receive(:put!). 7 | with("couchdb_url", anything()) 8 | 9 | CouchDocs.destructive_database_create("couchdb_url") 10 | end 11 | 12 | it "should be able to load design and normal documents" do 13 | CouchDocs. 14 | should_receive(:put_design_dir). 15 | with("uri", "fixtures/_design") 16 | 17 | CouchDocs. 18 | should_receive(:put_document_dir). 19 | with("uri", "fixtures") 20 | 21 | CouchDocs.put_dir("uri", "fixtures") 22 | end 23 | 24 | it "should be able to load directory/JS files into CouchDB as design docs" do 25 | store = mock("Store") 26 | CouchDocs::Store.stub!(:new).and_return(store) 27 | 28 | dir = mock("Design Directory") 29 | dir.stub!(:to_hash).and_return({ "foo" => "bar" }) 30 | CouchDocs::DesignDirectory.stub!(:new).and_return(dir) 31 | 32 | store. 33 | should_receive(:put_design_documents). 34 | with({ "foo" => "bar" }) 35 | 36 | CouchDocs.put_design_dir("uri", "fixtures") 37 | end 38 | 39 | it "should be able to load documents into CouchDB" do 40 | dir = mock("Document Directory") 41 | dir. 42 | stub!(:each_document). 43 | and_yield('foo', {"foo" => "1"}) 44 | 45 | CouchDocs::DocumentDirectory.stub!(:new).and_return(dir) 46 | 47 | CouchDocs::Store. 48 | should_receive(:put!). 49 | with('uri/foo', {"foo" => "1"}) 50 | 51 | CouchDocs.put_document_dir("uri", "fixtures") 52 | end 53 | 54 | it "should be able to upload a single document into CouchDB" do 55 | CouchDocs::Store. 56 | should_receive(:put!). 57 | with('uri/foo', {"foo" => "1"}) 58 | 59 | File.stub!(:read).and_return('{"foo": "1"}') 60 | 61 | CouchDocs.put_file("uri", "/foo") 62 | end 63 | 64 | context "dumping CouchDB documents to a directory" do 65 | before(:each) do 66 | @store = mock("Store") 67 | CouchDocs::Store.stub!(:new).and_return(@store) 68 | 69 | @des_dir = mock("Design Directory").as_null_object 70 | CouchDocs::DesignDirectory.stub!(:new).and_return(@des_dir) 71 | 72 | @dir = mock("Document Directory").as_null_object 73 | CouchDocs::DocumentDirectory.stub!(:new).and_return(@dir) 74 | end 75 | it "should be able to store all CouchDB documents on the filesystem" do 76 | @store.stub!(:map).and_return([{'_id' => 'foo'}]) 77 | @dir. 78 | should_receive(:store_document). 79 | with({'_id' => 'foo'}) 80 | 81 | CouchDocs.dump("uri", "fixtures") 82 | end 83 | it "should ignore design documents" do 84 | @store.stub!(:map).and_return([{'_id' => '_design/foo'}]) 85 | @dir. 86 | should_not_receive(:store_document) 87 | 88 | CouchDocs.dump("uri", "fixtures") 89 | end 90 | it "should strip revision numbers" do 91 | @store.stub!(:map). 92 | and_return([{'_id' => 'foo', '_rev' => '1-1234'}]) 93 | @dir. 94 | should_receive(:store_document). 95 | with({'_id' => 'foo'}) 96 | 97 | CouchDocs.dump("uri", "fixtures") 98 | end 99 | it "should not dump regular docs when asked for only design docs" do 100 | @store.stub!(:map). 101 | and_return([{'foo' => 'bar'}]) 102 | 103 | @dir. 104 | should_not_receive(:store_document) 105 | 106 | CouchDocs.dump("uri", "fixtures", :design) 107 | end 108 | it "should not dump design docs when asked for only regular docs" do 109 | @store.stub!(:map). 110 | and_return([{'_id' => '_design/foo'}]) 111 | 112 | @des_dir. 113 | should_not_receive(:store_document) 114 | 115 | CouchDocs.dump("uri", "fixtures", :doc) 116 | end 117 | end 118 | end 119 | 120 | # EOF 121 | -------------------------------------------------------------------------------- /spec/spec.opts: -------------------------------------------------------------------------------- 1 | -cfs --color 2 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'couch_docs' 2 | require 'spec' 3 | require 'spec/autorun' 4 | 5 | Spec::Runner.configure do |config| 6 | # == Mock Framework 7 | # 8 | # RSpec uses it's own mocking framework by default. If you prefer to 9 | # use mocha, flexmock or RR, uncomment the appropriate line: 10 | # 11 | # config.mock_with :mocha 12 | # config.mock_with :flexmock 13 | # config.mock_with :rr 14 | end 15 | 16 | # EOF 17 | -------------------------------------------------------------------------------- /test/test_couch_docs.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eee-c/couch_docs/bb97eb57e4a6110172e7cf34ecc723deb89bcecb/test/test_couch_docs.rb --------------------------------------------------------------------------------