omgdav 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/.document +3 -0
  2. data/.gitignore +17 -0
  3. data/.manifest +53 -0
  4. data/.wrongdoc.yml +6 -0
  5. data/COPYING +661 -0
  6. data/ChangeLog +185 -0
  7. data/GIT-VERSION-FILE +1 -0
  8. data/GIT-VERSION-GEN +33 -0
  9. data/GNUmakefile +35 -0
  10. data/LATEST +1 -0
  11. data/NEWS +1 -0
  12. data/README +154 -0
  13. data/bin/omgdav-setup +4 -0
  14. data/bin/omgdav-sync +32 -0
  15. data/lib/omgdav/app.rb +70 -0
  16. data/lib/omgdav/copy.rb +100 -0
  17. data/lib/omgdav/copy_move.rb +54 -0
  18. data/lib/omgdav/db.rb +258 -0
  19. data/lib/omgdav/delete.rb +66 -0
  20. data/lib/omgdav/get.rb +35 -0
  21. data/lib/omgdav/http_get.rb +146 -0
  22. data/lib/omgdav/input_wrapper.rb +32 -0
  23. data/lib/omgdav/migrations/0001_initial.rb +45 -0
  24. data/lib/omgdav/migrations/0002_contenttype.rb +15 -0
  25. data/lib/omgdav/migrations/0003_synctmp.rb +14 -0
  26. data/lib/omgdav/mkcol.rb +28 -0
  27. data/lib/omgdav/move.rb +74 -0
  28. data/lib/omgdav/options.rb +21 -0
  29. data/lib/omgdav/propfind.rb +46 -0
  30. data/lib/omgdav/propfind_response.rb +150 -0
  31. data/lib/omgdav/proppatch.rb +116 -0
  32. data/lib/omgdav/put.rb +110 -0
  33. data/lib/omgdav/rack_util.rb +56 -0
  34. data/lib/omgdav/setup.rb +16 -0
  35. data/lib/omgdav/sync.rb +78 -0
  36. data/lib/omgdav/version.rb +2 -0
  37. data/lib/omgdav.rb +27 -0
  38. data/omgdav.gemspec +35 -0
  39. data/pkg.mk +175 -0
  40. data/setup.rb +1586 -0
  41. data/test/integration.rb +232 -0
  42. data/test/test_copy.rb +121 -0
  43. data/test/test_delete.rb +15 -0
  44. data/test/test_litmus.rb +61 -0
  45. data/test/test_move.rb +66 -0
  46. data/test/test_omgdav_app.rb +102 -0
  47. data/test/test_propfind.rb +30 -0
  48. data/test/test_proppatch.rb +156 -0
  49. data/test/test_put.rb +31 -0
  50. data/test/test_readonly.rb +22 -0
  51. data/test/test_sync.rb +49 -0
  52. data/test/test_urlmap.rb +59 -0
  53. data/test/test_worm.rb +26 -0
  54. metadata +342 -0
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,2 @@
1
+ # :enddoc:
2
+ OMGDAV::VERSION = '0.0.0'
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"