lib/rack-webdav/resource.rb in rack-webdav-0.4.3 vs lib/rack-webdav/resource.rb in rack-webdav-0.4.4
- old
+ new
@@ -1,484 +1,189 @@
-require 'uuidtools'
-require 'rack-webdav/http_status'
-
module RackWebDAV
-
- class LockFailure < RuntimeError
- attr_reader :path_status
- def initialize(*args)
- super(*args)
- @path_status = {}
- end
-
- def add_failure(path, status)
- @path_status[path] = status
- end
- end
-
+
class Resource
- attr_reader :path, :options, :public_path, :request,
- :response, :propstat_relative_path, :root_xml_attributes
- attr_accessor :user
- @@blocks = {}
-
- class << self
-
- # This lets us define a bunch of before and after blocks that are
- # either called before all methods on the resource, or only specific
- # methods on the resource
- def method_missing(*args, &block)
- class_sym = self.name.to_sym
- @@blocks[class_sym] ||= {:before => {}, :after => {}}
- m = args.shift
- parts = m.to_s.split('_')
- type = parts.shift.to_s.to_sym
- method = parts.empty? ? nil : parts.join('_').to_sym
- if(@@blocks[class_sym][type] && block_given?)
- if(method)
- @@blocks[class_sym][type][method] ||= []
- @@blocks[class_sym][type][method] << block
- else
- @@blocks[class_sym][type][:'__all__'] ||= []
- @@blocks[class_sym][type][:'__all__'] << block
- end
- else
- raise NoMethodError.new("Undefined method #{m} for class #{self}")
- end
- end
-
- end
-
- include RackWebDAV::HTTPStatus
-
- # public_path:: Path received via request
- # path:: Internal resource path (Only different from public path when using root_uri's for webdav)
- # request:: Rack::Request
- # options:: Any options provided for this resource
- # Creates a new instance of the resource.
- # NOTE: path and public_path will only differ if the root_uri has been set for the resource. The
- # controller will strip out the starting path so the resource can easily determine what
- # it is working on. For example:
- # request -> /my/webdav/directory/actual/path
- # public_path -> /my/webdav/directory/actual/path
- # path -> /actual/path
- # NOTE: Customized Resources should not use initialize for setup. Instead
- # use the #setup method
- def initialize(public_path, path, request, response, options)
- @skip_alias = [
- :authenticate, :authentication_error_msg,
- :authentication_realm, :path, :options,
- :public_path, :request, :response, :user,
- :user=, :setup
- ]
- @public_path = public_path.dup
- @path = path.dup
- @propstat_relative_path = !!options.delete(:propstat_relative_path)
- @root_xml_attributes = options.delete(:root_xml_attributes) || {}
+
+ attr_reader :path, :options
+
+ def initialize(path, request, response, options)
+ @path = path
@request = request
@response = response
- unless(options.has_key?(:lock_class))
- require 'rack-webdav/lock_store'
- @lock_class = LockStore
- else
- @lock_class = options[:lock_class]
- raise NameError.new("Unknown lock type constant provided: #{@lock_class}") unless @lock_class.nil? || defined?(@lock_class)
- end
- @options = options.dup
- @max_timeout = options[:max_timeout] || 86400
- @default_timeout = options[:default_timeout] || 60
- @user = @options[:user] || request.ip
- setup if respond_to?(:setup)
- public_methods(false).each do |method|
- next if @skip_alias.include?(method.to_sym) || method[0,4] == 'DAV_' || method[0,5] == '_DAV_'
- self.class.class_eval "alias :'_DAV_#{method}' :'#{method}'"
- self.class.class_eval "undef :'#{method}'"
- end
- @runner = lambda do |class_sym, kind, method_name|
- [:'__all__', method_name.to_sym].each do |sym|
- if(@@blocks[class_sym] && @@blocks[class_sym][kind] && @@blocks[class_sym][kind][sym])
- @@blocks[class_sym][kind][sym].each do |b|
- args = [self, sym == :'__all__' ? method_name : nil].compact
- b.call(*args)
- end
- end
- end
- end
+ @options = options
end
-
- # This allows us to call before and after blocks
- def method_missing(*args)
- result = nil
- orig = args.shift
- class_sym = self.class.name.to_sym
- m = orig.to_s[0,5] == '_DAV_' ? orig : "_DAV_#{orig}" # If hell is doing the same thing over and over and expecting a different result this is a hell preventer
- raise NoMethodError.new("Undefined method: #{orig} for class #{self}.") unless respond_to?(m)
- @runner.call(class_sym, :before, orig)
- result = send m, *args
- @runner.call(class_sym, :after, orig)
- result
- end
-
- # Returns if resource supports locking
- def supports_locking?
- false #true
- end
# If this is a collection, return the child resources.
def children
- NotImplemented
+ raise NotImplementedError
end
# Is this resource a collection?
def collection?
- NotImplemented
+ raise NotImplementedError
end
- # Does this resource exist?
+ # Does this recource exist?
def exist?
- NotImplemented
+ raise NotImplementedError
end
-
- # Does the parent resource exist?
- def parent_exists?
- parent.exist?
- end
-
- # Is the parent resource a collection?
- def parent_collection?
- parent.collection?
- end
-
+
# Return the creation time.
def creation_date
- raise NotImplemented
+ raise NotImplementedError
end
# Return the time of last modification.
def last_modified
- raise NotImplemented
+ raise NotImplementedError
end
-
+
# Set the time of last modification.
def last_modified=(time)
- # Is this correct?
- raise NotImplemented
+ raise NotImplementedError
end
# Return an Etag, an unique hash value for this resource.
def etag
- raise NotImplemented
+ raise NotImplementedError
end
- # Return the resource type. Generally only used to specify
- # resource is a collection.
+ # Return the resource type.
+ #
+ # If this is a collection, return a collection element
def resource_type
- :collection if collection?
+ if collection?
+ Nokogiri::XML::fragment('<D:collection xmlns:D="DAV:"/>').children.first
+ end
end
# Return the mime type of this resource.
def content_type
- raise NotImplemented
+ raise NotImplementedError
end
# Return the size in bytes for this resource.
def content_length
- raise NotImplemented
+ raise NotImplementedError
end
# HTTP GET request.
#
# Write the content of the resource to the response.body.
- def get(request, response)
- NotImplemented
+ def get
+ raise NotImplementedError
end
# HTTP PUT request.
#
# Save the content of the request.body.
- def put(request, response)
- NotImplemented
+ def put
+ raise NotImplementedError
end
-
+
# HTTP POST request.
#
# Usually forbidden.
- def post(request, response)
- NotImplemented
+ def post
+ raise NotImplementedError
end
-
+
# HTTP DELETE request.
#
# Delete this resource.
def delete
- NotImplemented
+ raise NotImplementedError
end
-
+
# HTTP COPY request.
#
# Copy this resource to given destination resource.
- def copy(dest, overwrite=false)
- NotImplemented
+ def copy(dest)
+ raise NotImplementedError
end
-
+
# HTTP MOVE request.
#
# Move this resource to given destination resource.
- def move(dest, overwrite=false)
- NotImplemented
+ def move(dest)
+ copy(dest)
+ delete
end
-
- # args:: Hash of lock arguments
- # Request for a lock on the given resource. A valid lock should lock
- # all descendents. Failures should be noted and returned as an exception
- # using LockFailure.
- # Valid args keys: :timeout -> requested timeout
- # :depth -> lock depth
- # :scope -> lock scope
- # :type -> lock type
- # :owner -> lock owner
- # Should return a tuple: [lock_time, locktoken] where lock_time is the
- # given timeout
- # NOTE: See section 9.10 of RFC 4918 for guidance about
- # how locks should be generated and the expected responses
- # (http://www.webdav.org/specs/rfc4918.html#rfc.section.9.10)
-
- def lock(args)
- unless(@lock_class)
- NotImplemented
- else
- unless(parent_exists?)
- Conflict
- else
- lock_check(args[:scope])
- lock = @lock_class.explicit_locks(@path).find{|l| l.scope == args[:scope] && l.kind == args[:type] && l.user == @user}
- unless(lock)
- token = UUIDTools::UUID.random_create.to_s
- lock = @lock_class.generate(@path, @user, token)
- lock.scope = args[:scope]
- lock.kind = args[:type]
- lock.owner = args[:owner]
- lock.depth = args[:depth].is_a?(Symbol) ? args[:depth] : args[:depth].to_i
- if(args[:timeout])
- lock.timeout = args[:timeout] <= @max_timeout && args[:timeout] > 0 ? args[:timeout] : @max_timeout
- else
- lock.timeout = @default_timeout
- end
- lock.save if lock.respond_to? :save
- end
- begin
- lock_check(args[:type])
- rescue RackWebDAV::LockFailure => lock_failure
- lock.destroy
- raise lock_failure
- rescue HTTPStatus::Status => status
- status
- end
- [lock.remaining_timeout, lock.token]
- end
- end
- end
- # lock_scope:: scope of lock
- # Check if resource is locked. Raise RackWebDAV::LockFailure if locks are in place.
- def lock_check(lock_scope=nil)
- return unless @lock_class
- if(@lock_class.explicitly_locked?(@path))
- raise Locked if @lock_class.explicit_locks(@path).find_all{|l|l.scope == 'exclusive' && l.user != @user}.size > 0
- elsif(@lock_class.implicitly_locked?(@path))
- if(lock_scope.to_s == 'exclusive')
- locks = @lock_class.implicit_locks(@path)
- failure = RackWebDAV::LockFailure.new("Failed to lock: #{@path}")
- locks.each do |lock|
- failure.add_failure(@path, Locked)
- end
- raise failure
- else
- locks = @lock_class.implict_locks(@path).find_all{|l| l.scope == 'exclusive' && l.user != @user}
- if(locks.size > 0)
- failure = LockFailure.new("Failed to lock: #{@path}")
- locks.each do |lock|
- failure.add_failure(@path, Locked)
- end
- raise failure
- end
- end
- end
- end
-
- # token:: Lock token
- # Remove the given lock
- def unlock(token)
- unless(@lock_class)
- NotImplemented
- else
- token = token.slice(1, token.length - 2)
- if(token.nil? || token.empty?)
- BadRequest
- else
- lock = @lock_class.find_by_token(token)
- if(lock.nil? || lock.user != @user)
- Forbidden
- elsif(lock.path !~ /^#{Regexp.escape(@path)}.*$/)
- Conflict
- else
- lock.destroy
- NoContent
- end
- end
- end
- end
-
-
+ # HTTP MKCOL request.
+ #
# Create this resource as collection.
def make_collection
- NotImplemented
+ raise NotImplementedError
end
- # other:: Resource
- # Returns if current resource is equal to other resource
def ==(other)
path == other.path
end
- # Name of the resource
def name
File.basename(path)
end
- # Name of the resource to be displayed to the client
def display_name
name
end
-
- # Available properties
- def properties
- %w(creationdate displayname getlastmodified getetag resourcetype getcontenttype getcontentlength).collect do |prop|
- {:name => prop, :ns_href => 'DAV:'}
- end
+
+ def child(name, option={})
+ self.class.new(path + '/' + name, @request, @response, options)
end
-
- # name:: String - Property name
- # Returns the value of the given property
- def get_property(element)
- raise NotImplemented if (element[:ns_href] != 'DAV:')
- case element[:name]
+
+ def lockable?
+ self.respond_to?(:lock) && self.respond_to?(:unlock)
+ end
+
+ def property_names
+ %w(creationdate displayname getlastmodified getetag resourcetype getcontenttype getcontentlength)
+ end
+
+ def get_property(name)
+ case name
when 'resourcetype' then resource_type
when 'displayname' then display_name
- when 'creationdate' then use_ms_compat_creationdate? ? creation_date.httpdate : creation_date.xmlschema
+ when 'creationdate' then creation_date.xmlschema
when 'getcontentlength' then content_length.to_s
when 'getcontenttype' then content_type
when 'getetag' then etag
when 'getlastmodified' then last_modified.httpdate
- else raise NotImplemented
+ else self.get_custom_property(name) if self.respond_to?(:get_custom_property)
end
end
- # name:: String - Property name
- # value:: New value
- # Set the property to the given value
- def set_property(element, value)
- raise NotImplemented if (element[:ns_href] != 'DAV:')
- case element[:name]
+ def set_property(name, value)
+ case name
when 'resourcetype' then self.resource_type = value
when 'getcontenttype' then self.content_type = value
when 'getetag' then self.etag = value
when 'getlastmodified' then self.last_modified = Time.httpdate(value)
- else raise NotImplemented
+ else self.set_custom_property(name, value) if self.respond_to?(:set_custom_property)
end
+ rescue Errno::EOPNOTSUPP
+ # nothing done
+ rescue ArgumentError
+ raise HTTPStatus::Conflict
end
- # name:: Property name
- # Remove the property from the resource
- def remove_property(element)
- Forbidden
+ def remove_property(name)
+ raise HTTPStatus::Forbidden if property_names.include?(name)
end
- # name:: Name of child
- # Create a new child with the given name
- # NOTE:: Include trailing '/' if child is collection
- def child(name)
- new_public = public_path.dup
- new_public = new_public + '/' unless new_public[-1,1] == '/'
- new_public = '/' + new_public unless new_public[0,1] == '/'
- new_path = path.dup
- new_path = new_path + '/' unless new_path[-1,1] == '/'
- new_path = '/' + new_path unless new_path[0,1] == '/'
- self.class.new("#{new_public}#{name}", "#{new_path}#{name}", request, response, options.merge(:user => @user))
- end
-
- # Return parent of this resource
def parent
- unless(@path.to_s.empty?)
- self.class.new(
- File.split(@public_path).first,
- File.split(@path).first,
- @request,
- @response,
- @options.merge(
- :user => @user
- )
- )
- end
+ elements = @path.scan(/[^\/]+/)
+ return nil if elements.empty?
+ self.class.new('/' + elements[0..-2].to_a.join('/'), @options)
end
-
- # Return list of descendants
+
def descendants
list = []
children.each do |child|
list << child
list.concat(child.descendants)
end
list
- end
-
- # Index page template for GETs on collection
- def index_page
- '<html><head> <title>%s</title>
- <meta http-equiv="content-type" content="text/html; charset=utf-8" /></head>
- <body> <h1>%s</h1> <hr /> <table> <tr> <th class="name">Name</th>
- <th class="size">Size</th> <th class="type">Type</th>
- <th class="mtime">Last Modified</th> </tr> %s </table> <hr /> </body></html>'
- end
-
- # Does client allow GET redirection
- # TODO: Get a comprehensive list in here.
- # TODO: Allow this to be dynamic so users can add regexes to match if they know of a client
- # that can be supported that is not listed.
- def allows_redirect?
- [
- %r{cyberduck}i,
- %r{konqueror}i
- ].any? do |regexp|
- (request.respond_to?(:user_agent) ? request.user_agent : request.env['HTTP_USER_AGENT']).to_s =~ regexp
- end
- end
-
- def use_compat_mkcol_response?
- @options[:compat_mkcol] || @options[:compat_all]
- end
-
- # Returns true if using an MS client
- def use_ms_compat_creationdate?
- if(@options[:compat_ms_mangled_creationdate] || @options[:compat_all])
- is_ms_client?
- end
- end
-
- # Basic user agent testing for MS authored client
- def is_ms_client?
- [%r{microsoft-webdav}i, %r{microsoft office}i].any? do |regexp|
- (request.respond_to?(:user_agent) ? request.user_agent : request.env['HTTP_USER_AGENT']).to_s =~ regexp
- end
- end
-
- protected
-
- # Returns authentication credentials if available in form of [username,password]
- # TODO: Add support for digest
- def auth_credentials
- auth = Rack::Auth::Basic::Request.new(request.env)
- auth.provided? && auth.basic? ? auth.credentials : [nil,nil]
end
end
end