couch-quilt 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +3 -0
- data/INSTALL +32 -0
- data/README.md +33 -0
- data/Rakefile +39 -0
- data/bin/couchquilt +55 -0
- data/couch-quilt.gemspec +56 -0
- data/lib/couchquilt.rb +37 -0
- data/lib/couchquilt/couch_client.rb +44 -0
- data/lib/couchquilt/debugged_fs.rb +21 -0
- data/lib/couchquilt/fs.rb +414 -0
- data/lib/couchquilt/version.rb +3 -0
- data/spec/couchquilt_spec.rb +691 -0
- data/spec/spec_helper.rb +8 -0
- metadata +68 -0
data/.gitignore
ADDED
data/INSTALL
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
Installing Quilt
|
2
|
+
================
|
3
|
+
|
4
|
+
Dependencies
|
5
|
+
------------
|
6
|
+
|
7
|
+
Quilt is written in Ruby using the Ruby FuseFS library.
|
8
|
+
So all you need is Ruby itself, the Rubygems package manager and the ruby fusefs library.
|
9
|
+
On Debian derivates you can achieve that using the following command:
|
10
|
+
|
11
|
+
apt-get install ruby rubygems libfusefs-ruby
|
12
|
+
|
13
|
+
|
14
|
+
Installation
|
15
|
+
------------
|
16
|
+
|
17
|
+
As soon I released this as a gem, you will be able to install via Rubygems:
|
18
|
+
|
19
|
+
gem install quilt
|
20
|
+
|
21
|
+
|
22
|
+
Problems
|
23
|
+
--------
|
24
|
+
|
25
|
+
On Ubuntu the fuse install lack adequate permission settings:
|
26
|
+
|
27
|
+
fusermount: failed to open /etc/fuse.conf: Permission denied
|
28
|
+
|
29
|
+
|
30
|
+
The following line can be used to fix this:
|
31
|
+
|
32
|
+
chmod a+rwx /etc/fuse.conf
|
data/README.md
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
Quilt
|
2
|
+
=====
|
3
|
+
|
4
|
+
Read and write CouchDB documents via FUSE userspace filesystem
|
5
|
+
|
6
|
+
|
7
|
+
Install dependencies
|
8
|
+
--------------------
|
9
|
+
|
10
|
+
apt-get install ruby rubygems libfusefs-ruby
|
11
|
+
|
12
|
+
|
13
|
+
Install Quilt
|
14
|
+
-------------
|
15
|
+
|
16
|
+
gem install couch-quilt
|
17
|
+
|
18
|
+
|
19
|
+
Getting started
|
20
|
+
---------------
|
21
|
+
|
22
|
+
start quilt by typing
|
23
|
+
|
24
|
+
couchquilt mountpoint [server] [--debug]
|
25
|
+
|
26
|
+
|
27
|
+
Your mapped CouchDB will now be available at *mountpoint*.
|
28
|
+
|
29
|
+
You can create databases and documents with mkdir, add properties via echo >> and so on.
|
30
|
+
Databases and documents can be deleted via a touch /database_id/_delete or /database_id/document_id/_delete.
|
31
|
+
|
32
|
+
|
33
|
+
Read more on the [projectpage](http://jo.github.com/quilt/).
|
data/Rakefile
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'spec/rake/spectask'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
require File.join(File.dirname(__FILE__), 'lib', 'couchquilt', 'version')
|
5
|
+
|
6
|
+
desc 'Default: run specs.'
|
7
|
+
task :default => :spec
|
8
|
+
|
9
|
+
desc "Run all examples"
|
10
|
+
Spec::Rake::SpecTask.new(:spec) do |t|
|
11
|
+
t.spec_files = FileList['spec/*_spec.rb']
|
12
|
+
t.spec_opts = ["--color"]
|
13
|
+
end
|
14
|
+
|
15
|
+
desc "Run all examples with RCov"
|
16
|
+
Spec::Rake::SpecTask.new(:coverage) do |t|
|
17
|
+
t.spec_files = FileList['spec/*_spec.rb']
|
18
|
+
t.rcov = true
|
19
|
+
t.rcov_opts = ['--exclude', '/var/lib/gems', '--exclude', "spec"]
|
20
|
+
end
|
21
|
+
|
22
|
+
begin
|
23
|
+
require 'jeweler'
|
24
|
+
Jeweler::Tasks.new do |s|
|
25
|
+
s.name = "couch-quilt"
|
26
|
+
s.version = Couchquilt::VERSION
|
27
|
+
s.summary = "Access CouchDB from filesystem."
|
28
|
+
s.email = "schmidt@netzmerk.com"
|
29
|
+
s.homepage = "http://jo.github.com/quilt"
|
30
|
+
s.description = "Access CouchDB JSON documents from filesystem."
|
31
|
+
s.authors = ['Johannes Jörg Schmidt']
|
32
|
+
s.rubyforge_project = "couch-quilt"
|
33
|
+
#s.files = FileList["[A-Z]*(.rdoc)", "{bin,lib,spec}/**/*", "README.md", "INSTALL", "Rakefile"]
|
34
|
+
end
|
35
|
+
|
36
|
+
Jeweler::GemcutterTasks.new
|
37
|
+
rescue LoadError
|
38
|
+
puts "Jeweler, or one of its dependencies, is not available. Install it with: sudo gem install jeweler"
|
39
|
+
end
|
data/bin/couchquilt
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# Copyright 2010 Johannes J. Schmidt, TF
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
# you may not use this file except in compliance with the License.
|
6
|
+
# You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
# See the License for the specific language governing permissions and
|
14
|
+
# limitations under the License.
|
15
|
+
#
|
16
|
+
# usage:
|
17
|
+
# couchquilt ~/quilt http://localhost:5984
|
18
|
+
|
19
|
+
QUILT_ROOT = File.expand_path(File.join(File.dirname(__FILE__), ".."))
|
20
|
+
require File.join(QUILT_ROOT, "lib/couchquilt")
|
21
|
+
|
22
|
+
DEBUG = ARGV.delete("--debug")
|
23
|
+
require File.join(File.dirname(__FILE__), '../lib/couchquilt/debugged_fs') if DEBUG
|
24
|
+
|
25
|
+
if ARGV[0].nil?
|
26
|
+
puts "usage:"
|
27
|
+
puts "couchquilt mountpoint [server] [--debug]"
|
28
|
+
exit
|
29
|
+
end
|
30
|
+
|
31
|
+
# database server url defaults to http://127.0.0.1:5984
|
32
|
+
# couch fs will be mounted on ./app/127.0.0.1:5984 per default
|
33
|
+
mountpoint = ARGV[0]
|
34
|
+
server = ARGV[1] || "http://127.0.0.1:5984"
|
35
|
+
|
36
|
+
# create mount point if needed
|
37
|
+
Dir.mkdir(mountpoint) unless File.directory?(mountpoint)
|
38
|
+
|
39
|
+
# init quilt fs
|
40
|
+
quilt_fs_class = DEBUG ? Couchquilt::DebuggedFS : Couchquilt::FS
|
41
|
+
FuseFS.set_root quilt_fs_class.new(server)
|
42
|
+
FuseFS.mount_under mountpoint
|
43
|
+
|
44
|
+
# listen for exit signals and unmount fuse
|
45
|
+
trap("INT") do
|
46
|
+
puts "ancelling..."
|
47
|
+
FuseFS.unmount
|
48
|
+
FuseFS.exit
|
49
|
+
puts "Bye."
|
50
|
+
end
|
51
|
+
|
52
|
+
# actually do the Fuse mount
|
53
|
+
puts "Quilt maps #{server} to #{mountpoint}"
|
54
|
+
puts "Debug mode" if DEBUG
|
55
|
+
FuseFS.run
|
data/couch-quilt.gemspec
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{couch-quilt}
|
8
|
+
s.version = "0.2.1"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Johannes J\303\266rg Schmidt"]
|
12
|
+
s.date = %q{2010-02-25}
|
13
|
+
s.default_executable = %q{couchquilt}
|
14
|
+
s.description = %q{Access CouchDB JSON documents from filesystem.}
|
15
|
+
s.email = %q{schmidt@netzmerk.com}
|
16
|
+
s.executables = ["couchquilt"]
|
17
|
+
s.extra_rdoc_files = [
|
18
|
+
"README.md"
|
19
|
+
]
|
20
|
+
s.files = [
|
21
|
+
".gitignore",
|
22
|
+
"INSTALL",
|
23
|
+
"README.md",
|
24
|
+
"Rakefile",
|
25
|
+
"bin/couchquilt",
|
26
|
+
"couch-quilt.gemspec",
|
27
|
+
"lib/couchquilt.rb",
|
28
|
+
"lib/couchquilt/couch_client.rb",
|
29
|
+
"lib/couchquilt/debugged_fs.rb",
|
30
|
+
"lib/couchquilt/fs.rb",
|
31
|
+
"lib/couchquilt/version.rb",
|
32
|
+
"spec/couchquilt_spec.rb",
|
33
|
+
"spec/spec_helper.rb"
|
34
|
+
]
|
35
|
+
s.homepage = %q{http://jo.github.com/quilt}
|
36
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
37
|
+
s.require_paths = ["lib"]
|
38
|
+
s.rubyforge_project = %q{couch-quilt}
|
39
|
+
s.rubygems_version = %q{1.3.5}
|
40
|
+
s.summary = %q{Access CouchDB from filesystem.}
|
41
|
+
s.test_files = [
|
42
|
+
"spec/couchquilt_spec.rb",
|
43
|
+
"spec/spec_helper.rb"
|
44
|
+
]
|
45
|
+
|
46
|
+
if s.respond_to? :specification_version then
|
47
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
48
|
+
s.specification_version = 3
|
49
|
+
|
50
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
51
|
+
else
|
52
|
+
end
|
53
|
+
else
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
data/lib/couchquilt.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# Copyright 2010 Johannes J. Schmidt, TF
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
#
|
15
|
+
# Quilt mixes CouchDB Design Documents into FUSE
|
16
|
+
|
17
|
+
|
18
|
+
require "rubygems"
|
19
|
+
begin
|
20
|
+
require 'json'
|
21
|
+
rescue LoadError
|
22
|
+
raise "You need install and require your own json compatible library since Quilt couldn't load the json/json_pure gem" unless Kernel.const_defined?("JSON")
|
23
|
+
end
|
24
|
+
require "rest_client"
|
25
|
+
require 'fusefs'
|
26
|
+
|
27
|
+
$:.unshift File.dirname(__FILE__) unless
|
28
|
+
$:.include?(File.dirname(__FILE__)) ||
|
29
|
+
$:.include?(File.expand_path(File.dirname(__FILE__)))
|
30
|
+
|
31
|
+
require 'couchquilt/couch_client'
|
32
|
+
require 'couchquilt/fs'
|
33
|
+
|
34
|
+
|
35
|
+
# Set out your Quilt
|
36
|
+
module Couchquilt
|
37
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# speaking to CouchDB server
|
2
|
+
module Couchquilt
|
3
|
+
class CouchClient
|
4
|
+
def initialize(server_name)
|
5
|
+
@server_name = server_name
|
6
|
+
end
|
7
|
+
|
8
|
+
# initiates a GET request and returns the JSON parsed response
|
9
|
+
def get(path)
|
10
|
+
response = RestClient.get(url_for(path))
|
11
|
+
JSON.parse(response) rescue response
|
12
|
+
rescue RestClient::ResourceNotFound
|
13
|
+
nil
|
14
|
+
end
|
15
|
+
|
16
|
+
# initiates a PUT request and returns true if it was successful
|
17
|
+
def put(path, payload = {})
|
18
|
+
RestClient.put url_for(path), payload.to_json
|
19
|
+
true
|
20
|
+
end
|
21
|
+
|
22
|
+
# initiates a DELETE request and returns true if it was successful
|
23
|
+
def delete(path)
|
24
|
+
RestClient.delete url_for(path)
|
25
|
+
true
|
26
|
+
rescue
|
27
|
+
false
|
28
|
+
end
|
29
|
+
|
30
|
+
# initiates a HEAD request to +url+ and returns true if the resource exists
|
31
|
+
def head(path)
|
32
|
+
RestClient.head url_for(path)
|
33
|
+
true
|
34
|
+
rescue RestClient::ResourceNotFound
|
35
|
+
false
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def url_for(path)
|
41
|
+
File.join(@server_name, path)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Couchquilt
|
2
|
+
# proxy sends all requests to QuiltFS
|
3
|
+
# used for printing debug information
|
4
|
+
class DebuggedFS
|
5
|
+
def initialize(server_name)
|
6
|
+
@quilt = FS.new(server_name)
|
7
|
+
rescue => e
|
8
|
+
STDERR.puts e.message, e.backtrace
|
9
|
+
end
|
10
|
+
|
11
|
+
(FS.public_instance_methods - public_instance_methods).each do |method|
|
12
|
+
class_eval <<-STR
|
13
|
+
def #{method}(*args)
|
14
|
+
@quilt.#{method}(*args)
|
15
|
+
rescue => e
|
16
|
+
STDOUT.puts "#{method}", args.inspect, e.class, e.message, e.backtrace
|
17
|
+
end
|
18
|
+
STR
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,414 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
module Couchquilt
|
5
|
+
class FS
|
6
|
+
# initializes Quilt FS with the database server name
|
7
|
+
def initialize(server_name)
|
8
|
+
@couch = CouchClient.new(server_name)
|
9
|
+
end
|
10
|
+
|
11
|
+
# list contents of path
|
12
|
+
def contents(path)
|
13
|
+
database, id, *parts = extract_parts(path)
|
14
|
+
|
15
|
+
list case named_path(path)
|
16
|
+
when :root
|
17
|
+
@couch.get("_all_dbs")
|
18
|
+
when :database
|
19
|
+
["_design"] +
|
20
|
+
# database meta data
|
21
|
+
map_json(@couch.get(database)) +
|
22
|
+
# all documents but design documents
|
23
|
+
# Note: we can not use ?startkey="_design/"&endkey="_design0" here,
|
24
|
+
# because that would return no results for databases without design documents
|
25
|
+
@couch.get("#{database}/_all_docs")["rows"].map { |r| r["id"] }.select { |id| id !~ /^_design\// }
|
26
|
+
when :_design
|
27
|
+
query = URI.encode('startkey="_design"&endkey="_design0"')
|
28
|
+
# all design documents
|
29
|
+
@couch.get("#{database}/_all_docs?#{query}")["rows"].map { |r| r["id"].sub("_design/", "") }
|
30
|
+
when :_show
|
31
|
+
(@couch.get("#{database}/#{id}")["shows"] || {}).keys
|
32
|
+
when :_list
|
33
|
+
(@couch.get("#{database}/#{id}")["lists"] || {}).keys
|
34
|
+
when :_view
|
35
|
+
(@couch.get("#{database}/#{id}")["views"] || {}).keys
|
36
|
+
when :list_function
|
37
|
+
(@couch.get("#{database}/#{id}")["views"] || {}).keys.map { |name| "#{name}.html" }
|
38
|
+
when :show_function
|
39
|
+
query = URI.encode('startkey="_design0"')
|
40
|
+
@couch.get("#{database}/_all_docs?#{query}")["rows"].map { |r| "#{r["id"]}.html" }
|
41
|
+
when :view_function, :view_function_result
|
42
|
+
map_json(get_view_result_part(database, id, parts))
|
43
|
+
when :design_document
|
44
|
+
["_list", "_show", "_view"] +
|
45
|
+
map_json(get_document_part(database, id))
|
46
|
+
else
|
47
|
+
map_json(get_document_part(database, id, parts))
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# is path a directory?
|
52
|
+
def directory?(path)
|
53
|
+
database, id, *parts = extract_parts(path)
|
54
|
+
|
55
|
+
case named_path(path)
|
56
|
+
when :database, :document, :design_document
|
57
|
+
@couch.head(path)
|
58
|
+
when :view_function_result
|
59
|
+
doc = get_view_result_part(database, id, parts)
|
60
|
+
# arrays and hashes are mapped into directories
|
61
|
+
doc.is_a?(Hash) || doc.is_a?(Array)
|
62
|
+
when :database_info, :show_function_result, :list_function_result
|
63
|
+
false
|
64
|
+
when nil
|
65
|
+
# look into document
|
66
|
+
doc = get_document_part(database, id, parts)
|
67
|
+
# arrays and hashes are mapped into directories
|
68
|
+
doc.is_a?(Hash) || doc.is_a?(Array)
|
69
|
+
else
|
70
|
+
# all other special paths are directories by now
|
71
|
+
# TODO: thats not so good.
|
72
|
+
true
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# is path a file?
|
77
|
+
def file?(path)
|
78
|
+
database, id, *parts = extract_parts(path)
|
79
|
+
|
80
|
+
case named_path(path)
|
81
|
+
when :database_info, :show_function_result, :list_function_result
|
82
|
+
true
|
83
|
+
when :view_function_result
|
84
|
+
# Every javascript or HTML is file, based on extension
|
85
|
+
[".js", ".html"].include?(File.extname(path))
|
86
|
+
when nil
|
87
|
+
# look into document
|
88
|
+
doc = get_document_part(database, id, parts)
|
89
|
+
# only arrays and hashes are mapped into directories
|
90
|
+
!doc.nil? && !(doc.is_a?(Hash) || doc.is_a?(Array))
|
91
|
+
else
|
92
|
+
false
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# reading file contents of path
|
97
|
+
def read_file(path)
|
98
|
+
database, id, *parts = extract_parts(path)
|
99
|
+
|
100
|
+
content case named_path(path)
|
101
|
+
when :database_info
|
102
|
+
@couch.get(database)[remove_extname(id)]
|
103
|
+
when :show_function_result
|
104
|
+
parts.shift
|
105
|
+
@couch.get(remove_extname(File.join(database, id, "_show", *parts)))
|
106
|
+
when :list_function_result
|
107
|
+
parts.shift
|
108
|
+
@couch.get(remove_extname(File.join(database, id, "_list", *parts)))
|
109
|
+
when :view_function_result
|
110
|
+
get_view_result_part(database, id, parts)
|
111
|
+
else
|
112
|
+
get_document_part(database, id, parts)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# is path writable?
|
117
|
+
# every javascript file is writable, except ones starting with an underscore
|
118
|
+
def can_write?(path)
|
119
|
+
database, id, *parts = extract_parts(path)
|
120
|
+
|
121
|
+
case named_path(path)
|
122
|
+
when :switch_delete_database, :switch_delete_document
|
123
|
+
true
|
124
|
+
when :database_info, :show_function_result, :list_function_result, :view_function_result
|
125
|
+
false
|
126
|
+
else
|
127
|
+
File.basename(path) !~ /\A_/ && File.extname(path) == ".js"
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# writes content str to path
|
132
|
+
def write_to(path, str)
|
133
|
+
database, id, *parts = extract_parts(path)
|
134
|
+
# fetch document
|
135
|
+
doc = @couch.get("#{database}/#{id}")
|
136
|
+
# update the value that the file at path holds
|
137
|
+
update_value(doc, parts, str)
|
138
|
+
# save document
|
139
|
+
@couch.put("#{database}/#{id}", doc)
|
140
|
+
end
|
141
|
+
|
142
|
+
# can I delete path?
|
143
|
+
def can_delete?(path)
|
144
|
+
return false if File.basename(path) =~ /\A_/
|
145
|
+
|
146
|
+
case named_path(path)
|
147
|
+
when :database_info, :show_function_result, :list_function_result, :view_function_result
|
148
|
+
false
|
149
|
+
else
|
150
|
+
true
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# deletes path
|
155
|
+
# either deletes a database, document or removes a part of a document
|
156
|
+
def delete(path)
|
157
|
+
database, id, *parts = extract_parts(path)
|
158
|
+
|
159
|
+
# fetch document
|
160
|
+
doc = @couch.get("#{database}/#{id}")
|
161
|
+
# remove object
|
162
|
+
remove_object(doc, parts)
|
163
|
+
# save document
|
164
|
+
@couch.put("#{database}/#{id}", doc)
|
165
|
+
end
|
166
|
+
|
167
|
+
# can I make a directory at path?
|
168
|
+
def can_mkdir?(path)
|
169
|
+
database, id, *parts = extract_parts(path)
|
170
|
+
|
171
|
+
case named_path(path)
|
172
|
+
when :root, :_design, :_list, :list_function, :_show, :show_function, :_view, :view_function, :view_function_result
|
173
|
+
false
|
174
|
+
when :database, :document, :design_document
|
175
|
+
# can create database or document unless exists
|
176
|
+
!@couch.head(path)
|
177
|
+
else
|
178
|
+
!get_document_part(database, id, parts)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# makes a directory
|
183
|
+
# this creates either a database, a document or inserts an object into a document
|
184
|
+
def mkdir(path)
|
185
|
+
database, id, *parts = extract_parts(path)
|
186
|
+
|
187
|
+
case named_path(path)
|
188
|
+
when :database
|
189
|
+
@couch.put(database)
|
190
|
+
when :document, :design_document
|
191
|
+
@couch.put("#{database}/#{id}")
|
192
|
+
else
|
193
|
+
# fetch document
|
194
|
+
doc = @couch.get("#{database}/#{id}")
|
195
|
+
# insert empty object
|
196
|
+
update_value(doc, parts, {})
|
197
|
+
# save document
|
198
|
+
@couch.put("#{database}/#{id}", doc)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
# can I remove a directory at path?
|
203
|
+
def can_rmdir?(path)
|
204
|
+
database, id, *parts = extract_parts(path)
|
205
|
+
|
206
|
+
case named_path(path)
|
207
|
+
when :root, :_design, :_list, :list_function, :_show, :show_function, :_view, :view_function, :view_function_result, :database, :document, :design_document
|
208
|
+
false
|
209
|
+
else
|
210
|
+
get_document_part(database, id, parts).empty? rescue nil
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
# deletes a directory
|
215
|
+
# that is a part of a document
|
216
|
+
def rmdir(path)
|
217
|
+
database, id, *parts = extract_parts(path)
|
218
|
+
|
219
|
+
# fetch document
|
220
|
+
doc = @couch.get("#{database}/#{id}")
|
221
|
+
# remove object
|
222
|
+
update_value(doc, parts, nil)
|
223
|
+
# save document
|
224
|
+
@couch.put("#{database}/#{id}", doc)
|
225
|
+
end
|
226
|
+
|
227
|
+
# switch to delete a database or document
|
228
|
+
def touch(path)
|
229
|
+
database, id, *parts = extract_parts(path)
|
230
|
+
|
231
|
+
case named_path(path)
|
232
|
+
when :switch_delete_database
|
233
|
+
@couch.delete(database)
|
234
|
+
when :switch_delete_document
|
235
|
+
doc = @couch.get("#{database}/#{id}")
|
236
|
+
@couch.delete("#{database}/#{id}?rev=#{doc["_rev"]}")
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
private
|
241
|
+
|
242
|
+
# gets the database, id and parts from path
|
243
|
+
def extract_parts(path)
|
244
|
+
database, id, *parts = path.scan(/[^\/]+/)
|
245
|
+
if id == "_design" && !parts.empty?
|
246
|
+
id << "/#{parts.shift}"
|
247
|
+
end
|
248
|
+
[database, id] + parts
|
249
|
+
end
|
250
|
+
|
251
|
+
# returns a path identifier for special paths
|
252
|
+
def named_path(path)
|
253
|
+
database, id, *parts = extract_parts(path)
|
254
|
+
|
255
|
+
if database.nil?
|
256
|
+
# /
|
257
|
+
:root
|
258
|
+
elsif id == "_delete"
|
259
|
+
:switch_delete_database
|
260
|
+
elsif parts.size == 1 && parts.first == "_delete"
|
261
|
+
:switch_delete_document
|
262
|
+
elsif id.nil?
|
263
|
+
# /database_id
|
264
|
+
:database
|
265
|
+
elsif ["compact_running.b.js", "db_name.js", "disk_format_version.i.js", "disk_size.i.js", "doc_count.i.js", "doc_del_count.i.js", "instance_start_time.js", "purge_seq.i.js", "update_seq.i.js"].include?(id) && parts.empty?
|
266
|
+
:database_info
|
267
|
+
elsif id == "_design" && parts.empty?
|
268
|
+
# /database_id/_design
|
269
|
+
:_design
|
270
|
+
elsif id =~ /_design\// && parts.empty?
|
271
|
+
# /database_id/_design/design_document_id
|
272
|
+
:design_document
|
273
|
+
elsif id =~ /_design\// && parts == ["_show"]
|
274
|
+
# /database_id/_design/design_document_id/_show
|
275
|
+
:_show
|
276
|
+
elsif id =~ /_design\// && parts == ["_list"]
|
277
|
+
# /database_id/_design/design_document_id/_list
|
278
|
+
:_list
|
279
|
+
elsif id =~ /_design\// && parts == ["_view"]
|
280
|
+
# /database_id/_design/design_document_id/_view
|
281
|
+
:_view
|
282
|
+
elsif id =~ /_design\// && parts.size == 2 && parts.first == "_list"
|
283
|
+
# /database_id/_design/design_document_id/_list/list_function_name
|
284
|
+
:list_function
|
285
|
+
elsif id =~ /_design\// && parts.size == 3 && parts.first == "_list"
|
286
|
+
# /database_id/_design/design_document_id/_list/list_function_name/view_function_name
|
287
|
+
:list_function_result
|
288
|
+
elsif id =~ /_design\// && parts.size == 2 && parts.first == "_show"
|
289
|
+
# /database_id/_design/design_document_id/_show/show_function_name
|
290
|
+
:show_function
|
291
|
+
elsif id =~ /_design\// && parts.size == 3 && parts.first == "_show"
|
292
|
+
# /database_id/_design/design_document_id/_show/show_function_name/document_id
|
293
|
+
:show_function_result
|
294
|
+
elsif id =~ /_design\// && parts.size == 2 && parts.first == "_view"
|
295
|
+
# /database_id/_design/design_document_id/_view/view_function_name
|
296
|
+
:view_function
|
297
|
+
elsif id =~ /_design\// && parts.size >= 3 && parts.first == "_view"
|
298
|
+
# /database_id/_design/design_document_id/_view/view_function_name/document_id
|
299
|
+
:view_function_result
|
300
|
+
elsif parts.empty?
|
301
|
+
# /database_id/document_id
|
302
|
+
:document
|
303
|
+
else
|
304
|
+
# /database_id/document_id/object
|
305
|
+
# /database_id/_design/design_document_id
|
306
|
+
# /database_id/_design/design_document_id/object
|
307
|
+
nil
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
# maps json contents into contents array
|
312
|
+
def map_json(doc)
|
313
|
+
case doc
|
314
|
+
when Hash
|
315
|
+
# Hash is mapped to directory
|
316
|
+
doc.keys.sort.map { |k| append_extname(k, doc[k]) }
|
317
|
+
when Array
|
318
|
+
# Array is mapped to directory
|
319
|
+
doc.map { |k| append_extname(doc.index(k), k) }
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
# fetch part of document
|
324
|
+
def get_document_part(database, id, parts = [])
|
325
|
+
doc = @couch.get("#{database}/#{id}")
|
326
|
+
get_part(doc, parts)
|
327
|
+
end
|
328
|
+
|
329
|
+
# get view result, or a part of that document.
|
330
|
+
def get_view_result_part(database, id, parts = [])
|
331
|
+
a, view_function_name, *rest = parts
|
332
|
+
view = [id.sub("_design/", ""), "_view", view_function_name].join("/")
|
333
|
+
doc = @couch.get("#{database}/_design/#{view}")
|
334
|
+
get_part(doc, rest)
|
335
|
+
end
|
336
|
+
|
337
|
+
# get a part of the document
|
338
|
+
# eg: get_part({ :a => { :b => :c}}, [:a, :b]) #=> :c
|
339
|
+
# also removes the file extension and gets index of arrays
|
340
|
+
def get_part(doc, parts)
|
341
|
+
return if doc.nil?
|
342
|
+
doc = doc.dup
|
343
|
+
parts.each do |part|
|
344
|
+
case doc
|
345
|
+
when Hash
|
346
|
+
doc = doc[remove_extname(part)]
|
347
|
+
when Array
|
348
|
+
doc = doc[part.to_i]
|
349
|
+
end
|
350
|
+
end
|
351
|
+
doc
|
352
|
+
end
|
353
|
+
|
354
|
+
# updates a part of a hash
|
355
|
+
# Example:
|
356
|
+
# update_value({:a => { :b => 'c'}}, [:a, :b], 'cis') #=> {:a => { :b => 'cis'}}
|
357
|
+
def update_value(hash, keys, value)
|
358
|
+
key = remove_extname(keys.shift)
|
359
|
+
if keys.empty?
|
360
|
+
hash[key] = value
|
361
|
+
else
|
362
|
+
hash[key] = update_value(hash[key], keys, value)
|
363
|
+
end
|
364
|
+
hash
|
365
|
+
end
|
366
|
+
|
367
|
+
# removes an object from a hash
|
368
|
+
# Example:
|
369
|
+
# remove_object({:a => { :b => 'c'}}, [:a, :b]) #=> {:a => { }}
|
370
|
+
def remove_object(hash, keys)
|
371
|
+
key = remove_extname(keys.shift)
|
372
|
+
if keys.empty?
|
373
|
+
hash.delete(key)
|
374
|
+
else
|
375
|
+
hash[key] = remove_object(hash[key], keys)
|
376
|
+
end
|
377
|
+
hash
|
378
|
+
end
|
379
|
+
|
380
|
+
# remove extname to get the id
|
381
|
+
def remove_extname(filename)
|
382
|
+
filename.sub(/((\.(f|i|b))?\.js|\.html)\z/, "")
|
383
|
+
end
|
384
|
+
|
385
|
+
# Appends extname, that is: builds a filename from key and value.
|
386
|
+
# Note: values are casted by extension.
|
387
|
+
def append_extname(key, value)
|
388
|
+
basename = key.is_a?(Integer) ? "%.3d" % key : key
|
389
|
+
|
390
|
+
case value
|
391
|
+
when Float
|
392
|
+
"#{basename}.f.js"
|
393
|
+
when Integer
|
394
|
+
"#{basename}.i.js"
|
395
|
+
when nil, String
|
396
|
+
"#{basename}.js"
|
397
|
+
when true, false
|
398
|
+
"#{basename}.b.js"
|
399
|
+
else
|
400
|
+
basename
|
401
|
+
end
|
402
|
+
end
|
403
|
+
|
404
|
+
# escapes the value for using as filename
|
405
|
+
def list(array)
|
406
|
+
return [] if array.nil?
|
407
|
+
array.compact.map { |v| CGI.escape(v) }.sort
|
408
|
+
end
|
409
|
+
|
410
|
+
def content(value)
|
411
|
+
value.to_s
|
412
|
+
end
|
413
|
+
end
|
414
|
+
end
|