rack-webdav 0.4.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.
@@ -0,0 +1,37 @@
1
+ require 'time'
2
+ require 'rack/utils'
3
+ require 'rack/mime'
4
+
5
+ module RackWebDAV
6
+ # RackWebDAV::File simply allows us to use Rack::File but with the
7
+ # specific location we deem appropriate
8
+ class File < Rack::File
9
+ attr_accessor :path
10
+
11
+ alias :to_path :path
12
+
13
+ def initialize(path)
14
+ @path = path
15
+ end
16
+
17
+ def _call(env)
18
+ begin
19
+ if F.file?(@path) && F.readable?(@path)
20
+ serving(env)
21
+ else
22
+ raise Errno::EPERM
23
+ end
24
+ rescue SystemCallError
25
+ not_found
26
+ end
27
+ end
28
+
29
+ def not_found
30
+ body = "File not found: #{Rack::Utils.unescape(env["PATH_INFO"])}\n"
31
+ [404, {"Content-Type" => "text/plain",
32
+ "Content-Length" => body.size.to_s,
33
+ "X-Cascade" => "pass"},
34
+ [body]]
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,169 @@
1
+ require 'pstore'
2
+
3
+ module RackWebDAV
4
+ class FileResourceLock
5
+ attr_accessor :path
6
+ attr_accessor :token
7
+ attr_accessor :timeout
8
+ attr_accessor :depth
9
+ attr_accessor :user
10
+ attr_accessor :scope
11
+ attr_accessor :kind
12
+ attr_accessor :owner
13
+ attr_reader :created_at
14
+ attr_reader :root
15
+
16
+ class << self
17
+ def explicitly_locked?(path, croot=nil)
18
+ store = init_pstore(croot)
19
+ !!store.transaction(true){
20
+ store[:paths][path]
21
+ }
22
+ end
23
+
24
+ def implicitly_locked?(path, croot=nil)
25
+ store = init_pstore(croot)
26
+ !!store.transaction(true){
27
+ store[:paths].keys.detect do |check|
28
+ check.start_with?(path)
29
+ end
30
+ }
31
+ end
32
+
33
+ def explicit_locks(path, croot, args={})
34
+ end
35
+
36
+ def implict_locks(path)
37
+ end
38
+
39
+ def find_by_path(path, croot=nil)
40
+ lock = self.class.new(:path => path, :root => croot)
41
+ lock.token.nil? ? nil : lock
42
+ end
43
+
44
+ def find_by_token(token, croot=nil)
45
+ store = init_pstore(croot)
46
+ struct = store.transaction(true){
47
+ store[:tokens][token]
48
+ }
49
+ if !struct
50
+ struct = store.transaction(true) {
51
+ store[:tokens].keys.each { |k| token = k if k.include?(token) }
52
+ store[:tokens][token]
53
+ }
54
+ end
55
+
56
+ if struct
57
+ self.new(:path => struct[:path], :root => croot)
58
+ else
59
+ nil
60
+ end
61
+ end
62
+
63
+ def generate(path, user, token, croot)
64
+ lock = self.new(:root => croot)
65
+ lock.user = user
66
+ lock.path = path
67
+ lock.token = token
68
+ lock.save
69
+ lock
70
+ end
71
+
72
+ def root=(path)
73
+ @root = path
74
+ end
75
+
76
+ def root
77
+ @root || '/tmp/rack-webdav'
78
+ end
79
+
80
+ def init_pstore(croot)
81
+ path = File.join(croot, '.attribs', 'locks.pstore')
82
+ FileUtils.mkdir_p(File.dirname(path)) unless File.directory?(File.dirname(path))
83
+ store = IS_18 ? PStore.new(path) : PStore.new(path, true)
84
+ store.transaction do
85
+ unless(store[:paths])
86
+ store[:paths] = {}
87
+ store[:tokens] = {}
88
+ store.commit
89
+ end
90
+ end
91
+ store
92
+ end
93
+ end
94
+
95
+ def initialize(args={})
96
+ @path = args[:path]
97
+ @root = args[:root]
98
+ @owner = args[:owner]
99
+ @store = init_pstore(@root)
100
+ @max_timeout = args[:max_timeout] || 86400
101
+ @default_timeout = args[:max_timeout] || 60
102
+ load_if_exists!
103
+ @new_record = true if token.nil?
104
+ end
105
+
106
+ def owner?(user)
107
+ user == owner
108
+ end
109
+
110
+ def reload
111
+ load_if_exists
112
+ self
113
+ end
114
+
115
+ def remaining_timeout
116
+ t = timeout.to_i - (Time.now.to_i - created_at.to_i)
117
+ t < 0 ? 0 : t
118
+ end
119
+
120
+ def save
121
+ struct = {
122
+ :path => path,
123
+ :token => token,
124
+ :timeout => timeout,
125
+ :depth => depth,
126
+ :created_at => Time.now,
127
+ :owner => owner
128
+ }
129
+ @store.transaction do
130
+ @store[:paths][path] = struct
131
+ @store[:tokens][token] = struct
132
+ @store.commit
133
+ end
134
+ @new_record = false
135
+ self
136
+ end
137
+
138
+ def destroy
139
+ @store.transaction do
140
+ @store[:paths].delete(path)
141
+ @store[:tokens].delete(token)
142
+ @store.commit
143
+ end
144
+ nil
145
+ end
146
+
147
+ private
148
+
149
+ def load_if_exists!
150
+ struct = @store.transaction do
151
+ @store[:paths][path]
152
+ end
153
+ if(struct)
154
+ @path = struct[:path]
155
+ @token = struct[:token]
156
+ @timeout = struct[:timeout]
157
+ @depth = struct[:depth]
158
+ @created_at = struct[:created_at]
159
+ @owner = struct[:owner]
160
+ end
161
+ self
162
+ end
163
+
164
+ def init_pstore(croot=nil)
165
+ self.class.init_pstore(croot || @root)
166
+ end
167
+
168
+ end
169
+ end
@@ -0,0 +1,63 @@
1
+ require 'rack-webdav/logger'
2
+
3
+ module RackWebDAV
4
+
5
+ class Handler
6
+ include RackWebDAV::HTTPStatus
7
+ def initialize(options={})
8
+ @options = options.dup
9
+ unless(@options[:resource_class])
10
+ require 'rack-webdav/resources/file_resource'
11
+ @options[:resource_class] = FileResource
12
+ @options[:root] ||= Dir.pwd
13
+ end
14
+ Logger.set(*@options[:log_to])
15
+ end
16
+
17
+ def call(env)
18
+ begin
19
+ start = Time.now
20
+ request = Rack::Request.new(env)
21
+ response = Rack::Response.new
22
+
23
+ Logger.info "Processing WebDAV request: #{request.path} (for #{request.ip} at #{Time.now}) [#{request.request_method}]"
24
+
25
+ controller = nil
26
+ begin
27
+ controller_class = @options[:controller_class] || Controller
28
+ controller = controller_class.new(request, response, @options.dup)
29
+ controller.authenticate
30
+ res = controller.send(request.request_method.downcase)
31
+ response.status = res.code if res.respond_to?(:code)
32
+ rescue HTTPStatus::Unauthorized => status
33
+ response.body = controller.resource.respond_to?(:authentication_error_msg) ? controller.resource.authentication_error_msg : 'Not Authorized'
34
+ response['WWW-Authenticate'] = "Basic realm=\"#{controller.resource.respond_to?(:authentication_realm) ? controller.resource.authentication_realm : 'Locked content'}\""
35
+ response.status = status.code
36
+ rescue HTTPStatus::Status => status
37
+ response.status = status.code
38
+ end
39
+
40
+ # Strings in Ruby 1.9 are no longer enumerable. Rack still expects the response.body to be
41
+ # enumerable, however.
42
+
43
+ response['Content-Length'] = response.body.to_s.length unless response['Content-Length'] || !response.body.is_a?(String)
44
+ response.body = [response.body] unless response.body.respond_to? :each
45
+ response.status = response.status ? response.status.to_i : 200
46
+ response.headers.keys.each{|k| response.headers[k] = response[k].to_s}
47
+
48
+ # Apache wants the body dealt with, so just read it and junk it
49
+ buf = true
50
+ buf = request.body.read(8192) while buf
51
+
52
+ Logger.debug "Response in string form. Outputting contents: \n#{response.body}" if response.body.is_a?(String)
53
+ Logger.info "Completed in: #{((Time.now.to_f - start.to_f) * 1000).to_i} ms | #{response.status} [#{request.url}]"
54
+ response
55
+ rescue Exception => e
56
+ Logger.error "WebDAV Error: #{e}\n#{e.backtrace.join("\n")}"
57
+ raise e
58
+ end
59
+ end
60
+
61
+ end
62
+
63
+ end
@@ -0,0 +1,108 @@
1
+ module RackWebDAV
2
+
3
+ module HTTPStatus
4
+
5
+ class Status < Exception
6
+
7
+ class << self
8
+ attr_accessor :code, :reason_phrase
9
+ alias_method :to_i, :code
10
+
11
+ def status_line
12
+ "#{code} #{reason_phrase}"
13
+ end
14
+
15
+ end
16
+
17
+ def code
18
+ self.class.code
19
+ end
20
+
21
+ def reason_phrase
22
+ self.class.reason_phrase
23
+ end
24
+
25
+ def status_line
26
+ self.class.status_line
27
+ end
28
+
29
+ def to_i
30
+ self.class.to_i
31
+ end
32
+
33
+ end
34
+
35
+ StatusMessage = {
36
+ 100 => 'Continue',
37
+ 101 => 'Switching Protocols',
38
+ 102 => 'Processing',
39
+ 200 => 'OK',
40
+ 201 => 'Created',
41
+ 202 => 'Accepted',
42
+ 203 => 'Non-Authoritative Information',
43
+ 204 => 'No Content',
44
+ 205 => 'Reset Content',
45
+ 206 => 'Partial Content',
46
+ 207 => 'Multi-Status',
47
+ 300 => 'Multiple Choices',
48
+ 301 => 'Moved Permanently',
49
+ 302 => 'Found',
50
+ 303 => 'See Other',
51
+ 304 => 'Not Modified',
52
+ 305 => 'Use Proxy',
53
+ 307 => 'Temporary Redirect',
54
+ 400 => 'Bad Request',
55
+ 401 => 'Unauthorized',
56
+ 402 => 'Payment Required',
57
+ 403 => 'Forbidden',
58
+ 404 => 'Not Found',
59
+ 405 => 'Method Not Allowed',
60
+ 406 => 'Not Acceptable',
61
+ 407 => 'Proxy Authentication Required',
62
+ 408 => 'Request Timeout',
63
+ 409 => 'Conflict',
64
+ 410 => 'Gone',
65
+ 411 => 'Length Required',
66
+ 412 => 'Precondition Failed',
67
+ 413 => 'Request Entity Too Large',
68
+ 414 => 'Request-URI Too Large',
69
+ 415 => 'Unsupported Media Type',
70
+ 416 => 'Request Range Not Satisfiable',
71
+ 417 => 'Expectation Failed',
72
+ 422 => 'Unprocessable Entity',
73
+ 423 => 'Locked',
74
+ 424 => 'Failed Dependency',
75
+ 500 => 'Internal Server Error',
76
+ 501 => 'Not Implemented',
77
+ 502 => 'Bad Gateway',
78
+ 503 => 'Service Unavailable',
79
+ 504 => 'Gateway Timeout',
80
+ 505 => 'HTTP Version Not Supported',
81
+ 507 => 'Insufficient Storage'
82
+ }
83
+
84
+ StatusMessage.each do |code, reason_phrase|
85
+ klass = Class.new(Status)
86
+ klass.code = code
87
+ klass.reason_phrase = reason_phrase
88
+ klass_name = reason_phrase.gsub(/[ \-]/,'')
89
+ const_set(klass_name, klass)
90
+ end
91
+
92
+ end
93
+
94
+ end
95
+
96
+
97
+ module Rack
98
+ class Response
99
+ module Helpers
100
+ RackWebDAV::HTTPStatus::StatusMessage.each do |code, reason_phrase|
101
+ name = reason_phrase.gsub(/[ \-]/,'_').downcase
102
+ define_method(name + '?') do
103
+ @status == code
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,22 @@
1
+ require 'rack-webdav/interceptor_resource'
2
+ module RackWebDAV
3
+ class Interceptor
4
+ def initialize(app, args={})
5
+ @roots = args[:mappings].keys
6
+ @args = args
7
+ @app = app
8
+ @intercept_methods = %w(OPTIONS PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK)
9
+ @intercept_methods -= args[:ignore_methods] if args[:ignore_methods]
10
+ end
11
+
12
+ def call(env)
13
+ path = env['PATH_INFO'].downcase
14
+ method = env['REQUEST_METHOD'].upcase
15
+ app = nil
16
+ if(@roots.detect{|x| path =~ /^#{Regexp.escape(x.downcase)}\/?/}.nil? && @intercept_methods.include?(method))
17
+ app = RackWebDAV::Handler.new(:resource_class => InterceptorResource, :mappings => @args[:mappings], :log_to => @args[:log_to])
18
+ end
19
+ app ? app.call(env) : @app.call(env)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,119 @@
1
+ require 'digest/sha1'
2
+
3
+ module RackWebDAV
4
+
5
+ class InterceptorResource < Resource
6
+ attr_reader :path, :options
7
+
8
+ def initialize(*args)
9
+ super
10
+ @root_paths = @options[:mappings].keys
11
+ @mappings = @options[:mappings]
12
+ end
13
+
14
+ def children
15
+ childs = @root_paths.find_all{|x|x =~ /^#{Regexp.escape(@path)}/}
16
+ childs = childs.map{|a| child a.gsub(/^#{Regexp.escape(@path)}/, '').split('/').delete_if{|x|x.empty?}.first }.flatten
17
+ end
18
+
19
+ def collection?
20
+ true if exist?
21
+ end
22
+
23
+ def exist?
24
+ !@root_paths.find_all{|x| x =~ /^#{Regexp.escape(@path)}/}.empty?
25
+ end
26
+
27
+ def creation_date
28
+ Time.now
29
+ end
30
+
31
+ def last_modified
32
+ Time.now
33
+ end
34
+
35
+ def last_modified=(time)
36
+ Time.now
37
+ end
38
+
39
+ def etag
40
+ Digest::SHA1.hexdigest(@path)
41
+ end
42
+
43
+ def content_type
44
+ 'text/html'
45
+ end
46
+
47
+ def content_length
48
+ 0
49
+ end
50
+
51
+ def get(request, response)
52
+ raise Forbidden
53
+ end
54
+
55
+ def put(request, response)
56
+ raise Forbidden
57
+ end
58
+
59
+ def post(request, response)
60
+ raise Forbidden
61
+ end
62
+
63
+ def delete
64
+ raise Forbidden
65
+ end
66
+
67
+ def copy(dest)
68
+ raise Forbidden
69
+ end
70
+
71
+ def move(dest)
72
+ raise Forbidden
73
+ end
74
+
75
+ def make_collection
76
+ raise Forbidden
77
+ end
78
+
79
+ def ==(other)
80
+ path == other.path
81
+ end
82
+
83
+ def name
84
+ ::File.basename(path)
85
+ end
86
+
87
+ def display_name
88
+ ::File.basename(path.to_s)
89
+ end
90
+
91
+ def child(name, option={})
92
+ new_path = path.dup
93
+ new_path = '/' + new_path unless new_path[0,1] == '/'
94
+ new_path.slice!(-1) if new_path[-1,1] == '/'
95
+ name = '/' + name unless name[-1,1] == '/'
96
+ new_path = "#{new_path}#{name}"
97
+ new_public = public_path.dup
98
+ new_public = '/' + new_public unless new_public[0,1] == '/'
99
+ new_public.slice!(-1) if new_public[-1,1] == '/'
100
+ new_public = "#{new_public}#{name}"
101
+ if(key = @root_paths.find{|x| new_path =~ /^#{Regexp.escape(x.downcase)}\/?/})
102
+ @mappings[key][:resource_class].new(new_public, new_path.gsub(key, ''), request, response, {:root_uri_path => key, :user => @user}.merge(options).merge(@mappings[key]))
103
+ else
104
+ self.class.new(new_public, new_path, request, response, {:user => @user}.merge(options))
105
+ end
106
+ end
107
+
108
+ def descendants
109
+ list = []
110
+ children.each do |child|
111
+ list << child
112
+ list.concat(child.descendants)
113
+ end
114
+ list
115
+ end
116
+
117
+ end
118
+
119
+ end