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
data/lib/omgdav/db.rb ADDED
@@ -0,0 +1,258 @@
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
+ module OMGDAV::DB
7
+
8
+ # returns a domain ID
9
+ def ensure_domain(domain)
10
+ domains = @db[:domains]
11
+ @db.transaction do
12
+ q = { domain: domain }
13
+ dom = domains[q]
14
+ dom ? dom[:id] : domains.insert(q)
15
+ end
16
+ end
17
+
18
+ def node_merge_info(node, info)
19
+ info or return
20
+ node[:length] = info["length"]
21
+ end
22
+
23
+ def node_update(node, info = nil)
24
+ node_merge_info(node, info)
25
+ mtime = node[:mtime] = Time.now.to_i
26
+ node_id = node.delete(:id)
27
+ @db[:paths].where(id: node_id).update(node)
28
+
29
+ unless node[:collection]
30
+ @db[:paths].where(id: node[:parent_id]).update(mtime: mtime)
31
+ end
32
+
33
+ node[:id] = node_id
34
+ end
35
+
36
+ def node_update_maybe(node, info)
37
+ info or return
38
+ if info && node[:length] != info["length"]
39
+ node_update(node)
40
+ end
41
+ end
42
+
43
+ def node_ensure(parent_id, name, info = nil)
44
+ q = { name: name, domain_id: @domain_id, parent_id: parent_id }
45
+ collection = info ? false : true
46
+
47
+ node = @db[:paths][q]
48
+ if node
49
+ # node already exists
50
+ raise OMGDAV::TypeConflict if node[:collection] != collection
51
+ node_update_maybe(node, info)
52
+ else
53
+ # brand new node
54
+ node = q.dup
55
+ node[:created] = node[:mtime] = Time.now.to_i
56
+ node[:collection] = collection
57
+ node_merge_info(node, info)
58
+ begin
59
+ node[:id] = @db[:paths].insert(node)
60
+ rescue Sequel::DatabaseError
61
+ # we may conflict on insert if we didn't use a transaction
62
+ raise if @db.in_transaction?
63
+ node = @db[:paths][q] or raise
64
+ raise OMGDAV::TypeConflict if node[:collection] != collection
65
+ end
66
+ end
67
+
68
+ node
69
+ end
70
+
71
+ # ensures the given args maps to a directory, returns a hash for the row
72
+ def col_ensure(parent_id, colname)
73
+ node_ensure(parent_id, colname)
74
+ end
75
+
76
+ # ensures the given args maps to a file, returns a hash for the row
77
+ def file_ensure(parent_id, filename, info)
78
+ node_ensure(parent_id, filename, info)
79
+ end
80
+
81
+ def node_lookup(parent_id, name)
82
+ @db[:paths][name: name, domain_id: @domain_id, parent_id: parent_id]
83
+ end
84
+
85
+ def node_to_parts(node, cache)
86
+ root_id = root_node[:id]
87
+ return [] if root_id == node[:id] # special case
88
+
89
+ orig_name = node[:name]
90
+ cache_key = node[:parent_id]
91
+ unless parts = cache[cache_key]
92
+ parts = []
93
+ until root_id == node[:parent_id]
94
+ node = @db[:paths][id: node[:parent_id], domain_id: @domain_id] or break
95
+ parts.unshift(node[:name].dup.freeze)
96
+ end
97
+
98
+ cache[cache_key] = parts.freeze
99
+ end
100
+
101
+ parts += [ orig_name ]
102
+ end
103
+
104
+ def node_to_key(node, cache)
105
+ node_to_parts(node, cache).join("/")
106
+ end
107
+
108
+ def col_resolve(parts)
109
+ node = root_node
110
+ parts.each do |colpart|
111
+ node = node_lookup(node[:id], colpart)
112
+ return unless node
113
+ raise OMGDAV::TypeConflict unless node[:collection]
114
+ end
115
+ raise OMGDAV::TypeConflict unless node[:collection]
116
+ node
117
+ end
118
+
119
+ def node_resolve(parts)
120
+ return root_node if parts.empty?
121
+ parts = parts.dup
122
+ basename = parts.pop or return
123
+
124
+ parent = col_resolve(parts) or return
125
+ node_lookup(parent[:id], basename)
126
+ end
127
+
128
+ def file_resolve(parts)
129
+ node = node_resolve(parts) or return
130
+ raise OMGDAV::TypeConflict if node[:collection]
131
+ node
132
+ end
133
+
134
+ def col_vivify(parts)
135
+ col = root_node
136
+ parts.each do |colname|
137
+ col = col_ensure(col[:id], colname)
138
+ end
139
+ col
140
+ end
141
+
142
+ def node_delete(node)
143
+ rc = @db[:paths].where(id: node[:id]).delete
144
+ if rc != 1
145
+ key = node_to_key(node, {}).inspect
146
+ warn "expected rc=1 for delete(id=#{node[:id]}) (key=#{key}) got rc=#{rc}"
147
+ end
148
+ rc
149
+ end
150
+
151
+ def dead_props_get(node)
152
+ db_props = node[:dead_props] or return
153
+ db_props = JSON.parse(db_props)
154
+ mapping = {}
155
+ @db[:prop_mappings].each { |row| mapping[row[:id]] = row[:value] }
156
+ raw_props = {}
157
+ db_props.each do |ns, hash|
158
+ tmp = raw_props[mapping[ns.to_i] || ns] = {}
159
+ hash.each { |i, value| tmp[mapping[i.to_i] || i] = value }
160
+ end
161
+ raw_props
162
+ end
163
+
164
+ # returns the new property ID for a given value
165
+ def prop_mapping_ensure(value)
166
+ tbl = @db[:prop_mappings]
167
+ q = { value: value }
168
+ tbl.insert(q)
169
+ rescue Sequel::Database::Error
170
+ raise if @db.in_transaction?
171
+ mapping = tbl[q] or raise
172
+ mapping[:id]
173
+ end
174
+
175
+ def dead_props_set(node, raw_props)
176
+ mapping = {}
177
+ @db[:prop_mappings].each { |row| mapping[row[:value]] = row[:id] }
178
+ db_props = {}
179
+ raw_props.each do |ns, hash|
180
+ ns_id = mapping[ns] ||= prop_mapping_ensure(ns)
181
+ tmp = db_props[ns_id] = {}
182
+ hash.each do |key,value|
183
+ key_id = mapping[key] ||= prop_mapping_ensure(key)
184
+ tmp[key_id] = value
185
+ end
186
+ end
187
+ node[:dead_props] = db_props.to_json
188
+ end
189
+
190
+ def root_node
191
+ q = @root_node and return q
192
+ # root node always has parent_id:0
193
+ q = {
194
+ parent_id: 0,
195
+ collection: true,
196
+ name: "",
197
+ domain_id: @domain_id
198
+ }
199
+ node = @db[:paths][q] and return (@root_node = node)
200
+ q[:mtime] = q[:created] = Time.now.to_i
201
+ begin
202
+ q[:id] = @db[:paths].insert(q)
203
+ q
204
+ rescue Sequel::DatabaseError
205
+ # we may conflict on insert if we didn't use a transaction
206
+ raise if @db.in_transaction?
207
+ @root_node = @db[:paths][q] or raise
208
+ end
209
+ end
210
+
211
+ # returns the mime type based on key name
212
+ def key_mime_type(key) # :nodoc:
213
+ /(\.[^.]+)\z/ =~ key
214
+ Rack::Mime.mime_type($1) # nil => 'application/octet-stream'
215
+ end
216
+
217
+ def content_type(node, cache = nil)
218
+ if ctid = node[:contenttype]
219
+ if row = @db[:prop_mappings][id: ctid]
220
+ type = row[:value]
221
+ else
222
+ warn "DB consistency error: no property mapping for #{ctid.inspect}"
223
+ type = key_mime_type(node[:name])
224
+ end
225
+ cache ? (cache[ctid] = type) : type
226
+ else
227
+ key_mime_type(node[:name])
228
+ end
229
+ end
230
+
231
+ def check_attr!(name, value)
232
+ # TODO: check compliance, erring on the side of being too strict here
233
+ /\A[\w\.\+-]+\z/ =~ value or
234
+ raise OMGDAV::InvalidContentType, "#{name}=#{value.inspect} invalid"
235
+ end
236
+
237
+ def content_type_id(value)
238
+ # normalize the content type
239
+ type_subtype, parameter = value.split(/;\s+/, 2)
240
+ type, subtype = type_subtype.downcase.split(%r{/}, 2)
241
+ check_attr!(:type, type)
242
+ check_attr!(:subtype, subtype)
243
+ value = "#{type}/#{subtype}"
244
+
245
+ if parameter
246
+ attr_key, attr_val = parameter.split(/=/, 2)
247
+ attr_val or
248
+ raise OMGDAV::InvalidContentType,
249
+ "parameter=#{parameter.inspect} invalid"
250
+ attr_key.downcase!
251
+ check_attr!(:parameter, attr_key)
252
+ value << "; #{attr_key}=#{attr_val}"
253
+ end
254
+
255
+ row = @db[:prop_mappings][value: value] and return row[:id]
256
+ prop_mapping_ensure(value)
257
+ end
258
+ end
@@ -0,0 +1,66 @@
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::Delete
9
+ include OMGDAV::DB
10
+ include OMGDAV::RackUtil
11
+
12
+ def delete_file(node, cache)
13
+ node_delete(node)
14
+ key = node_to_key(node, cache)
15
+ @mogc.delete(key)
16
+ rescue MogileFS::Backend::UnknownKeyError
17
+ warn "key=#{key.inspect} missing on delete from domain=#{@mogc.domain}"
18
+ end
19
+
20
+ def delete_collection(node, cache)
21
+ queue = [ node ]
22
+ q = { domain_id: @domain_id }
23
+
24
+ while cur_node = queue.pop
25
+ q[:parent_id] = cur_node[:id]
26
+
27
+ begin
28
+ state = :done
29
+ @db[:paths].where(q).limit(@sql_limit).each do |child_node|
30
+ if child_node[:collection]
31
+ queue << cur_node
32
+ queue << child_node
33
+ state = :descend
34
+ break
35
+ else
36
+ delete_file(child_node, cache)
37
+ state = :continue
38
+ end
39
+ end
40
+ end while state == :continue
41
+
42
+ node_delete(cur_node) if state == :done
43
+ end
44
+ end
45
+
46
+ def delete_entry(node, cache)
47
+ if node[:collection]
48
+ delete_collection(node, cache)
49
+ else
50
+ delete_file(node, cache)
51
+ end
52
+ end
53
+
54
+ def call_delete(env)
55
+ # makes litmus happy with the "delete_fragment" test
56
+ # FRAGMENT may be set by Mongrel and descendents (unicorn/thin)
57
+ env["FRAGMENT"] and return r(400)
58
+
59
+ parts = path_split(env)
60
+ node = node_resolve(parts) or return r(404)
61
+ node[:parent_id] == 0 and return r(403)
62
+ delete_entry(node, {})
63
+
64
+ r(204)
65
+ end
66
+ end
data/lib/omgdav/get.rb ADDED
@@ -0,0 +1,35 @@
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::Get
9
+ include OMGDAV::DB
10
+ include OMGDAV::RackUtil
11
+
12
+ def call_get(env)
13
+ parts = path_split(env)
14
+ basename = parts.pop
15
+ parent = col_resolve(parts) or return r(404)
16
+ node = node_lookup(parent[:id], basename) or return r(404)
17
+ length = node[:length]
18
+ parts << basename
19
+ @mogc.get_uris(parts.join("/"), @get_path_opts).each do |uri|
20
+ case res = OMGDAV::HttpGet.run(env, uri)
21
+ when Array
22
+ headers = res[1]
23
+ headers["Last-Modified"] = Time.at(node[:mtime]).httpdate
24
+ headers["Content-Type"] = content_type(node)
25
+ return res
26
+ else
27
+ logger(env).error("#{uri}: #{res.message} (#{res.class})")
28
+ end
29
+ end
30
+ r(500)
31
+ rescue MogileFS::Backend::UnknownKeyError,
32
+ MogileFS::Backend::DomainNotFoundError
33
+ r(404, "")
34
+ end
35
+ end
@@ -0,0 +1,146 @@
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 "thread"
6
+ require "timeout"
7
+ require "kcar"
8
+
9
+ class OMGDAV::HttpGet < Kcar::Response # :nodoc:
10
+ # support gzip and partial requests if storage nodes support it
11
+ PASS = %w(Range Accept-Encoding).map! do |x|
12
+ [ "HTTP_#{x.tr('a-z-', 'A-Z_')}", x ]
13
+ end
14
+
15
+ DEF_ENV = { "REQUEST_METHOD" => "GET" }
16
+ ADDR_LOCK = Mutex.new
17
+ ADDR_CACHE = {}
18
+ POOL_LOCK = Mutex.new
19
+ POOL = Hash.new { |pool,ip_port| pool[ip_port] = [] }
20
+
21
+ def self.reset
22
+ ADDR_LOCK.synchronize { ADDR_CACHE.clear }
23
+ POOL_LOCK.synchronize { POOL.clear }
24
+ end
25
+
26
+ class MySocket < Kgio::Socket # :nodoc:
27
+ attr_writer :expiry
28
+
29
+ def wait_time
30
+ tout = @expiry ? @expiry - Time.now : @timeout
31
+ raise Timeout::Error if tout < 0
32
+ tout
33
+ end
34
+
35
+ def readpartial(bytes, buf = Thread.current[:omgdav_buf] ||= "")
36
+ case kgio_tryread(bytes, buf)
37
+ when String
38
+ return buf
39
+ when :wait_readable
40
+ kgio_wait_readable(wait_time)
41
+ when nil
42
+ raise EOFError, "end of file reached", []
43
+ end while true
44
+ end
45
+
46
+ def start(buf, timeout)
47
+ @timeout = timeout
48
+ @expiry = Time.now + timeout
49
+ case rv = kgio_trywrite(buf)
50
+ when :wait_writable
51
+ kgio_wait_writable(wait_time)
52
+ when nil
53
+ return
54
+ when String
55
+ buf = rv
56
+ end while true
57
+ end
58
+ end
59
+
60
+ # Called by the Rack server at the end of a successful response
61
+ def close
62
+ reusable = @parser.keepalive? && @parser.body_eof?
63
+ super
64
+ POOL_LOCK.synchronize { POOL[@ip_port] << self } if reusable
65
+ nil
66
+ end
67
+
68
+ def dispatch(req, timeout)
69
+ @sock.start(req, timeout)
70
+ end
71
+
72
+ # returns true if the socket is still alive, nil if dead
73
+ def sock_alive?
74
+ @reused = (:wait_readable == (@sock.kgio_tryread(1) rescue nil)) ?
75
+ true : @sock.close
76
+ end
77
+
78
+ # returns true if the socket was reused and thus retryable
79
+ def fail_retryable?
80
+ @sock.close
81
+ @reused
82
+ end
83
+
84
+ def initialize(sock, ip_port)
85
+ super(sock)
86
+ @reused = false
87
+ @ip_port = ip_port
88
+ end
89
+
90
+ def self.optional_headers(env)
91
+ PASS.map { |from, to| tmp = env[from] and "#{to}: #{tmp}\r\n" }.join
92
+ end
93
+
94
+ def self.run(env, uri, timeout = 2)
95
+ obj = obj_get(uri.host, uri.port)
96
+ env ||= DEF_ENV
97
+ m = env["REQUEST_METHOD"]
98
+ req = "#{m} #{uri.path} HTTP/1.0\r\n" \
99
+ "Connection: keep-alive\r\n" \
100
+ "#{optional_headers(env)}\r\n"
101
+ begin
102
+ obj.dispatch(req, timeout)
103
+ status, header, body = res = obj.rack
104
+
105
+ case m
106
+ when "GET"
107
+ body.sock.expiry = nil
108
+ when "HEAD"
109
+ # kcar doesn't know if it's a HEAD or GET response, and HEAD
110
+ # responses have Content-Length in it which fools kcar..
111
+ body.parser.body_bytes_left = 0
112
+ res[1] = header.dup
113
+ body.close # clobbers original header
114
+ res[2] = body = []
115
+ end
116
+
117
+ case status.to_i
118
+ when 206, 200
119
+ res
120
+ else
121
+ OMGDAV::BadResponse.new(status.to_s)
122
+ end
123
+ rescue => e
124
+ retry if obj.fail_retryable?
125
+ return e
126
+ end
127
+ end
128
+
129
+ def self.obj_get(ip, port)
130
+ ip_port = "#{ip}:#{port}"
131
+ while obj = POOL_LOCK.synchronize { POOL[ip_port].pop }
132
+ return obj if obj.sock_alive?
133
+ end
134
+
135
+ addr = ADDR_LOCK.synchronize do
136
+ ADDR_CACHE[ip_port] ||= Socket.sockaddr_in(port, ip)
137
+ end
138
+
139
+ new(MySocket.start(addr), ip_port)
140
+ end
141
+
142
+ def stream_to(io)
143
+ each { |buf| io.write(buf) }
144
+ close
145
+ end
146
+ end
@@ -0,0 +1,32 @@
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
+ class OMGDAV::InputWrapper # :nodoc:
6
+ attr_reader :bytes
7
+
8
+ def initialize(env, max = 65536)
9
+ @input = env["rack.input"]
10
+ @bytes = 0
11
+ @max = max
12
+ end
13
+
14
+ def read(*args)
15
+ rv = @input.read(*args)
16
+ if String === rv
17
+ @bytes += rv.size
18
+ if @bytes > @max
19
+ buf = ""
20
+ while @input.read(16384, buf)
21
+ # drain input
22
+ end
23
+ raise "too many bytes for XML: #@bytes > #@max"
24
+ end
25
+ end
26
+ rv
27
+ end
28
+
29
+ # no-op to fool Nokogiri
30
+ def close
31
+ end
32
+ end
@@ -0,0 +1,45 @@
1
+ # Copyright (C) 2012, Eric Wong <normalperson@yhbt.net>
2
+ # License: AGPLv3 or later (https://www.gnu.org/licenses/agpl-3.0.txt)
3
+ # :enddoc:
4
+ Sequel.migration do
5
+ up do
6
+ create_table(:paths) do
7
+ primary_key :id, type: Bignum
8
+ Integer :domain_id, null: false # MogileFS domain
9
+ TrueClass :collection, default: false
10
+ Bignum :parent_id, null: false # parent collection
11
+ Bignum :mtime, null: false
12
+ Bignum :created, null: false
13
+ Bignum :length, null: true
14
+ TrueClass :executable, null: false, default: false
15
+ String :name, null: false
16
+ String :dead_props, null: true, text: true
17
+ unique [ :domain_id, :parent_id, :name ]
18
+ end
19
+
20
+ create_table(:domains) do
21
+ primary_key :id
22
+ String :domain, null: false, unique: true
23
+ end
24
+
25
+ create_table(:prop_mappings) do
26
+ primary_key :id
27
+ String :value, null: false, unique: true
28
+ end
29
+
30
+ create_table(:copymove_locks) do
31
+ Integer :domain_id, null: false
32
+ Bignum :src_id, null: false
33
+ Bignum :dst_id, null: false
34
+ Bignum :lock_expire, null: false
35
+ unique [ :domain_id, :src_id, :dst_id ]
36
+ end
37
+ end
38
+
39
+ down do
40
+ drop_table(:paths)
41
+ drop_table(:domains)
42
+ drop_table(:prop_mappings)
43
+ drop_table(:copymove_locks)
44
+ end
45
+ end
@@ -0,0 +1,15 @@
1
+ # Copyright (C) 2012, Eric Wong <normalperson@yhbt.net>
2
+ # License: AGPLv3 or later (https://www.gnu.org/licenses/agpl-3.0.txt)
3
+ # :enddoc:
4
+ Sequel.migration do
5
+ up do
6
+ alter_table(:paths) do
7
+ add_column :contenttype, Integer, null: true, default: nil
8
+ end
9
+ end
10
+ down do
11
+ alter_table(:paths) do
12
+ drop_column :contenttype
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,14 @@
1
+ # Copyright (C) 2012, Eric Wong <normalperson@yhbt.net>
2
+ # License: AGPLv3 or later (https://www.gnu.org/licenses/agpl-3.0.txt)
3
+ # :enddoc:
4
+ Sequel.migration do
5
+ up do
6
+ create_table(:synctmp) do
7
+ primary_key :id, type: Bignum
8
+ end
9
+ end
10
+
11
+ down do
12
+ drop_table(:synctmp)
13
+ end
14
+ end
@@ -0,0 +1,28 @@
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::Mkcol
9
+ include OMGDAV::DB
10
+ include OMGDAV::RackUtil
11
+
12
+ def call_mkcol(env)
13
+ bytes = drain_input(env)
14
+ return r(415) if bytes > 0
15
+ parts = path_split(env)
16
+ return r(405) if col_resolve(parts)
17
+ basename = parts.pop or return r(403)
18
+ parent = col_resolve(parts) or return r(409)
19
+ if node = node_lookup(parent[:id], basename)
20
+ return r(403) unless node[:collection]
21
+ else
22
+ node = col_ensure(parent[:id], basename)
23
+ end
24
+ r(201)
25
+ rescue OMGDAV::TypeConflict
26
+ r(409)
27
+ end
28
+ end