Dav4rack sources replaces with correspondig gem

This commit is contained in:
Karel Picman 2014-03-27 10:36:43 +01:00
parent 58eda96713
commit 4350868cef
16 changed files with 24 additions and 1936 deletions

View File

@ -4,6 +4,7 @@ gem 'rubyzip', '>= 1.0.0'
gem 'zip-zip' # Just to avoid 'cannot load such file -- zip/zip' error
gem 'simple_enum'
gem 'uuidtools', '~> 2.1.1'
gem 'dav4rack', '=0.2.11'
group :production do
gem 'nokogiri', '>= 1.5.10'

View File

@ -1,9 +0,0 @@
require 'time'
require 'uri'
require 'nokogiri'
require 'rack'
require 'dav4rack/http_status'
require 'dav4rack/resource'
require 'dav4rack/handler'
require 'dav4rack/controller'

View File

@ -1,545 +0,0 @@
require 'uri'
module DAV4Rack
class Controller
include DAV4Rack::HTTPStatus
attr_reader :request, :response, :resource
# request:: Rack::Request
# response:: Rack::Response
# options:: Options hash
# Create a new Controller.
# NOTE: options will be passed to Resource
def initialize(request, response, options={})
raise Forbidden if request.path_info.include?('..')
@request = request
@response = response
@options = options
@resource = resource_class.new(actual_path, implied_path, @request, @response, @options)
end
# s:: string
# Escape URL string
def url_format(resource)
ret = URI.escape(resource.public_path)
if resource.collection? and ret[-1,1] != '/'
ret += '/'
end
ret
end
# s:: string
# Unescape URL string
def url_unescape(s)
URI.unescape(s)
end
# Return response to OPTIONS
def options
response["Allow"] = 'OPTIONS,HEAD,GET,PUT,POST,DELETE,PROPFIND,PROPPATCH,MKCOL,COPY,MOVE,LOCK,UNLOCK'
response["Dav"] = "1, 2"
response["Ms-Author-Via"] = "DAV"
OK
end
# Return response to HEAD
def head
if(resource.exist?)
response['Etag'] = resource.etag
response['Content-Type'] = resource.content_type
response['Last-Modified'] = resource.last_modified.httpdate
OK
else
NotFound
end
end
# Return response to GET
def get
if(resource.exist?)
res = resource.get(request, response)
if(res == OK && !resource.collection?)
response['Etag'] = resource.etag
response['Content-Type'] = resource.content_type
response['Content-Length'] = resource.content_length.to_s
response['Last-Modified'] = resource.last_modified.httpdate
end
res
else
NotFound
end
end
# Return response to PUT
def put
if(resource.collection?)
Forbidden
elsif(!resource.parent_exists? || !resource.parent.collection?)
Conflict
else
resource.lock_check
status = resource.put(request, response)
response['Location'] = "#{scheme}://#{host}:#{port}#{url_format(resource)}" if status == Created
response.body = response['Location']
status
end
end
# Return response to POST
def post
resource.post(request, response)
end
# Return response to DELETE
def delete
if(resource.exist?)
resource.lock_check
resource.delete
else
NotFound
end
end
# Return response to MKCOL
def mkcol
resource.lock_check
status = resource.make_collection
gen_url = "#{scheme}://#{host}:#{port}#{url_format(resource)}" if status == Created
if(resource.use_compat_mkcol_response?)
multistatus do |xml|
xml.response do
xml.href gen_url
xml.status "#{http_version} #{status.status_line}"
end
end
else
response['Location'] = gen_url
status
end
end
# Return response to COPY
def copy
move(:copy)
end
# args:: Only argument used: :copy
# Move Resource to new location. If :copy is provided,
# Resource will be copied (implementation ease)
def move(*args)
unless(resource.exist?)
NotFound
else
resource.lock_check unless args.include?(:copy)
destination = url_unescape(env['HTTP_DESTINATION'].sub(%r{https?://([^/]+)}, ''))
dest_host = $1
if(dest_host && dest_host.gsub(/:\d{2,5}$/, '') != request.host)
BadGateway
elsif(destination == resource.public_path)
Forbidden
else
collection = resource.collection?
dest = resource_class.new(destination, clean_path(destination), @request, @response, @options.merge(:user => resource.user))
status = nil
if(args.include?(:copy))
status = resource.copy(dest, overwrite)
else
return Conflict unless depth.is_a?(Symbol) || depth > 1
status = resource.move(dest, overwrite)
end
response['Location'] = "#{scheme}://#{host}:#{port}#{url_format(dest)}" if status == Created
# RFC 2518
if collection
multistatus do |xml|
xml.response do
xml.href "#{scheme}://#{host}:#{port}#{url_format(status == Created ? dest : resource)}"
xml.status "#{http_version} #{status.status_line}"
end
end
else
status
end
end
end
end
# Return respoonse to PROPFIND
def propfind
unless(resource.exist?)
NotFound
else
unless(request_document.xpath("//#{ns}propfind/#{ns}allprop").empty?)
names = resource.property_names
else
names = (
ns.empty? ? request_document.remove_namespaces! : request_document
).xpath(
"//#{ns}propfind/#{ns}prop"
).children.find_all{ |item|
item.element? && item.name.start_with?(ns)
}.map{ |item|
item.name.sub("#{ns}::", '')
}
raise BadRequest if names.empty?
names = resource.property_names if names.empty?
end
multistatus do |xml|
find_resources.each do |resource|
xml.response do
unless(resource.propstat_relative_path)
xml.href "#{scheme}://#{host}:#{port}#{url_format(resource)}"
else
xml.href url_format(resource)
end
propstats(xml, get_properties(resource, names))
end
end
end
end
end
# Return response to PROPPATCH
def proppatch
unless(resource.exist?)
NotFound
else
resource.lock_check
prop_rem = request_match('/propertyupdate/remove/prop').children.map{|n| [n.name] }
prop_set = request_match('/propertyupdate/set/prop').children.map{|n| [n.name, n.text] }
multistatus do |xml|
find_resources.each do |resource|
xml.response do
xml.href "#{scheme}://#{host}:#{port}#{url_format(resource)}"
propstats(xml, set_properties(resource, prop_set))
end
end
end
end
end
# Lock current resource
# NOTE: This will pass an argument hash to Resource#lock and
# wait for a success/failure response.
def lock
lockinfo = request_document.xpath("//#{ns}lockinfo")
asked = {}
asked[:timeout] = request.env['Timeout'].split(',').map{|x|x.strip} if request.env['Timeout']
asked[:depth] = depth
unless([0, :infinity].include?(asked[:depth]))
BadRequest
else
asked[:scope] = lockinfo.xpath("//#{ns}lockscope").children.find_all{|n|n.element?}.map{|n|n.name}.first
asked[:type] = lockinfo.xpath("#{ns}locktype").children.find_all{|n|n.element?}.map{|n|n.name}.first
asked[:owner] = lockinfo.xpath("//#{ns}owner/#{ns}href").children.map{|n|n.text}.first
begin
lock_time, locktoken = resource.lock(asked)
render_xml(:prop) do |xml|
xml.lockdiscovery do
xml.activelock do
if(asked[:scope])
xml.lockscope do
xml.send(asked[:scope])
end
end
if(asked[:type])
xml.locktype do
xml.send(asked[:type])
end
end
xml.depth asked[:depth].to_s
xml.timeout lock_time ? "Second-#{lock_time}" : 'infinity'
xml.locktoken do
xml.href locktoken
end
if(asked[:owner])
xml.owner asked[:owner]
end
end
end
end
response.status = resource.exist? ? OK : Created
rescue LockFailure => e
multistatus do |xml|
e.path_status.each_pair do |path, status|
xml.response do
xml.href path
xml.status "#{http_version} #{status.status_line}"
end
end
end
end
end
end
# Unlock current resource
def unlock
resource.unlock(lock_token)
end
# Perform authentication
# NOTE: Authentication will only be performed if the Resource
# has defined an #authenticate method
def authenticate
authed = true
if(resource.respond_to?(:authenticate, true))
authed = false
uname = nil
password = nil
if(request.env['HTTP_AUTHORIZATION'])
auth = Rack::Auth::Basic::Request.new(request.env)
if(auth.basic? && auth.credentials)
uname = auth.credentials[0]
password = auth.credentials[1]
end
end
authed = resource.send(:authenticate, uname, password)
end
raise Unauthorized unless authed
end
# ************************************************************
# private methods
private
# Request environment variables
def env
@request.env
end
# Current request scheme (http/https)
def scheme
request.scheme
end
# Request host
def host
request.host
end
# Request port
def port
request.port
end
# Class of the resource in use
def resource_class
@options[:resource_class]
end
# Root URI path for the resource
def root_uri_path
@options[:root_uri_path]
end
# Returns Resource path with root URI removed
def implied_path
return clean_path(@request.path_info.dup) unless @request.path_info.empty?
c_path = clean_path(@request.path.dup)
return c_path if c_path.length != @request.path.length
#if we're here then it's probably down to thin
return @request.path.dup.gsub!(/^#{Regexp.escape(@request.script_name)}/, '') unless @request.script_name.empty?
return c_path #This will probably result in a processing error if we hit here
end
# x:: request path
# Unescapes path and removes root URI if applicable
def clean_path(x)
ip = url_unescape(x)
ip.gsub!(/^#{Regexp.escape(root_uri_path)}/, '') if root_uri_path
ip
end
# Unescaped request path
def actual_path
url_unescape(@request.path.dup)
end
# Lock token if provided by client
def lock_token
env['HTTP_LOCK_TOKEN'] || nil
end
# Requested depth
def depth
d = env['HTTP_DEPTH']
if(d =~ /^\d+$/)
d = d.to_i
else
d = :infinity
end
d
end
# Current HTTP version being used
def http_version
env['HTTP_VERSION'] || env['SERVER_PROTOCOL'] || 'HTTP/1.0'
end
# Overwrite is allowed
def overwrite
env['HTTP_OVERWRITE'].to_s.upcase != 'F'
end
# Find resources at depth requested
def find_resources(with_current_resource=true)
ary = nil
case depth
when 0
ary = []
when 1
ary = resource.children
else
ary = resource.descendants
end
with_current_resource ? [resource] + ary : ary
end
# XML parsed request
def request_document
@request_document ||= Nokogiri.XML(request.body.read)
rescue
raise BadRequest
end
# Namespace being used within XML document
# TODO: Make this better
def ns
_ns = ''
if(request_document && request_document.root && request_document.root.namespace_definitions.size > 0)
_ns = request_document.root.namespace_definitions.first.prefix.to_s
_ns += ':' unless _ns.empty?
end
_ns
end
# pattern:: XPath pattern
# Search XML document for given XPath
# TODO: Stripping namespaces not so great
def request_match(pattern)
request_document.remove_namespaces!.xpath(pattern, request_document.root.namespaces)
end
# root_type:: Root tag name
# Render XML and set Rack::Response#body= to final XML
def render_xml(root_type)
raise ArgumentError.new 'Expecting block' unless block_given?
doc = Nokogiri::XML::Builder.new do |xml_base|
xml_base.send(root_type.to_s, {'xmlns:D' => 'DAV:'}.merge(resource.root_xml_attributes)) do
xml_base.parent.namespace = xml_base.parent.namespace_definitions.first
xml = xml_base['D']
yield xml
end
end
if(@options[:pretty_xml])
response.body = doc.to_xml
else
response.body = doc.to_xml(
:save_with => Nokogiri::XML::Node::SaveOptions::AS_XML
)
end
response["Content-Type"] = 'text/xml; charset="utf-8"'
response["Content-Length"] = response.body.size.to_s
end
# block:: block
# Creates a multistatus response using #render_xml and
# returns the correct status
def multistatus(&block)
render_xml(:multistatus, &block)
MultiStatus
end
# xml:: Nokogiri::XML::Builder
# errors:: Array of errors
# Crafts responses for errors
def response_errors(xml, errors)
for path, status in errors
xml.response do
xml.href "#{scheme}://#{host}:#{port}#{URI.escape(path)}"
xml.status "#{http_version} #{status.status_line}"
end
end
end
# resource:: Resource
# names:: Property names
# Returns array of property values for given names
def get_properties(resource, names)
stats = Hash.new { |h, k| h[k] = [] }
for name in names
begin
val = resource.get_property(name)
stats[OK].push [name, val]
rescue Unauthorized => u
raise u
rescue Status
stats[$!.class] << name
end
end
stats
end
# resource:: Resource
# pairs:: name value pairs
# Sets the given properties
def set_properties(resource, pairs)
stats = Hash.new { |h, k| h[k] = [] }
for name, value in pairs
begin
stats[OK] << [name, resource.set_property(name, value)]
rescue Unauthorized => u
raise u
rescue Status
stats[$!.class] << name
end
end
stats
end
# xml:: Nokogiri::XML::Builder
# stats:: Array of stats
# Build propstats response
def propstats(xml, stats)
return if stats.empty?
for status, props in stats
xml.propstat do
xml.prop do
for name, value in props
if(value.is_a?(Nokogiri::XML::DocumentFragment))
xml.__send__ :insert, value
elsif(value.is_a?(Nokogiri::XML::Node))
xml.send(name) do
xml_convert(xml, value)
end
elsif(value.is_a?(Symbol))
xml.send(name) do
xml.send(value)
end
else
xml.send(name, value)
end
end
end
xml.status "#{http_version} #{status.status_line}"
end
end
end
# xml:: Nokogiri::XML::Builder
# element:: Nokogiri::XML::Element
# Converts element into proper text
def xml_convert(xml, element)
xml.doc.root.add_child(element)
end
end
end

View File

@ -1,37 +0,0 @@
require 'time'
require 'rack/utils'
require 'rack/mime'
module DAV4Rack
# DAV4Rack::File simply allows us to use Rack::File but with the
# specific location we deem appropriate
class File < Rack::File
attr_accessor :path
alias :to_path :path
def initialize(path)
@path = path
end
def _call(env)
begin
if F.file?(@path) && F.readable?(@path)
serving
else
raise Errno::EPERM
end
rescue SystemCallError
not_found
end
end
def not_found
body = "File not found: #{Rack::Utils.unescape(env["PATH_INFO"])}\n"
[404, {"Content-Type" => "text/plain",
"Content-Length" => body.size.to_s,
"X-Cascade" => "pass"},
[body]]
end
end
end

View File

@ -1,257 +0,0 @@
require 'webrick/httputils'
module DAV4Rack
class FileResource < Resource
include WEBrick::HTTPUtils
# If this is a collection, return the child resources.
def children
Dir[file_path + '/*'].map do |path|
child File.basename(path)
end
end
# Is this resource a collection?
def collection?
File.directory?(file_path)
end
# Does this recource exist?
def exist?
File.exist?(file_path)
end
# Return the creation time.
def creation_date
stat.ctime
end
# Return the time of last modification.
def last_modified
stat.mtime
end
# Set the time of last modification.
def last_modified=(time)
File.utime(Time.now, time, file_path)
end
# Return an Etag, an unique hash value for this resource.
def etag
sprintf('%x-%x-%x', stat.ino, stat.size, stat.mtime.to_i)
end
# Return the mime type of this resource.
def content_type
if stat.directory?
"text/html"
else
mime_type(file_path, DefaultMimeTypes)
end
end
# Return the size in bytes for this resource.
def content_length
stat.size
end
# HTTP GET request.
#
# Write the content of the resource to the response.body.
def get(request, response)
raise NotFound unless exist?
if stat.directory?
response.body = ""
Rack::Directory.new(root).call(request.env)[2].each do |line|
response.body << line
end
response['Content-Length'] = response.body.bytesize.to_s
else
file = Rack::File.new(root)
response.body = file
end
OK
end
# HTTP PUT request.
#
# Save the content of the request.body.
def put(request, response)
write(request.body)
Created
end
# HTTP POST request.
#
# Usually forbidden.
def post(request, response)
raise HTTPStatus::Forbidden
end
# HTTP DELETE request.
#
# Delete this resource.
def delete
if stat.directory?
FileUtils.rm_rf(file_path)
else
File.unlink(file_path)
end
NoContent
end
# HTTP COPY request.
#
# Copy this resource to given destination resource.
# Copy this resource to given destination resource.
def copy(dest, overwrite)
if(collection?)
if(dest.exist?)
if(dest.collection? && overwrite)
FileUtils.cp_r(file_path, dest.send(:file_path))
Created
else
if(overwrite)
FileUtils.rm(dest.send(:file_path))
FileUtils.cp_r(file_path, dest.send(:file_path))
NoContent
else
PreconditionFailed
end
end
else
FileUtils.cp_r(file_path, dest.send(:file_path))
Created
end
else
if(dest.exist? && !overwrite)
PreconditionFailed
else
if(File.directory?(File.dirname(dest.send(:file_path))))
new = !dest.exist?
if(dest.collection? && dest.exist?)
FileUtils.rm_rf(dest.send(:file_path))
end
FileUtils.cp(file_path, dest.send(:file_path).sub(/\/$/, ''))
new ? Created : NoContent
else
Conflict
end
end
end
end
# HTTP MOVE request.
#
# Move this resource to given destination resource.
def move(*args)
result = copy(*args)
delete if [Created, NoContent].include?(result)
result
end
# HTTP MKCOL request.
#
# Create this resource as collection.
def make_collection
if(request.body.read.to_s == '')
if(File.directory?(file_path))
MethodNotAllowed
else
if(File.directory?(File.dirname(file_path)))
Dir.mkdir(file_path)
Created
else
Conflict
end
end
else
UnsupportedMediaType
end
end
# Write to this resource from given IO.
def write(io)
tempfile = "#{file_path}.#{Process.pid}.#{object_id}"
open(tempfile, "wb") do |file|
while part = io.read(8192)
file << part
end
end
File.rename(tempfile, file_path)
ensure
File.unlink(tempfile) rescue nil
end
# name:: String - Property name
# Returns the value of the given property
def get_property(name)
super || custom_props(name)
end
# name:: String - Property name
# value:: New value
# Set the property to the given value
def set_property(name, value)
super || set_custom_props(name,value)
end
protected
def set_custom_props(key,val)
prop_hash[key.to_sym] = val
File.open(prop_path, 'w') do |file|
file.write(YAML.dump(prop_hash))
end
end
def custom_props(key)
prop_hash[key.to_sym]
end
def prop_path
path = File.join(root, '.props', File.dirname(file_path), File.basename(file_path))
unless(File.directory?(File.dirname(path)))
FileUtils.mkdir_p(File.dirname(path))
end
path
end
def prop_hash
unless(@_prop_hash)
if(File.exists?(prop_path))
@_prop_hash = YAML.load(File.read(prop_path))
else
@_prop_hash = {}
end
end
@_prop_hash
end
def authenticate(user, pass)
if(options[:username])
options[:username] == user && options[:password] == pass
else
true
end
end
def root
@options[:root]
end
def file_path
File.join(root, path)
end
def stat
@stat ||= File.stat(file_path)
end
end
end

View File

@ -1,64 +0,0 @@
require 'dav4rack/logger'
module DAV4Rack
class Handler
include DAV4Rack::HTTPStatus
def initialize(options={})
@options = options.dup
unless(@options[:resource_class])
require 'dav4rack/file_resource'
@options[:resource_class] = FileResource
@options[:root] ||= Dir.pwd
end
Logger.set(*@options[:log_to])
end
def call(env)
begin
start = Time.now
request = Rack::Request.new(env)
response = Rack::Response.new
Logger.info "Processing WebDAV request: #{request.path} (for #{request.ip} at #{Time.now}) [#{request.request_method}]"
controller = nil
begin
controller_class = @options[:controller_class] || Controller
controller = controller_class.new(request, response, @options.dup)
controller.authenticate
res = controller.send(request.request_method.downcase)
response.status = res.code if res.respond_to?(:code)
rescue HTTPStatus::Unauthorized => status
response.body = controller.resource.respond_to?(:authentication_error_msg) ? controller.resource.authentication_error_msg : 'Not Authorized'
response['WWW-Authenticate'] = "Basic realm=\"#{controller.resource.respond_to?(:authentication_realm) ? controller.resource.authentication_realm : 'Locked content'}\""
response.status = status.code
rescue HTTPStatus::Status => status
response.status = status.code
end
# Strings in Ruby 1.9 are no longer enumerable. Rack still expects the response.body to be
# enumerable, however.
response['Content-Length'] = response.body.to_s.length unless response['Content-Length'] || !response.body.is_a?(String)
response.body = [response.body] unless response.body.respond_to? :each
response.status = response.status ? response.status.to_i : 200
response.headers.keys.each{|k| response.headers[k] = response[k].to_s}
# Apache wants the body dealt with, so just read it and junk it
buf = true
buf = request.body.read(8192) while buf
Logger.debug "Response in string form. Outputting contents: \n#{response.body}" if response.body.is_a?(String)
Logger.info "Completed in: #{((Time.now.to_f - start.to_f) * 1000).to_i} ms | #{response.status} [#{request.url}]"
response.body.is_a?(Rack::File) ? response.body.call(env) : response.finish
rescue Exception => e
Logger.error "WebDAV Error: #{e}\n#{e.backtrace.join("\n")}"
raise e
end
end
end
end

View File

@ -1,108 +0,0 @@
module DAV4Rack
module HTTPStatus
class Status < Exception
class << self
attr_accessor :code, :reason_phrase
alias_method :to_i, :code
def status_line
"#{code} #{reason_phrase}"
end
end
def code
self.class.code
end
def reason_phrase
self.class.reason_phrase
end
def status_line
self.class.status_line
end
def to_i
self.class.to_i
end
end
StatusMessage = {
100 => 'Continue',
101 => 'Switching Protocols',
102 => 'Processing',
200 => 'OK',
201 => 'Created',
202 => 'Accepted',
203 => 'Non-Authoritative Information',
204 => 'No Content',
205 => 'Reset Content',
206 => 'Partial Content',
207 => 'Multi-Status',
300 => 'Multiple Choices',
301 => 'Moved Permanently',
302 => 'Found',
303 => 'See Other',
304 => 'Not Modified',
305 => 'Use Proxy',
307 => 'Temporary Redirect',
400 => 'Bad Request',
401 => 'Unauthorized',
402 => 'Payment Required',
403 => 'Forbidden',
404 => 'Not Found',
405 => 'Method Not Allowed',
406 => 'Not Acceptable',
407 => 'Proxy Authentication Required',
408 => 'Request Timeout',
409 => 'Conflict',
410 => 'Gone',
411 => 'Length Required',
412 => 'Precondition Failed',
413 => 'Request Entity Too Large',
414 => 'Request-URI Too Large',
415 => 'Unsupported Media Type',
416 => 'Request Range Not Satisfiable',
417 => 'Expectation Failed',
422 => 'Unprocessable Entity',
423 => 'Locked',
424 => 'Failed Dependency',
500 => 'Internal Server Error',
501 => 'Not Implemented',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
504 => 'Gateway Timeout',
505 => 'HTTP Version Not Supported',
507 => 'Insufficient Storage'
}
StatusMessage.each do |code, reason_phrase|
klass = Class.new(Status)
klass.code = code
klass.reason_phrase = reason_phrase
klass_name = reason_phrase.gsub(/[ \-]/,'')
const_set(klass_name, klass)
end
end
end
module Rack
class Response
module Helpers
DAV4Rack::HTTPStatus::StatusMessage.each do |code, reason_phrase|
name = reason_phrase.gsub(/[ \-]/,'_').downcase
define_method(name + '?') do
@status == code
end
end
end
end
end

View File

@ -1,22 +0,0 @@
require 'dav4rack/interceptor_resource'
module DAV4Rack
class Interceptor
def initialize(app, args={})
@roots = args[:mappings].keys
@args = args
@app = app
@intercept_methods = %w(OPTIONS PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK)
@intercept_methods -= args[:ignore_methods] if args[:ignore_methods]
end
def call(env)
path = env['PATH_INFO'].downcase
method = env['REQUEST_METHOD'].upcase
app = nil
if(@roots.detect{|x| path =~ /^#{Regexp.escape(x.downcase)}\/?/}.nil? && @intercept_methods.include?(method))
app = DAV4Rack::Handler.new(:resource_class => InterceptorResource, :mappings => @args[:mappings], :log_to => @args[:log_to])
end
app ? app.call(env) : @app.call(env)
end
end
end

View File

@ -1,119 +0,0 @@
require 'digest/sha1'
module DAV4Rack
class InterceptorResource < Resource
attr_reader :path, :options
def initialize(*args)
super
@root_paths = @options[:mappings].keys
@mappings = @options[:mappings]
end
def children
childs = @root_paths.find_all{|x|x =~ /^#{Regexp.escape(@path)}/}
childs = childs.map{|a| child a.gsub(/^#{Regexp.escape(@path)}/, '').split('/').delete_if{|x|x.empty?}.first }.flatten
end
def collection?
true if exist?
end
def exist?
!@root_paths.find_all{|x| x =~ /^#{Regexp.escape(@path)}/}.empty?
end
def creation_date
Time.now
end
def last_modified
Time.now
end
def last_modified=(time)
Time.now
end
def etag
Digest::SHA1.hexdigest(@path)
end
def content_type
'text/html'
end
def content_length
0
end
def get(request, response)
raise Forbidden
end
def put(request, response)
raise Forbidden
end
def post(request, response)
raise Forbidden
end
def delete
raise Forbidden
end
def copy(dest)
raise Forbidden
end
def move(dest)
raise Forbidden
end
def make_collection
raise Forbidden
end
def ==(other)
path == other.path
end
def name
::File.basename(path)
end
def display_name
::File.basename(path.to_s)
end
def child(name, option={})
new_path = path.dup
new_path = '/' + new_path unless new_path[0,1] == '/'
new_path.slice!(-1) if new_path[-1,1] == '/'
name = '/' + name unless name[-1,1] == '/'
new_path = "#{new_path}#{name}"
new_public = public_path.dup
new_public = '/' + new_public unless new_public[0,1] == '/'
new_public.slice!(-1) if new_public[-1,1] == '/'
new_public = "#{new_public}#{name}"
if(key = @root_paths.find{|x| new_path =~ /^#{Regexp.escape(x.downcase)}\/?/})
@mappings[key][:resource_class].new(new_public, new_path.gsub(key, ''), request, response, {:root_uri_path => key, :user => @user}.merge(options).merge(@mappings[key]))
else
self.class.new(new_public, new_path, request, response, {:user => @user}.merge(options))
end
end
def descendants
list = []
children.each do |child|
list << child
list.concat(child.descendants)
end
list
end
end
end

View File

@ -1,40 +0,0 @@
module DAV4Rack
class Lock
def initialize(args={})
@args = args
@store = nil
@args[:created_at] = Time.now
@args[:updated_at] = Time.now
end
def store
@store
end
def store=(s)
raise TypeError.new 'Expecting LockStore' unless s.respond_to? :remove
@store = s
end
def destroy
if(@store)
@store.remove(self)
end
end
def remaining_timeout
@args[:timeout].to_i - (Time.now.to_i - @args[:created_at].to_i)
end
def method_missing(*args)
if(@args.has_key?(args.first.to_sym))
@args[args.first.to_sym]
elsif(args.first.to_s[-1,1] == '=')
@args[args.first.to_s[0, args.first.to_s.length - 1].to_sym] = args[1]
else
raise NoMethodError.new "Undefined method #{args.first} for #{self}"
end
end
end
end

View File

@ -1,61 +0,0 @@
require 'dav4rack/lock'
module DAV4Rack
class LockStore
class << self
def create
@locks_by_path = {}
@locks_by_token = {}
end
def add(lock)
@locks_by_path[lock.path] = lock
@locks_by_token[lock.token] = lock
end
def remove(lock)
@locks_by_path.delete(lock.path)
@locks_by_token.delete(lock.token)
end
def find_by_path(path)
@locks_by_path.map do |lpath, lock|
lpath == path && lock.remaining_timeout > 0 ? lock : nil
end.compact.first
end
def find_by_token(token)
@locks_by_token.map do |ltoken, lock|
ltoken == token && lock.remaining_timeout > 0 ? lock : nil
end.compact.first
end
def explicit_locks(path)
@locks_by_path.map do |lpath, lock|
lpath == path && lock.remaining_timeout > 0 ? lock : nil
end.compact
end
def implicit_locks(path)
@locks_by_path.map do |lpath, lock|
lpath =~ /^#{Regexp.escape(path)}/ && lock.remaining_timeout > 0 && lock.depth > 0 ? lock : nil
end.compact
end
def explicitly_locked?(path)
self.explicit_locks(path).size > 0
end
def implicitly_locked?(path)
self.implicit_locks(path).size > 0
end
def generate(path, user, token)
l = Lock.new(:path => path, :user => user, :token => token)
l.store = self
add(l)
l
end
end
end
end
DAV4Rack::LockStore.create

View File

@ -1,30 +0,0 @@
require 'logger'
module DAV4Rack
# This is a simple wrapper for the Logger class. It allows easy access
# to log messages from the library.
class Logger
class << self
# args:: Arguments for Logger -> [path, level] (level is optional) or a Logger instance
# Set the path to the log file.
def set(*args)
if(%w(info debug warn fatal).all?{|meth| args.first.respond_to?(meth)})
@@logger = args.first
elsif(args.first.respond_to?(:to_s) && !args.first.to_s.empty?)
@@logger = ::Logger.new(args.first.to_s, 'weekly')
elsif(args.first)
raise 'Invalid type specified for logger'
end
if(args.size > 1)
@@logger.level = args[1]
end
end
def method_missing(*args)
if(defined? @@logger)
@@logger.send *args
end
end
end
end
end

View File

@ -1,148 +0,0 @@
require 'net/http'
require 'uri'
require 'digest/sha1'
require 'rack/file'
module DAV4Rack
class RemoteFile < Rack::File
attr_accessor :path
alias :to_path :path
# path:: path to file (Actual path, preferably a URL since this is a *REMOTE* file)
# args:: Hash of arguments:
# :size -> Integer - number of bytes
# :mime_type -> String - mime type
# :last_modified -> String/Time - Time of last modification
# :sendfile -> True or String to define sendfile header variation
# :cache_directory -> Where to store cached files
# :cache_ref -> Reference to be used for cache file name (useful for changing URLs like S3)
# :sendfile_prefix -> String directory prefix. Eg: 'webdav' will result in: /wedav/#{path.sub('http://', '')}
# :sendfile_fail_gracefully -> Boolean if true will simply proxy if unable to determine proper sendfile
def initialize(path, args={})
@path = path
@args = args
@heads = {}
@cache_file = args[:cache_directory] ? cache_file_path : nil
@redefine_prefix = nil
if(@cache_file && File.exists?(@cache_file))
@root = ''
@path_info = @cache_file
@path = @path_info
elsif(args[:sendfile])
@redefine_prefix = 'sendfile'
@sendfile_header = args[:sendfile].is_a?(String) ? args[:sendfile] : nil
else
setup_remote
end
do_redefines(@redefine_prefix) if @redefine_prefix
end
# env:: Environment variable hash
# Process the call
def call(env)
serving(env)
end
# env:: Environment variable hash
# Return an empty result with the proper header information
def sendfile_serving(env)
header = @sendfile_header || env['sendfile.type'] || env['HTTP_X_SENDFILE_TYPE']
unless(header)
raise 'Failed to determine proper sendfile header value' unless @args[:sendfile_fail_gracefully]
setup_remote
do_redefines('remote')
call(env)
end
prefix = (@args[:sendfile_prefix] || env['HTTP_X_ACCEL_REMOTE_MAPPING']).to_s.sub(/^\//, '').sub(/\/$/, '')
[200, {
"Last-Modified" => last_modified,
"Content-Type" => content_type,
"Content-Length" => size,
"Redirect-URL" => @path,
"Redirect-Host" => @path.scan(%r{^https?://([^/\?]+)}).first.first,
header => "/#{prefix}"
},
['']]
end
# env:: Environment variable hash
# Return self to be processed
def remote_serving(e)
[200, {
"Last-Modified" => last_modified,
"Content-Type" => content_type,
"Content-Length" => size
}, self]
end
# Get the remote file
def remote_each
if(@store)
yield @store
else
@con.request_get(@call_path) do |res|
res.read_body(@store) do |part|
@cf.write part if @cf
yield part
end
end
end
end
# Size based on remote headers or given size
def size
@heads['content-length'] || @size
end
private
# Content type based on provided or remote headers
def content_type
@mime_type || @heads['content-type']
end
# Last modified type based on provided, remote headers or current time
def last_modified
@heads['last-modified'] || @modified || Time.now.httpdate
end
# Builds the path for the cached file
def cache_file_path
raise IOError.new 'Write permission is required for cache directory' unless File.writable?(@args[:cache_directory])
"#{@args[:cache_directory]}/#{Digest::SHA1.hexdigest((@args[:cache_ref] || @path).to_s + size.to_s + last_modified.to_s)}.cache"
end
# prefix:: prefix of methods to be redefined
# Redefine methods to do what we want in the proper situation
def do_redefines(prefix)
self.public_methods.each do |method|
m = method.to_s.dup
next unless m.slice!(0, prefix.to_s.length + 1) == "#{prefix}_"
self.class.class_eval "undef :'#{m}'"
self.class.class_eval "alias :'#{m}' :'#{method}'"
end
end
# Sets up all the requirements for proxying a remote file
def setup_remote
if(@cache_file)
begin
@cf = File.open(@cache_file, 'w+')
rescue
@cf = nil
end
end
@uri = URI.parse(@path)
@con = Net::HTTP.new(@uri.host, @uri.port)
@call_path = @uri.path + (@uri.query ? "?#{@uri.query}" : '')
res = @con.request_get(@call_path)
@heads = res.to_hash
res.value
@store = nil
@redefine_prefix = 'remote'
end
end
end

View File

@ -1,469 +0,0 @@
require 'uuidtools'
require 'dav4rack/http_status'
module DAV4Rack
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 DAV4Rack::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) || {}
@request = request
@response = response
unless(options.has_key?(:lock_class))
require 'dav4rack/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
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
# If this is a collection, return the child resources.
def children
NotImplemented
end
# Is this resource a collection?
def collection?
NotImplemented
end
# Does this resource exist?
def exist?
NotImplemented
end
# Does the parent resource exist?
def parent_exists?
parent.exist?
end
# Return the creation time.
def creation_date
raise NotImplemented
end
# Return the time of last modification.
def last_modified
raise NotImplemented
end
# Set the time of last modification.
def last_modified=(time)
# Is this correct?
raise NotImplemented
end
# Return an Etag, an unique hash value for this resource.
def etag
raise NotImplemented
end
# Return the resource type. Generally only used to specify
# resource is a collection.
def resource_type
:collection if collection?
end
# Return the mime type of this resource.
def content_type
raise NotImplemented
end
# Return the size in bytes for this resource.
def content_length
raise NotImplemented
end
# HTTP GET request.
#
# Write the content of the resource to the response.body.
def get(request, response)
NotImplemented
end
# HTTP PUT request.
#
# Save the content of the request.body.
def put(request, response)
NotImplemented
end
# HTTP POST request.
#
# Usually forbidden.
def post(request, response)
NotImplemented
end
# HTTP DELETE request.
#
# Delete this resource.
def delete
NotImplemented
end
# HTTP COPY request.
#
# Copy this resource to given destination resource.
def copy(dest, overwrite=false)
NotImplemented
end
# HTTP MOVE request.
#
# Move this resource to given destination resource.
def move(dest, overwrite=false)
NotImplemented
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 DAV4Rack::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 DAV4Rack::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 = DAV4Rack::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
# Create this resource as collection.
def make_collection
NotImplemented
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 property_names
%w(creationdate displayname getlastmodified getetag resourcetype getcontenttype getcontentlength)
end
# name:: String - Property name
# Returns the value of the given property
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 'getcontentlength' then content_length.to_s
when 'getcontenttype' then content_type
when 'getetag' then etag
when 'getlastmodified' then last_modified.httpdate
end
end
# name:: String - Property name
# value:: New value
# Set the property to the given value
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)
end
end
# name:: Property name
# Remove the property from the resource
def remove_property(name)
Forbidden
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
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

View File

@ -1,17 +0,0 @@
module DAV4Rack
class Version
attr_reader :major, :minor, :tiny
def initialize(version)
version = version.split('.')
@major, @minor, @tiny = [version[0].to_i, version[1].to_i, version[2].to_i]
end
def to_s
"#{@major}.#{@minor}.#{@tiny}"
end
end
VERSION = Version.new('0.2.11')
end

View File

@ -1,6 +1,7 @@
# Redmine plugin for Document Management System "Features"
#
# Copyright (C) 2012 Daniel Munn <dan.munn@munnster.co.uk>
# Copyright (C) 2011-14 Karel Picman <karel.picman@kontron.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@ -23,9 +24,9 @@ module RedmineDmsf
# Overload default options
def options
raise NotFound unless resource.exist?
response["Allow"] = 'OPTIONS,HEAD,GET,PUT,POST,DELETE,PROPFIND,PROPPATCH,MKCOL,COPY,MOVE,LOCK,UNLOCK'
response["Dav"] = "1,2,3"
response["Ms-Author-Via"] = "DAV"
response['Allow'] = 'OPTIONS,HEAD,GET,PUT,POST,DELETE,PROPFIND,PROPPATCH,MKCOL,COPY,MOVE,LOCK,UNLOCK'
response['Dav'] = '1,2,3'
response['Ms-Author-Via'] = 'DAV'
OK
end
@ -94,10 +95,22 @@ module RedmineDmsf
end
end
response.body = doc.to_xml
response["Content-Type"] = 'application/xml; charset="utf-8"'
response["Content-Length"] = response.body.bytesize.to_s
response['Content-Type'] = 'application/xml; charset="utf-8"'
response['Content-Length'] = response.body.bytesize.to_s
end
# Returns Resource path with root URI removed
def implied_path
return clean_path(@request.path_info.dup) unless @request.path_info.empty?
c_path = clean_path(@request.path.dup)
return c_path if c_path.length != @request.path.length
# If we're here then it's probably down to thin
return @request.path.dup.gsub!(/^#{Regexp.escape(@request.script_name)}/, '') unless @request.script_name.empty?
return c_path # This will probably result in a processing error if we hit here
end
private
def ns(opt_head = '')