omgdav 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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"
|