omgdav 0.0.0
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/.document +3 -0
- data/.gitignore +17 -0
- data/.manifest +53 -0
- data/.wrongdoc.yml +6 -0
- data/COPYING +661 -0
- data/ChangeLog +185 -0
- data/GIT-VERSION-FILE +1 -0
- data/GIT-VERSION-GEN +33 -0
- data/GNUmakefile +35 -0
- data/LATEST +1 -0
- data/NEWS +1 -0
- data/README +154 -0
- data/bin/omgdav-setup +4 -0
- data/bin/omgdav-sync +32 -0
- data/lib/omgdav/app.rb +70 -0
- data/lib/omgdav/copy.rb +100 -0
- data/lib/omgdav/copy_move.rb +54 -0
- data/lib/omgdav/db.rb +258 -0
- data/lib/omgdav/delete.rb +66 -0
- data/lib/omgdav/get.rb +35 -0
- data/lib/omgdav/http_get.rb +146 -0
- data/lib/omgdav/input_wrapper.rb +32 -0
- data/lib/omgdav/migrations/0001_initial.rb +45 -0
- data/lib/omgdav/migrations/0002_contenttype.rb +15 -0
- data/lib/omgdav/migrations/0003_synctmp.rb +14 -0
- data/lib/omgdav/mkcol.rb +28 -0
- data/lib/omgdav/move.rb +74 -0
- data/lib/omgdav/options.rb +21 -0
- data/lib/omgdav/propfind.rb +46 -0
- data/lib/omgdav/propfind_response.rb +150 -0
- data/lib/omgdav/proppatch.rb +116 -0
- data/lib/omgdav/put.rb +110 -0
- data/lib/omgdav/rack_util.rb +56 -0
- data/lib/omgdav/setup.rb +16 -0
- data/lib/omgdav/sync.rb +78 -0
- data/lib/omgdav/version.rb +2 -0
- data/lib/omgdav.rb +27 -0
- data/omgdav.gemspec +35 -0
- data/pkg.mk +175 -0
- data/setup.rb +1586 -0
- data/test/integration.rb +232 -0
- data/test/test_copy.rb +121 -0
- data/test/test_delete.rb +15 -0
- data/test/test_litmus.rb +61 -0
- data/test/test_move.rb +66 -0
- data/test/test_omgdav_app.rb +102 -0
- data/test/test_propfind.rb +30 -0
- data/test/test_proppatch.rb +156 -0
- data/test/test_put.rb +31 -0
- data/test/test_readonly.rb +22 -0
- data/test/test_sync.rb +49 -0
- data/test/test_urlmap.rb +59 -0
- data/test/test_worm.rb +26 -0
- metadata +342 -0
data/lib/omgdav/move.rb
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
# :stopdoc:
|
3
|
+
# Copyright (C) 2012, Eric Wong <normalperson@yhbt.net>
|
4
|
+
# License: AGPLv3 or later (https://www.gnu.org/licenses/agpl-3.0.txt)
|
5
|
+
require "omgdav/rack_util"
|
6
|
+
require "omgdav/db"
|
7
|
+
|
8
|
+
module OMGDAV::Move
|
9
|
+
include OMGDAV::DB
|
10
|
+
include OMGDAV::RackUtil
|
11
|
+
include OMGDAV::CopyMove
|
12
|
+
|
13
|
+
def call_move(env)
|
14
|
+
src, dst = {}, {}
|
15
|
+
err = copy_move_prepare!(env, src, dst) and return err
|
16
|
+
|
17
|
+
dst_node = { name: dst[:basename], parent_id: dst[:parent][:id] }
|
18
|
+
|
19
|
+
if src[:node][:collection]
|
20
|
+
case env['HTTP_DEPTH']
|
21
|
+
when nil, "infinity"
|
22
|
+
move_collection(src[:node], dst_node)
|
23
|
+
else
|
24
|
+
return r(400, "invalid Depth: #{env['HTTP_DEPTH'].inspect}")
|
25
|
+
end
|
26
|
+
else
|
27
|
+
dst_key = node_to_key(dst_node, {})
|
28
|
+
@mogc.rename(src[:key], dst_key)
|
29
|
+
end
|
30
|
+
@db[:paths].where(id: src[:node][:id]).update(dst_node)
|
31
|
+
r(dst[:node] ? 204 : 201)
|
32
|
+
end
|
33
|
+
|
34
|
+
def move_collection(src_node, dst_node)
|
35
|
+
cache_old = {}
|
36
|
+
paths = @db[:paths]
|
37
|
+
queue = [ [ 0, src_node ] ]
|
38
|
+
max_id = paths.max(:id)
|
39
|
+
q = { domain_id: @domain_id }
|
40
|
+
|
41
|
+
# poison cache_new with post-MOVE data
|
42
|
+
cache_new = {}
|
43
|
+
cache_new[src_node[:id]] = node_to_parts(dst_node, {}).freeze
|
44
|
+
|
45
|
+
while cur_job = queue.pop
|
46
|
+
min_id, cur_src = cur_job
|
47
|
+
q[:parent_id] = cur_src[:id]
|
48
|
+
next if min_id == max_id
|
49
|
+
begin
|
50
|
+
continue = false
|
51
|
+
q[:id] = ((min_id+1)..max_id)
|
52
|
+
paths.order(:id).where(q).limit(@sql_limit).each do |child_node|
|
53
|
+
min_id = child_node[:id]
|
54
|
+
if child_node[:collection]
|
55
|
+
queue << [ min_id, cur_src ]
|
56
|
+
queue << [ 0, child_node ]
|
57
|
+
|
58
|
+
# poison cache_new with post-MOVE data
|
59
|
+
tmp = { name: child_node[:name], parent_id: cur_src[:id] }
|
60
|
+
cache_new[child_node[:id]] = node_to_parts(tmp, cache_new).freeze
|
61
|
+
|
62
|
+
continue = false
|
63
|
+
break
|
64
|
+
else
|
65
|
+
old = node_to_key(child_node, cache_old)
|
66
|
+
new = node_to_key(child_node, cache_new)
|
67
|
+
@mogc.rename(old, new)
|
68
|
+
continue = true
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end while continue
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
# :enddoc:
|
3
|
+
# Copyright (C) 2012, Eric Wong <normalperson@yhbt.net>
|
4
|
+
# License: AGPLv3 or later (https://www.gnu.org/licenses/agpl-3.0.txt)
|
5
|
+
require "omgdav/rack_util"
|
6
|
+
require "omgdav/db"
|
7
|
+
|
8
|
+
module OMGDAV::Options
|
9
|
+
include OMGDAV::DB
|
10
|
+
include OMGDAV::RackUtil
|
11
|
+
|
12
|
+
def call_options(env)
|
13
|
+
resp = r(200)
|
14
|
+
if %r{\A/} =~ env["PATH_INFO"]
|
15
|
+
resp[1]["DAV"] = "1"
|
16
|
+
end
|
17
|
+
resp
|
18
|
+
ensure
|
19
|
+
drain_input(env)
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
# :enddoc:
|
3
|
+
# Copyright (C) 2012, Eric Wong <normalperson@yhbt.net>
|
4
|
+
# License: AGPLv3 or later (https://www.gnu.org/licenses/agpl-3.0.txt)
|
5
|
+
require "omgdav/rack_util"
|
6
|
+
require "omgdav/db"
|
7
|
+
require "omgdav/propfind_response"
|
8
|
+
require "omgdav/input_wrapper"
|
9
|
+
|
10
|
+
module OMGDAV::Propfind
|
11
|
+
def call_propfind(env)
|
12
|
+
input_validate_propfind(env)
|
13
|
+
parts = path_split(env)
|
14
|
+
depth = env["HTTP_DEPTH"]
|
15
|
+
case depth
|
16
|
+
when "0", "1"
|
17
|
+
# resolve the collection
|
18
|
+
node = node_resolve(parts)
|
19
|
+
|
20
|
+
if node
|
21
|
+
OMGDAV::PropfindResponse.new(env, node, @db).response
|
22
|
+
else
|
23
|
+
r(404)
|
24
|
+
end
|
25
|
+
else
|
26
|
+
return r(400, "Depth: #{depth} not supported")
|
27
|
+
end
|
28
|
+
rescue Nokogiri::SyntaxError => e
|
29
|
+
r(400, "syntax error: #{e.message}")
|
30
|
+
end
|
31
|
+
|
32
|
+
def input_validate_propfind(env)
|
33
|
+
input = OMGDAV::InputWrapper.new(env)
|
34
|
+
parser = Nokogiri::XML::SAX::Parser.new(Validator.new)
|
35
|
+
parser.parse_io(input)
|
36
|
+
rescue Nokogiri::SyntaxError
|
37
|
+
# don't choke on empty input
|
38
|
+
raise if input.bytes != 0
|
39
|
+
end
|
40
|
+
|
41
|
+
class Validator < Nokogiri::XML::SAX::Document
|
42
|
+
def error(string)
|
43
|
+
raise Nokogiri::SyntaxError, string
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
# :enddoc:
|
3
|
+
# Copyright (C) 2012, Eric Wong <normalperson@yhbt.net>
|
4
|
+
# License: AGPLv3 or later (https://www.gnu.org/licenses/agpl-3.0.txt)
|
5
|
+
require "omgdav/db"
|
6
|
+
|
7
|
+
class OMGDAV::PropfindResponse # :nodoc:
|
8
|
+
include OMGDAV::DB
|
9
|
+
|
10
|
+
# This is a Rack response body for HTTP/1.0 folks who can't handle
|
11
|
+
# Transfer-Encoding: chunked
|
12
|
+
class XMLTmp < Tempfile # :nodoc:
|
13
|
+
def close
|
14
|
+
super true # unlink immediately
|
15
|
+
end
|
16
|
+
|
17
|
+
# no line buffering
|
18
|
+
def each
|
19
|
+
buf = ""
|
20
|
+
while read(16384, buf)
|
21
|
+
yield buf
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize(env, parent, db)
|
27
|
+
@env = env
|
28
|
+
@parent = parent
|
29
|
+
@db = db
|
30
|
+
@n2k_cache = {}
|
31
|
+
@domain_id = parent[:domain_id]
|
32
|
+
@script_name = Rack::Request.new(env).script_name
|
33
|
+
@ct_cache = {}
|
34
|
+
end
|
35
|
+
|
36
|
+
def response
|
37
|
+
headers = { "Content-Type" => 'text/xml; charset="utf-8"' }
|
38
|
+
case @env["HTTP_VERSION"]
|
39
|
+
when "HTTP/1.1"
|
40
|
+
headers["Transfer-Encoding"] = "chunked"
|
41
|
+
body = self
|
42
|
+
else
|
43
|
+
body = XMLTmp.new("omgdav_propfind")
|
44
|
+
body.sync = true
|
45
|
+
each_blob { |blob| body.write(blob) }
|
46
|
+
headers["Content-Length"] = body.size.to_s
|
47
|
+
body.rewind
|
48
|
+
end
|
49
|
+
|
50
|
+
[ 207, headers, body ]
|
51
|
+
end
|
52
|
+
|
53
|
+
# chunks a Rack response body for HTTP/1.1
|
54
|
+
def each
|
55
|
+
each_blob do |blob|
|
56
|
+
yield "#{blob.bytesize.to_s(16)}\r\n#{blob}\r\n"
|
57
|
+
end
|
58
|
+
yield "0\r\n\r\n"
|
59
|
+
end
|
60
|
+
|
61
|
+
def row_xml(x, row)
|
62
|
+
is_col = row[:collection]
|
63
|
+
props = dead_props_get(row)
|
64
|
+
if props
|
65
|
+
prop_ns = 'xmlns:ns0="DAV" '
|
66
|
+
i = 0
|
67
|
+
props_str = ""
|
68
|
+
props.keys.sort.each do |ns|
|
69
|
+
if ns.empty?
|
70
|
+
pfx = ""
|
71
|
+
else
|
72
|
+
pfx = "ns#{i += 1}"
|
73
|
+
prop_ns << "xmlns:#{pfx}=\"#{ns}\" "
|
74
|
+
pfx << ":"
|
75
|
+
end
|
76
|
+
props[ns].each do |name,value|
|
77
|
+
# no need to escape value here because we never escaped it on the
|
78
|
+
# way into the DB(!)
|
79
|
+
props_str << "<#{pfx}#{name}>#{value}</#{pfx}#{name}>"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
x << %Q(<D:response #{prop_ns}xmlns:lp1="DAV:" xmlns:lp2="#{OMGDAV::LP2}">)
|
85
|
+
name = node_to_key(row, @n2k_cache).fast_xs
|
86
|
+
name << "/" if is_col && ! row[:name].empty?
|
87
|
+
|
88
|
+
x << "<D:href>#@script_name/#{name}</D:href>"
|
89
|
+
|
90
|
+
x << "<D:propstat>"
|
91
|
+
x << "<D:prop>"
|
92
|
+
|
93
|
+
if is_col
|
94
|
+
x << "<lp1:resourcetype><D:collection/></lp1:resourcetype>"
|
95
|
+
else
|
96
|
+
x << "<lp1:resourcetype/>"
|
97
|
+
x << "<lp2:executable>#{row[:executable] ? 'T' : 'F'}</lp2:executable>"
|
98
|
+
x << "<lp1:getcontentlength>#{row[:length]}</lp1:getcontentlength>"
|
99
|
+
getcontenttype = content_type(row, @ct_cache)
|
100
|
+
x << "<lp1:getcontenttype>#{getcontenttype}</lp1:getcontenttype>"
|
101
|
+
end
|
102
|
+
x << props_str if props_str
|
103
|
+
|
104
|
+
created = Time.at(row[:created]).utc.xmlschema
|
105
|
+
x << "<lp1:creationdate>#{created}</lp1:creationdate>"
|
106
|
+
mtime = Time.at(row[:mtime]).httpdate
|
107
|
+
x << "<lp1:getlastmodified>#{mtime}</lp1:getlastmodified>"
|
108
|
+
x << "</D:prop>"
|
109
|
+
x << "<D:status>HTTP/1.1 200 OK</D:status>"
|
110
|
+
x << "</D:propstat>"
|
111
|
+
x << "</D:response>"
|
112
|
+
end
|
113
|
+
|
114
|
+
def each_blob
|
115
|
+
paths = @db[:paths]
|
116
|
+
q = { parent_id: @parent[:id], domain_id: @parent[:domain_id] }
|
117
|
+
prev = { name: "" }
|
118
|
+
x = '<?xml version="1.0" encoding="utf-8"?>' \
|
119
|
+
'<D:multistatus xmlns:D="DAV:">'
|
120
|
+
|
121
|
+
# FIXME: this might be horribly inefficient
|
122
|
+
case @env["HTTP_DEPTH"]
|
123
|
+
when "0"
|
124
|
+
row_xml(x, @parent)
|
125
|
+
when "1"
|
126
|
+
if @parent[:collection]
|
127
|
+
row_xml(x, @parent)
|
128
|
+
begin
|
129
|
+
seen = 0
|
130
|
+
paths.order(:name).where {
|
131
|
+
self.&(q, self.>(:name, prev[:name]))
|
132
|
+
}.limit(@sql_limit).each do |row|
|
133
|
+
seen += 1
|
134
|
+
prev = row
|
135
|
+
row_xml(x, row)
|
136
|
+
end
|
137
|
+
|
138
|
+
break if seen != @sql_limit
|
139
|
+
yield x
|
140
|
+
x.clear
|
141
|
+
end while true
|
142
|
+
else
|
143
|
+
row_xml(x, @parent)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
x << '</D:multistatus>'
|
148
|
+
yield x
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
# :enddoc:
|
3
|
+
# Copyright (C) 2012, Eric Wong <normalperson@yhbt.net>
|
4
|
+
# License: AGPLv3 or later (https://www.gnu.org/licenses/agpl-3.0.txt)
|
5
|
+
require "omgdav/rack_util"
|
6
|
+
require "omgdav/db"
|
7
|
+
require "omgdav/input_wrapper"
|
8
|
+
|
9
|
+
module OMGDAV::Proppatch
|
10
|
+
include OMGDAV::DB
|
11
|
+
include OMGDAV::RackUtil
|
12
|
+
|
13
|
+
def value(z)
|
14
|
+
z.children.text.strip
|
15
|
+
end
|
16
|
+
|
17
|
+
def set_props(node, x)
|
18
|
+
props = nil
|
19
|
+
x.children.each do |y|
|
20
|
+
next unless Nokogiri::XML::Element === y
|
21
|
+
next unless "prop" == y.name
|
22
|
+
y.children.each do |z|
|
23
|
+
next unless Nokogiri::XML::Element === z
|
24
|
+
ns = z.namespace.href rescue ""
|
25
|
+
case ns
|
26
|
+
when "DAV:"
|
27
|
+
case z.name
|
28
|
+
when "getcontenttype"
|
29
|
+
node[:contenttype] = content_type_id(value(z))
|
30
|
+
when "getlastmodified"
|
31
|
+
mtime = value(z)
|
32
|
+
node[:mtime] = Time.httpdate(mtime).to_i
|
33
|
+
when "creationdate"
|
34
|
+
created = value(z)
|
35
|
+
node[:created] = Time.iso8601(created).to_i
|
36
|
+
end
|
37
|
+
when OMGDAV::LP2
|
38
|
+
case z.name
|
39
|
+
when "executable"
|
40
|
+
node[:executable] = ("T" == z.children.to_a.join.strip)
|
41
|
+
end
|
42
|
+
else
|
43
|
+
props ||= dead_props_get(node) || {}
|
44
|
+
nsprops = props[ns] ||= {}
|
45
|
+
remove_namespace!(x)
|
46
|
+
nsprops[z.name] = z.children.to_xml
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
dead_props_set(node, props) if props
|
51
|
+
end
|
52
|
+
|
53
|
+
def remove_props(node, x)
|
54
|
+
props = nil
|
55
|
+
x.children.each do |y|
|
56
|
+
next unless Nokogiri::XML::Element === y
|
57
|
+
next unless "prop" == y.name
|
58
|
+
y.children.each do |z|
|
59
|
+
next unless Nokogiri::XML::Element === z
|
60
|
+
ns = z.namespace.href rescue ""
|
61
|
+
case ns
|
62
|
+
when "DAV:"
|
63
|
+
case z.name
|
64
|
+
when "getcontenttype"
|
65
|
+
node[:contenttype] = nil
|
66
|
+
end
|
67
|
+
when OMGDAV::LP2
|
68
|
+
case z.name
|
69
|
+
when "executable"
|
70
|
+
node[:executable] = false
|
71
|
+
end
|
72
|
+
else
|
73
|
+
props ||= dead_props_get(node) or next
|
74
|
+
nsprops = props[ns] or next
|
75
|
+
nsprops.delete(z.name) or next
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
dead_props_set(node, props) if props
|
80
|
+
end
|
81
|
+
|
82
|
+
def call_proppatch(env)
|
83
|
+
node = node_resolve(path_split(env)) or return r(404)
|
84
|
+
input = OMGDAV::InputWrapper.new(env)
|
85
|
+
xml = Nokogiri::XML(input)
|
86
|
+
xmlns = xml.root.namespace
|
87
|
+
return r(400) unless "DAV:" == xmlns.href
|
88
|
+
xml.xpath("//D:propertyupdate", "D" => "DAV:").children.each do |x|
|
89
|
+
next unless Nokogiri::XML::Element === x
|
90
|
+
|
91
|
+
case x.name
|
92
|
+
when "set"
|
93
|
+
set_props(node, x)
|
94
|
+
when "remove"
|
95
|
+
remove_props(node, x)
|
96
|
+
else
|
97
|
+
return r(400, "Unknown name=#{x.name}")
|
98
|
+
end
|
99
|
+
end
|
100
|
+
@db[:paths].where(id: node.delete(:id)).update(node)
|
101
|
+
r(200)
|
102
|
+
rescue Nokogiri::SyntaxError => e
|
103
|
+
r(400, "syntax error #{e.message}")
|
104
|
+
rescue OMGDAV::InvalidContentType => e
|
105
|
+
r(400, "bad getcontenttype: #{e.message}")
|
106
|
+
end
|
107
|
+
|
108
|
+
# removes namespace recursively without operating recursively
|
109
|
+
def remove_namespace!(el)
|
110
|
+
queue = [ el ]
|
111
|
+
while el = queue.shift
|
112
|
+
el.namespace = nil if el.respond_to?(:namespace=)
|
113
|
+
queue.concat(el.children.to_a) if el.respond_to?(:children)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
data/lib/omgdav/put.rb
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
# :enddoc:
|
3
|
+
# Copyright (C) 2012, Eric Wong <normalperson@yhbt.net>
|
4
|
+
# License: AGPLv3 or later (https://www.gnu.org/licenses/agpl-3.0.txt)
|
5
|
+
require "omgdav/rack_util"
|
6
|
+
require "omgdav/db"
|
7
|
+
|
8
|
+
module OMGDAV::Put
|
9
|
+
include OMGDAV::DB
|
10
|
+
include OMGDAV::RackUtil
|
11
|
+
|
12
|
+
def new_file_prepare(env)
|
13
|
+
params = Rack::Utils.parse_query(env["QUERY_STRING"])
|
14
|
+
|
15
|
+
# prepare options for create_open/create_close:
|
16
|
+
new_file_opts = @new_file_opts.dup
|
17
|
+
new_file_opts[:class] = params["class"] || "default"
|
18
|
+
|
19
|
+
# try to give a Content-Length to the tracker
|
20
|
+
clen = env["CONTENT_LENGTH"]
|
21
|
+
clen and new_file_opts[:content_length] = clen.to_i
|
22
|
+
|
23
|
+
if /\bContent-MD5\b/i =~ env["HTTP_TRAILER"]
|
24
|
+
# if the client will give the Content-MD5 as the trailer,
|
25
|
+
# we must lazily populate it since we're not guaranteed to
|
26
|
+
# have the trailer, yet (rack.input is lazily read on unicorn)
|
27
|
+
new_file_opts[:content_md5] = lambda { env["HTTP_CONTENT_MD5"] }
|
28
|
+
elsif cmd5 = env["HTTP_CONTENT_MD5"]
|
29
|
+
# maybe the client gave the Content-MD5 in the header
|
30
|
+
new_file_opts[:content_md5] = cmd5
|
31
|
+
end
|
32
|
+
|
33
|
+
new_file_opts
|
34
|
+
end
|
35
|
+
|
36
|
+
def edit_input(tmp, input, off_out)
|
37
|
+
tmp.seek(off_out)
|
38
|
+
IO.copy_stream(input, tmp)
|
39
|
+
tmp.rewind
|
40
|
+
tmp
|
41
|
+
end
|
42
|
+
|
43
|
+
def call_put(env)
|
44
|
+
return r(403) if %r{/\z} =~ env["PATH_INFO"]
|
45
|
+
return r(100) if %r{\b100-continue\b}i =~ env["HTTP_EXPECT"]
|
46
|
+
parts = path_split(env)
|
47
|
+
key = parts.join("/")
|
48
|
+
basename = parts.pop or return r(403)
|
49
|
+
|
50
|
+
if @create_full_put_path
|
51
|
+
parent = col_vivify(parts)
|
52
|
+
else
|
53
|
+
parent = col_resolve(parts) or return r(404)
|
54
|
+
end
|
55
|
+
|
56
|
+
node = node_lookup(parent[:id], basename)
|
57
|
+
return r(409) if node && @worm
|
58
|
+
|
59
|
+
if range = env["HTTP_CONTENT_RANGE"]
|
60
|
+
%r{\A\s*bytes\s+(\d+)-(\d+)/\*\s*\z} =~ range or
|
61
|
+
return r(400, "Bad range", env)
|
62
|
+
clen = env["CONTENT_LENGTH"] or
|
63
|
+
return r(400, "Content-Length required for Content-Range")
|
64
|
+
off_out = $1.to_i
|
65
|
+
len = $2.to_i - off_out + 1
|
66
|
+
len == clen.to_i or
|
67
|
+
return r(400,
|
68
|
+
"Bad range, Content-Range: #{range} does not match\n" \
|
69
|
+
"Content-Length: #{clen.inspect}", env)
|
70
|
+
tmp = Tempfile.new('put_cr')
|
71
|
+
tmp.sync = true
|
72
|
+
end
|
73
|
+
|
74
|
+
input = env["rack.input"]
|
75
|
+
if node
|
76
|
+
if tmp
|
77
|
+
@mogc.get_uris(key, @get_path_opts).each do |uri|
|
78
|
+
case res = OMGDAV::HttpGet.run(nil, uri)
|
79
|
+
when Array
|
80
|
+
res[2].stream_to(tmp)
|
81
|
+
input = edit_input(tmp, input, off_out)
|
82
|
+
break
|
83
|
+
else
|
84
|
+
logger(env).error("#{uri}: #{res.message} (#{res.class})")
|
85
|
+
end
|
86
|
+
end
|
87
|
+
input or return r(500, "Could not retrieve key=#{key.inspect}")
|
88
|
+
end
|
89
|
+
else
|
90
|
+
input = edit_input(tmp, input, off_out) if tmp && off_out != 0
|
91
|
+
end
|
92
|
+
|
93
|
+
# finally, upload the file
|
94
|
+
new_file_opts = new_file_prepare(env)
|
95
|
+
length = @mogc.new_file(key, new_file_opts) do |io|
|
96
|
+
IO.copy_stream(input, io)
|
97
|
+
end
|
98
|
+
info = { "length" => length }
|
99
|
+
|
100
|
+
if node
|
101
|
+
node_update(node, info)
|
102
|
+
r(204)
|
103
|
+
else
|
104
|
+
file_ensure(parent[:id], basename, info)
|
105
|
+
r(201)
|
106
|
+
end
|
107
|
+
ensure
|
108
|
+
tmp.close! if tmp
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
# :enddoc:
|
3
|
+
# Copyright (C) 2012, Eric Wong <normalperson@yhbt.net>
|
4
|
+
# License: AGPLv3 or later (https://www.gnu.org/licenses/agpl-3.0.txt)
|
5
|
+
require "omgdav"
|
6
|
+
require "rack"
|
7
|
+
require "rack/utils"
|
8
|
+
require "rack/mime"
|
9
|
+
require "logger"
|
10
|
+
module OMGDAV::RackUtil
|
11
|
+
|
12
|
+
def logger(env)
|
13
|
+
env["rack.logger"] || Logger.new($stderr)
|
14
|
+
end
|
15
|
+
|
16
|
+
# returns a plain-text HTTP response
|
17
|
+
def r(code, msg = nil, env = nil) # :nodoc:
|
18
|
+
if env
|
19
|
+
logger(env).warn("#{env['REQUEST_METHOD']} #{env['PATH_INFO']} " \
|
20
|
+
"#{code} #{msg.inspect}")
|
21
|
+
end
|
22
|
+
|
23
|
+
if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(code)
|
24
|
+
[ code, {}, [] ]
|
25
|
+
else
|
26
|
+
msg ||= Rack::Utils::HTTP_STATUS_CODES[code] || ""
|
27
|
+
|
28
|
+
if msg.size > 0
|
29
|
+
# using += to not modify original string (owned by Rack)
|
30
|
+
msg += "\n"
|
31
|
+
end
|
32
|
+
|
33
|
+
[ code,
|
34
|
+
{ 'Content-Type' => 'text/plain', 'Content-Length' => msg.size.to_s },
|
35
|
+
[ msg ] ]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def path_split(env)
|
40
|
+
parts = env["PATH_INFO"].gsub(%r{/+\z}, "").split(%r{/+})
|
41
|
+
parts.shift # leading slash
|
42
|
+
parts
|
43
|
+
end
|
44
|
+
|
45
|
+
def drain_input(env)
|
46
|
+
input = env["rack.input"]
|
47
|
+
bytes = 0
|
48
|
+
if buf = input.read(23)
|
49
|
+
bytes += buf.size
|
50
|
+
while input.read(666, buf)
|
51
|
+
bytes += buf.size
|
52
|
+
end
|
53
|
+
end
|
54
|
+
bytes
|
55
|
+
end
|
56
|
+
end
|
data/lib/omgdav/setup.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# :enddoc:
|
2
|
+
# Copyright (C) 2012, Eric Wong <normalperson@yhbt.net>
|
3
|
+
# License: AGPLv3 or later (https://www.gnu.org/licenses/agpl-3.0.txt)
|
4
|
+
# This is the code behind omgdav-setup(1)
|
5
|
+
require "omgdav"
|
6
|
+
require "rubygems" # we use Gem.load_path
|
7
|
+
|
8
|
+
module OMGDAV::Setup
|
9
|
+
def self.run(argv = ARGV.dup)
|
10
|
+
migdir = "#{File.dirname(__FILE__)}/migrations"
|
11
|
+
argv << "-m"
|
12
|
+
argv << migdir
|
13
|
+
ARGV.replace(argv)
|
14
|
+
load Gem.bin_path("sequel", "sequel")
|
15
|
+
end
|
16
|
+
end
|
data/lib/omgdav/sync.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
# :enddoc:
|
3
|
+
# Copyright (C) 2012, Eric Wong <normalperson@yhbt.net>
|
4
|
+
# License: AGPLv3 or later (https://www.gnu.org/licenses/agpl-3.0.txt)
|
5
|
+
require "omgdav/db"
|
6
|
+
|
7
|
+
# each of these is an sync job
|
8
|
+
class OMGDAV::Sync # :nodoc:
|
9
|
+
include OMGDAV::DB
|
10
|
+
|
11
|
+
def initialize(db, mogc, prefix = nil)
|
12
|
+
@db = db
|
13
|
+
@mogc = mogc
|
14
|
+
@prefix = prefix
|
15
|
+
@domain_id = ensure_domain(@mogc.domain)
|
16
|
+
end
|
17
|
+
|
18
|
+
def bad_key(key, msg) # :nodoc:
|
19
|
+
warn "key=#{key.inspect} #{msg}"
|
20
|
+
end
|
21
|
+
|
22
|
+
def info_import(info, seen) # :nodoc:
|
23
|
+
key = info["key"]
|
24
|
+
return bad_key(key, "may not have `//'") if %r{//} =~ key
|
25
|
+
return bad_key(key, "may not start with `/'") if %r{\A/} =~ key
|
26
|
+
return bad_key(key, "may not have a NUL byte") if %r{\0} =~ key
|
27
|
+
|
28
|
+
key.force_encoding(Encoding::UTF_8)
|
29
|
+
return bad_key(key, "is not valid UTF-8") unless key.valid_encoding?
|
30
|
+
|
31
|
+
parts = key.split(%r{/})
|
32
|
+
|
33
|
+
dot = parts.grep(/\A(?:\.\.|\.)\z/)[0]
|
34
|
+
return bad_key(key, "may not contain `..' or `.' as a component") if dot
|
35
|
+
|
36
|
+
filename = parts.pop
|
37
|
+
full_col = parts.join("/")
|
38
|
+
|
39
|
+
begin
|
40
|
+
unless parent_id = seen[full_col]
|
41
|
+
parent_id = root_node[:id]
|
42
|
+
parts.each do |colname|
|
43
|
+
col = col_ensure(parent_id, colname)
|
44
|
+
parent_id = col[:id]
|
45
|
+
end
|
46
|
+
seen[full_col] = parent_id
|
47
|
+
end
|
48
|
+
|
49
|
+
file_ensure(parent_id, filename, info)
|
50
|
+
rescue OMGDAV::TypeConflict
|
51
|
+
bad_key(key, "path conflicts with existing collection or file")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def sync # :nodoc:
|
56
|
+
synctmp = @db[:synctmp]
|
57
|
+
seen = {} # optimization for keys with many path elements
|
58
|
+
|
59
|
+
# snapshot all existing path ids into synctmp table
|
60
|
+
synctmp.delete
|
61
|
+
@db["INSERT INTO synctmp(id) SELECT id FROM paths"].insert
|
62
|
+
|
63
|
+
pd = synctmp.where(id: :$i).prepare(:delete, :delete_by_id, id: :$i)
|
64
|
+
|
65
|
+
@mogc.each_file_info(@prefix) do |info|
|
66
|
+
# delete valid nodes from synctmp as we iterate
|
67
|
+
node = info_import(info, seen) and pd.call(i: node[:id])
|
68
|
+
|
69
|
+
# don't let a pathological case OOM us
|
70
|
+
seen.clear if seen.size > 1000
|
71
|
+
end
|
72
|
+
|
73
|
+
# any ids leftover in the synctmp table are stale
|
74
|
+
@db["DELETE FROM paths WHERE id IN (SELECT id FROM synctmp)"].delete
|
75
|
+
ensure
|
76
|
+
synctmp.delete # cleanup
|
77
|
+
end
|
78
|
+
end
|
data/lib/omgdav.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
# :stopdoc:
|
3
|
+
# Copyright (C) 2012, Eric Wong <normalperson@yhbt.net>
|
4
|
+
# License: AGPLv3 or later (https://www.gnu.org/licenses/agpl-3.0.txt)
|
5
|
+
require "tempfile"
|
6
|
+
require 'time'
|
7
|
+
require 'uri'
|
8
|
+
require 'nokogiri'
|
9
|
+
require 'json'
|
10
|
+
require 'sequel'
|
11
|
+
require 'mogilefs'
|
12
|
+
require 'fast_xs'
|
13
|
+
require 'rack'
|
14
|
+
require 'rack/request'
|
15
|
+
# :startdoc:
|
16
|
+
|
17
|
+
module OMGDAV
|
18
|
+
# :stopdoc:
|
19
|
+
TypeConflict = Class.new(TypeError)
|
20
|
+
InvalidContentType = Class.new(ArgumentError)
|
21
|
+
BadResponse = Class.new(RuntimeError)
|
22
|
+
|
23
|
+
LP2 = "http://apache.org/dav/props/"
|
24
|
+
# :startdoc:
|
25
|
+
end
|
26
|
+
# :enddoc:
|
27
|
+
require "omgdav/version"
|