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