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.
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"