rack_dav 0.1.2

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,168 @@
1
+ module RackDAV
2
+
3
+ class FileResource < Resource
4
+ include WEBrick::HTTPUtils
5
+
6
+ # If this is a collection, return the child resources.
7
+ def children
8
+ Dir[file_path + '/*'].map do |path|
9
+ child File.basename(path)
10
+ end
11
+ end
12
+
13
+ # Is this resource a collection?
14
+ def collection?
15
+ File.directory?(file_path)
16
+ end
17
+
18
+ # Does this recource exist?
19
+ def exist?
20
+ File.exist?(file_path)
21
+ end
22
+
23
+ # Return the creation time.
24
+ def creation_date
25
+ stat.ctime
26
+ end
27
+
28
+ # Return the time of last modification.
29
+ def last_modified
30
+ stat.mtime
31
+ end
32
+
33
+ # Set the time of last modification.
34
+ def last_modified=(time)
35
+ File.utime(Time.now, time, file_path)
36
+ end
37
+
38
+ # Return an Etag, an unique hash value for this resource.
39
+ def etag
40
+ sprintf('%x-%x-%x', stat.ino, stat.size, stat.mtime.to_i)
41
+ end
42
+
43
+ # Return the resource type.
44
+ #
45
+ # If this is a collection, return
46
+ # REXML::Element.new('D:collection')
47
+ def resource_type
48
+ if collection?
49
+ REXML::Element.new('D:collection')
50
+ end
51
+ end
52
+
53
+ # Return the mime type of this resource.
54
+ def content_type
55
+ if stat.directory?
56
+ "text/html"
57
+ else
58
+ mime_type(file_path, DefaultMimeTypes)
59
+ end
60
+ end
61
+
62
+ # Return the size in bytes for this resource.
63
+ def content_length
64
+ stat.size
65
+ end
66
+
67
+ # HTTP GET request.
68
+ #
69
+ # Write the content of the resource to the response.body.
70
+ def get(request, response)
71
+ if stat.directory?
72
+ response.body = ""
73
+ Rack::Directory.new(root).call(request.env)[2].each do |line|
74
+ response.body << line
75
+ end
76
+ response['Content-Length'] = response.body.size.to_s
77
+ else
78
+ file = Rack::File.new(nil)
79
+ file.path = file_path
80
+ response.body = file
81
+ end
82
+ end
83
+
84
+ # HTTP PUT request.
85
+ #
86
+ # Save the content of the request.body.
87
+ def put(request, response)
88
+ write(request.body)
89
+ end
90
+
91
+ # HTTP POST request.
92
+ #
93
+ # Usually forbidden.
94
+ def post(request, response)
95
+ raise HTTPStatus::Forbidden
96
+ end
97
+
98
+ # HTTP DELETE request.
99
+ #
100
+ # Delete this resource.
101
+ def delete
102
+ if stat.directory?
103
+ Dir.rmdir(file_path)
104
+ else
105
+ File.unlink(file_path)
106
+ end
107
+ end
108
+
109
+ # HTTP COPY request.
110
+ #
111
+ # Copy this resource to given destination resource.
112
+ def copy(dest)
113
+ if stat.directory?
114
+ dest.make_collection
115
+ else
116
+ open(file_path, "rb") do |file|
117
+ dest.write(file)
118
+ end
119
+ end
120
+ end
121
+
122
+ # HTTP MOVE request.
123
+ #
124
+ # Move this resource to given destination resource.
125
+ def move(dest)
126
+ copy(dest)
127
+ delete
128
+ end
129
+
130
+ # HTTP MKCOL request.
131
+ #
132
+ # Create this resource as collection.
133
+ def make_collection
134
+ Dir.mkdir(file_path)
135
+ end
136
+
137
+ # Write to this resource from given IO.
138
+ def write(io)
139
+ tempfile = "#{file_path}.#{Process.pid}.#{object_id}"
140
+
141
+ open(tempfile, "wb") do |file|
142
+ while part = io.read(8192)
143
+ file << part
144
+ end
145
+ end
146
+
147
+ File.rename(tempfile, file_path)
148
+ ensure
149
+ File.unlink(tempfile) rescue nil
150
+ end
151
+
152
+ private
153
+
154
+ def root
155
+ @options[:root]
156
+ end
157
+
158
+ def file_path
159
+ root + '/' + path
160
+ end
161
+
162
+ def stat
163
+ @stat ||= File.stat(file_path)
164
+ end
165
+
166
+ end
167
+
168
+ end
@@ -0,0 +1,34 @@
1
+ module RackDAV
2
+
3
+ class Handler
4
+
5
+ def initialize(options={})
6
+ @options = {
7
+ :resource_class => FileResource,
8
+ :root => Dir.pwd
9
+ }.merge(options)
10
+ end
11
+
12
+ def call(env)
13
+ request = Rack::Request.new(env)
14
+ response = Rack::Response.new
15
+
16
+ begin
17
+ controller = Controller.new(request, response, @options.dup)
18
+ controller.send(request.request_method.downcase)
19
+
20
+ rescue HTTPStatus::Status => status
21
+ response.status = status.code
22
+ end
23
+
24
+ # Strings in Ruby 1.9 are no longer enumerable. Rack still expects the response.body to be
25
+ # enumerable, however.
26
+ response.body = [response.body] if not response.body.respond_to? :each
27
+
28
+ response.status = response.status ? response.status.to_i : 200
29
+ response.finish
30
+ end
31
+
32
+ end
33
+
34
+ end
@@ -0,0 +1,108 @@
1
+ module RackDAV
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
+ RackDAV::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,180 @@
1
+ module RackDAV
2
+
3
+ class Resource
4
+ attr_reader :path, :options
5
+
6
+ def initialize(path, options)
7
+ @path = path
8
+ @options = options
9
+ end
10
+
11
+ # If this is a collection, return the child resources.
12
+ def children
13
+ raise NotImplementedError
14
+ end
15
+
16
+ # Is this resource a collection?
17
+ def collection?
18
+ raise NotImplementedError
19
+ end
20
+
21
+ # Does this recource exist?
22
+ def exist?
23
+ raise NotImplementedError
24
+ end
25
+
26
+ # Return the creation time.
27
+ def creation_date
28
+ raise NotImplementedError
29
+ end
30
+
31
+ # Return the time of last modification.
32
+ def last_modified
33
+ raise NotImplementedError
34
+ end
35
+
36
+ # Set the time of last modification.
37
+ def last_modified=(time)
38
+ raise NotImplementedError
39
+ end
40
+
41
+ # Return an Etag, an unique hash value for this resource.
42
+ def etag
43
+ raise NotImplementedError
44
+ end
45
+
46
+ # Return the resource type.
47
+ #
48
+ # If this is a collection, return
49
+ # REXML::Element.new('D:collection')
50
+ def resource_type
51
+ if collection?
52
+ REXML::Element.new('D:collection')
53
+ end
54
+ end
55
+
56
+ # Return the mime type of this resource.
57
+ def content_type
58
+ raise NotImplementedError
59
+ end
60
+
61
+ # Return the size in bytes for this resource.
62
+ def content_length
63
+ raise NotImplementedError
64
+ end
65
+
66
+ # HTTP GET request.
67
+ #
68
+ # Write the content of the resource to the response.body.
69
+ def get(request, response)
70
+ raise NotImplementedError
71
+ end
72
+
73
+ # HTTP PUT request.
74
+ #
75
+ # Save the content of the request.body.
76
+ def put(request, response)
77
+ raise NotImplementedError
78
+ end
79
+
80
+ # HTTP POST request.
81
+ #
82
+ # Usually forbidden.
83
+ def post(request, response)
84
+ raise NotImplementedError
85
+ end
86
+
87
+ # HTTP DELETE request.
88
+ #
89
+ # Delete this resource.
90
+ def delete
91
+ raise NotImplementedError
92
+ end
93
+
94
+ # HTTP COPY request.
95
+ #
96
+ # Copy this resource to given destination resource.
97
+ def copy(dest)
98
+ raise NotImplementedError
99
+ end
100
+
101
+ # HTTP MOVE request.
102
+ #
103
+ # Move this resource to given destination resource.
104
+ def move(dest)
105
+ copy(dest)
106
+ delete
107
+ end
108
+
109
+ # HTTP MKCOL request.
110
+ #
111
+ # Create this resource as collection.
112
+ def make_collection
113
+ raise NotImplementedError
114
+ end
115
+
116
+ def ==(other)
117
+ path == other.path
118
+ end
119
+
120
+ def name
121
+ File.basename(path)
122
+ end
123
+
124
+ def display_name
125
+ name
126
+ end
127
+
128
+ def child(name, option={})
129
+ self.class.new(path + '/' + name, options)
130
+ end
131
+
132
+ def property_names
133
+ %w(creationdate displayname getlastmodified getetag resourcetype getcontenttype getcontentlength)
134
+ end
135
+
136
+ def get_property(name)
137
+ case name
138
+ when 'resourcetype' then resource_type
139
+ when 'displayname' then display_name
140
+ when 'creationdate' then creation_date.xmlschema
141
+ when 'getcontentlength' then content_length.to_s
142
+ when 'getcontenttype' then content_type
143
+ when 'getetag' then etag
144
+ when 'getlastmodified' then last_modified.httpdate
145
+ end
146
+ end
147
+
148
+ def set_property(name, value)
149
+ case name
150
+ when 'resourcetype' then self.resource_type = value
151
+ when 'getcontenttype' then self.content_type = value
152
+ when 'getetag' then self.etag = value
153
+ when 'getlastmodified' then self.last_modified = Time.httpdate(value)
154
+ end
155
+ rescue ArgumentError
156
+ raise HTTPStatus::Conflict
157
+ end
158
+
159
+ def remove_property(name)
160
+ raise HTTPStatus::Forbidden
161
+ end
162
+
163
+ def parent
164
+ elements = @path.scan(/[^\/]+/)
165
+ return nil if elements.empty?
166
+ self.class.new('/' + elements[0..-2].to_a.join('/'), @options)
167
+ end
168
+
169
+ def descendants
170
+ list = []
171
+ children.each do |child|
172
+ list << child
173
+ list.concat(child.descendants)
174
+ end
175
+ list
176
+ end
177
+
178
+ end
179
+
180
+ end
data/lib/rack_dav.rb ADDED
@@ -0,0 +1,15 @@
1
+ require 'rubygems'
2
+ require 'builder'
3
+ require 'time'
4
+ require 'uri'
5
+ require 'rexml/document'
6
+ require 'webrick/httputils'
7
+
8
+ require 'rack'
9
+ require 'rack_dav/builder_namespace'
10
+ require 'rack_dav/http_status'
11
+ require 'rack_dav/resource'
12
+ require 'rack_dav/file_resource'
13
+ require 'rack_dav/handler'
14
+ require 'rack_dav/controller'
15
+
data/rack_dav.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'rack_dav'
3
+ s.version = '0.1.2'
4
+ s.summary = 'WebDAV handler for Rack'
5
+ s.author = 'Matthias Georgi'
6
+ s.email = 'matti.georgi@gmail.com'
7
+ s.homepage = 'http://www.matthias-georgi.de/rack_dav'
8
+ s.description = 'WebDAV handler for Rack'
9
+ s.require_path = 'lib'
10
+ s.executables << 'rack_dav'
11
+ s.has_rdoc = true
12
+ s.extra_rdoc_files = ['README.md']
13
+ s.files = %w{
14
+ .gitignore
15
+ LICENSE
16
+ rack_dav.gemspec
17
+ lib/rack_dav.rb
18
+ lib/rack_dav/file_resource.rb
19
+ lib/rack_dav/handler.rb
20
+ lib/rack_dav/controller.rb
21
+ lib/rack_dav/builder_namespace.rb
22
+ lib/rack_dav/http_status.rb
23
+ lib/rack_dav/resource.rb
24
+ bin/rack_dav
25
+ spec/handler_spec.rb
26
+ README.md
27
+ }
28
+ end