Dav4Rack gem replaced with a local copy
This commit is contained in:
parent
8ece0ac007
commit
a8ce29e2c0
@ -6,6 +6,10 @@ Changelog for Redmine DMSF
|
|||||||
|
|
||||||
Javascript on pages is loaded asynchronously
|
Javascript on pages is loaded asynchronously
|
||||||
Obsolete Dav4Rack gem replaced with an up to date fork by Planio (Consequently WebDAV caching has been removed, sorry...)
|
Obsolete Dav4Rack gem replaced with an up to date fork by Planio (Consequently WebDAV caching has been removed, sorry...)
|
||||||
|
Instead of usage their gem in the Gemfile:
|
||||||
|
gem 'dav4rack', git: 'https://github.com/planio-gmbh/dav4rack.git', branch: 'master'
|
||||||
|
The library is a part of the project (lib/dav4rack).
|
||||||
|
Cloned from gem https://github.com/planio-gmbh/dav4rack.git
|
||||||
Project members can be chosen as recipients when sending documents by email
|
Project members can be chosen as recipients when sending documents by email
|
||||||
Responsive view
|
Responsive view
|
||||||
Direct editing of document in MS Office
|
Direct editing of document in MS Office
|
||||||
|
|||||||
11
Gemfile
11
Gemfile
@ -26,8 +26,17 @@ gem 'rubyzip', '>= 1.0.0'
|
|||||||
gem 'zip-zip'
|
gem 'zip-zip'
|
||||||
gem 'simple_enum'
|
gem 'simple_enum'
|
||||||
gem 'uuidtools'
|
gem 'uuidtools'
|
||||||
gem 'dav4rack', git: 'https://github.com/planio-gmbh/dav4rack.git', branch: 'master'
|
|
||||||
gem 'dalli'
|
gem 'dalli'
|
||||||
|
|
||||||
|
# Redmine extensions
|
||||||
unless %w(easyproject easy_gantt).any? { |plugin| Dir.exist?(File.expand_path("../../#{plugin}", __FILE__)) }
|
unless %w(easyproject easy_gantt).any? { |plugin| Dir.exist?(File.expand_path("../../#{plugin}", __FILE__)) }
|
||||||
gem 'redmine_extensions', '~> 0.2.5'
|
gem 'redmine_extensions', '~> 0.2.5'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Dav4Rack
|
||||||
|
gem 'rake'
|
||||||
|
gem 'mongo'
|
||||||
|
gem 'unicorn'
|
||||||
|
gem 'byebug', platforms: :mri
|
||||||
|
gem 'ruby-prof'
|
||||||
|
gem 'ox'
|
||||||
|
|||||||
15
lib/dav4rack.rb
Normal file
15
lib/dav4rack.rb
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
require 'time'
|
||||||
|
require 'uri'
|
||||||
|
require 'nokogiri'
|
||||||
|
require 'ox'
|
||||||
|
|
||||||
|
require 'rack'
|
||||||
|
require 'dav4rack/utils'
|
||||||
|
require 'dav4rack/http_status'
|
||||||
|
require 'dav4rack/resource'
|
||||||
|
require 'dav4rack/handler'
|
||||||
|
require 'dav4rack/controller'
|
||||||
|
|
||||||
|
module DAV4Rack
|
||||||
|
IS_18 = RUBY_VERSION[0,3] == '1.8'
|
||||||
|
end
|
||||||
426
lib/dav4rack/controller.rb
Normal file
426
lib/dav4rack/controller.rb
Normal file
@ -0,0 +1,426 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'uri'
|
||||||
|
require 'dav4rack/destination_header'
|
||||||
|
require 'dav4rack/request'
|
||||||
|
require 'dav4rack/xml_elements'
|
||||||
|
require 'dav4rack/xml_response'
|
||||||
|
|
||||||
|
module DAV4Rack
|
||||||
|
|
||||||
|
class Controller
|
||||||
|
include DAV4Rack::HTTPStatus
|
||||||
|
include DAV4Rack::Utils
|
||||||
|
|
||||||
|
attr_reader :request, :response, :resource
|
||||||
|
|
||||||
|
|
||||||
|
# request:: DAV4Rack::Request
|
||||||
|
# response:: Rack::Response
|
||||||
|
# options:: Options hash
|
||||||
|
# Create a new Controller.
|
||||||
|
#
|
||||||
|
# options will be passed on to the resource
|
||||||
|
def initialize(request, response, options={})
|
||||||
|
@options = options
|
||||||
|
|
||||||
|
@request = request
|
||||||
|
@response = response
|
||||||
|
|
||||||
|
@dav_extensions = options[:dav_extensions]
|
||||||
|
|
||||||
|
@always_include_dav_header = !!options[:always_include_dav_header]
|
||||||
|
@allow_unauthenticated_options_on_root = !!options[:allow_unauthenticated_options_on_root]
|
||||||
|
|
||||||
|
setup_resource
|
||||||
|
|
||||||
|
if @always_include_dav_header
|
||||||
|
add_dav_header
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# main entry point, called by the Handler
|
||||||
|
def process
|
||||||
|
if skip_authorization? || authenticate
|
||||||
|
status = process_action || OK
|
||||||
|
else
|
||||||
|
status = HTTPStatus::Unauthorized
|
||||||
|
end
|
||||||
|
rescue HTTPStatus::Status => e
|
||||||
|
status = e
|
||||||
|
ensure
|
||||||
|
if status
|
||||||
|
response.status = status.code
|
||||||
|
if status.code == 401
|
||||||
|
response.body = authentication_error_message
|
||||||
|
response['WWW-Authenticate'] = "Basic realm=\"#{authentication_realm}\""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# delegates to the handler method matching this requests http method.
|
||||||
|
# must return an HTTPStatus. If nil / false, the resulting status will be
|
||||||
|
# 200/OK
|
||||||
|
def process_action
|
||||||
|
send request.request_method.downcase
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# if true, resource#authenticate will never be called
|
||||||
|
def skip_authorization?
|
||||||
|
|
||||||
|
# Microsoft Web Client workaround (from RedmineDmsf plugin):
|
||||||
|
#
|
||||||
|
# MS web client will not attempt any authentication (it'd seem)
|
||||||
|
# until it's acknowledged a completed OPTIONS request.
|
||||||
|
#
|
||||||
|
# If the request method is OPTIONS return true, controller will simply
|
||||||
|
# call the options method within, which accesses nothing, just returns
|
||||||
|
# headers about the dav env.
|
||||||
|
return @allow_unauthenticated_options_on_root &&
|
||||||
|
request.request_method.downcase == 'options' &&
|
||||||
|
(request.path_info == '/' || request.path_info.empty?)
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Return response to OPTIONS
|
||||||
|
def options
|
||||||
|
status = resource.options request, response
|
||||||
|
if(status == OK)
|
||||||
|
add_dav_header
|
||||||
|
response['Allow'] ||= 'OPTIONS,HEAD,GET,PUT,POST,DELETE,PROPFIND,PROPPATCH,MKCOL,COPY,MOVE,LOCK,UNLOCK'
|
||||||
|
response['Ms-Author-Via'] ||= 'DAV'
|
||||||
|
end
|
||||||
|
status
|
||||||
|
end
|
||||||
|
|
||||||
|
# Return response to HEAD
|
||||||
|
def head
|
||||||
|
if(resource.exist?)
|
||||||
|
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
|
||||||
|
resource.head(request, response)
|
||||||
|
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 request.get_header('HTTP_CONTENT_RANGE')
|
||||||
|
# An origin server that allows PUT on a given target resource MUST send
|
||||||
|
# a 400 (Bad Request) response to a PUT request that contains a
|
||||||
|
# Content-Range header field.
|
||||||
|
# Reference: http://tools.ietf.org/html/rfc7231#section-4.3.4
|
||||||
|
Logger.error 'Content-Range on PUT requests is forbidden.'
|
||||||
|
BadRequest
|
||||||
|
elsif(resource.collection?)
|
||||||
|
Forbidden
|
||||||
|
elsif(!resource.parent_exists? || !resource.parent_collection?)
|
||||||
|
Conflict
|
||||||
|
else
|
||||||
|
resource.lock_check if resource.supports_locking?
|
||||||
|
status = resource.put(request, response)
|
||||||
|
if status == Created
|
||||||
|
response['Location'] ||= request.url_for resource.path
|
||||||
|
end
|
||||||
|
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 if resource.supports_locking?
|
||||||
|
resource.delete
|
||||||
|
else
|
||||||
|
NotFound
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Return response to MKCOL
|
||||||
|
def mkcol
|
||||||
|
if request.content_length.to_i > 0
|
||||||
|
return UnsupportedMediaType
|
||||||
|
end
|
||||||
|
return MethodNotAllowed if resource.exist?
|
||||||
|
|
||||||
|
resource.lock_check if resource.supports_locking?
|
||||||
|
status = resource.make_collection
|
||||||
|
|
||||||
|
if(resource.use_compat_mkcol_response?)
|
||||||
|
url = request.url_for resource.path, collection: true
|
||||||
|
r = XmlResponse.new(response, resource.namespaces)
|
||||||
|
r.multistatus do |xml|
|
||||||
|
xml << r.response(url, status)
|
||||||
|
end
|
||||||
|
MultiStatus
|
||||||
|
else
|
||||||
|
status
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Move Resource to new location.
|
||||||
|
def move
|
||||||
|
return NotFound unless(resource.exist?)
|
||||||
|
return BadRequest unless request.depth == :infinity
|
||||||
|
|
||||||
|
return BadRequest unless dest = request.destination
|
||||||
|
if status = dest.validate
|
||||||
|
return status
|
||||||
|
end
|
||||||
|
|
||||||
|
resource.lock_check if resource.supports_locking?
|
||||||
|
|
||||||
|
return resource.move dest.path_info, request.overwrite?
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Return response to COPY
|
||||||
|
def copy
|
||||||
|
return NotFound unless(resource.exist?)
|
||||||
|
return BadRequest unless request.depth == :infinity or request.depth == 0
|
||||||
|
|
||||||
|
return BadRequest unless dest = request.destination
|
||||||
|
if status = dest.validate(host: request.host,
|
||||||
|
resource_path: resource.path)
|
||||||
|
return status
|
||||||
|
end
|
||||||
|
|
||||||
|
resource.copy dest.path_info, request.overwrite?, request.depth
|
||||||
|
end
|
||||||
|
|
||||||
|
# Return response to PROPFIND
|
||||||
|
def propfind
|
||||||
|
return NotFound unless resource.exist?
|
||||||
|
|
||||||
|
ns = request.ns
|
||||||
|
document = request.document if request.content_length.to_i > 0
|
||||||
|
propfind = document.xpath("//#{ns}propfind") if document
|
||||||
|
|
||||||
|
# propname request
|
||||||
|
if propfind and propfind.xpath("//#{ns}propname").first
|
||||||
|
|
||||||
|
r = XmlResponse.new(response, resource.namespaces)
|
||||||
|
r.multistatus do |xml|
|
||||||
|
xml << Ox::Raw.new(resource.propnames_xml_with_depth)
|
||||||
|
end
|
||||||
|
return MultiStatus
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
properties = if propfind.nil? or
|
||||||
|
propfind.empty? or
|
||||||
|
propfind.xpath("//#{ns}allprop").first
|
||||||
|
|
||||||
|
resource.properties
|
||||||
|
|
||||||
|
elsif prop = propfind.xpath("//#{ns}prop").first
|
||||||
|
|
||||||
|
prop.children
|
||||||
|
.find_all{ |item| item.element? }
|
||||||
|
.map{ |item|
|
||||||
|
# We should do this, but Nokogiri transforms prefix w/ null href
|
||||||
|
# into something valid. Oops.
|
||||||
|
# TODO: Hacky grep fix that's horrible
|
||||||
|
hsh = to_element_hash(item)
|
||||||
|
if(hsh.namespace.nil? && !ns.empty?)
|
||||||
|
return BadRequest if prop.to_s.scan(%r{<#{item.name}[^>]+xmlns=""}).empty?
|
||||||
|
end
|
||||||
|
hsh
|
||||||
|
}.compact
|
||||||
|
else
|
||||||
|
Logger.error "no properties found"
|
||||||
|
raise BadRequest
|
||||||
|
end
|
||||||
|
|
||||||
|
properties = properties.map{|property| {element: property}}
|
||||||
|
properties = resource.propfind_add_additional_properties(properties)
|
||||||
|
|
||||||
|
prop_xml = resource.properties_xml_with_depth(get: properties)
|
||||||
|
|
||||||
|
r = XmlResponse.new(response, resource.namespaces)
|
||||||
|
r.multistatus do |xml|
|
||||||
|
xml << r.raw(prop_xml)
|
||||||
|
end
|
||||||
|
MultiStatus
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Return response to PROPPATCH
|
||||||
|
def proppatch
|
||||||
|
return NotFound unless resource.exist?
|
||||||
|
return BadRequest unless doc = request.document
|
||||||
|
|
||||||
|
resource.lock_check if resource.supports_locking?
|
||||||
|
properties = {}
|
||||||
|
doc.xpath("/#{request.ns}propertyupdate").children.each do |element|
|
||||||
|
action = element.name
|
||||||
|
if action == 'set' || action == 'remove'
|
||||||
|
properties[action] ||= []
|
||||||
|
if prp = element.children.detect{|e|e.name == 'prop'}
|
||||||
|
prp.children.each do |elm|
|
||||||
|
next if elm.name == 'text'
|
||||||
|
properties[action] << { element: to_element_hash(elm), value: elm.text }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
r = XmlResponse.new(response, resource.namespaces)
|
||||||
|
r.multistatus do |xml|
|
||||||
|
xml << Ox::Raw.new(resource.properties_xml_with_depth(properties))
|
||||||
|
end
|
||||||
|
MultiStatus
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Lock current resource
|
||||||
|
# NOTE: This will pass an argument hash to Resource#lock and
|
||||||
|
# wait for a success/failure response.
|
||||||
|
def lock
|
||||||
|
depth = request.depth
|
||||||
|
return BadRequest unless depth == 0 || depth == :infinity
|
||||||
|
|
||||||
|
asked = { depth: depth }
|
||||||
|
|
||||||
|
if timeout = request.env['Timeout']
|
||||||
|
asked[:timeout] = timeout.split(',').map{|x|x.strip}
|
||||||
|
end
|
||||||
|
|
||||||
|
ns = request.ns
|
||||||
|
if doc = request.document and lockinfo = doc.xpath("//#{ns}lockinfo")
|
||||||
|
|
||||||
|
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
|
||||||
|
end
|
||||||
|
|
||||||
|
r = XmlResponse.new(response, resource.namespaces)
|
||||||
|
begin
|
||||||
|
|
||||||
|
lock_time, locktoken = resource.lock(asked)
|
||||||
|
|
||||||
|
r.render_lockdiscovery(
|
||||||
|
time: lock_time,
|
||||||
|
token: locktoken,
|
||||||
|
depth: asked[:depth].to_s,
|
||||||
|
scope: asked[:scope],
|
||||||
|
type: asked[:type],
|
||||||
|
owner: asked[:owner]
|
||||||
|
)
|
||||||
|
|
||||||
|
response.headers['Lock-Token'] = "<#{locktoken}>"
|
||||||
|
return resource.exist? ? OK : Created
|
||||||
|
|
||||||
|
rescue LockFailure => e
|
||||||
|
|
||||||
|
r.multistatus do |xml|
|
||||||
|
e.path_status.each_pair do |path, status|
|
||||||
|
xml << r.response(path, status)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return MultiStatus
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Unlock current resource
|
||||||
|
def unlock
|
||||||
|
resource.unlock(request.lock_token)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Perform authentication
|
||||||
|
#
|
||||||
|
# implement your authentication by overriding Resource#authenticate
|
||||||
|
def authenticate
|
||||||
|
uname = nil
|
||||||
|
password = nil
|
||||||
|
if request.authorization?
|
||||||
|
auth = Rack::Auth::Basic::Request.new(request.env)
|
||||||
|
if(auth.basic? && auth.credentials)
|
||||||
|
uname = auth.credentials[0]
|
||||||
|
password = auth.credentials[1]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
resource.authenticate uname, password
|
||||||
|
end
|
||||||
|
|
||||||
|
def authentication_error_message
|
||||||
|
resource.authentication_error_message
|
||||||
|
end
|
||||||
|
|
||||||
|
def authentication_realm
|
||||||
|
resource.authentication_realm
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# override this for custom resource initialization
|
||||||
|
def setup_resource
|
||||||
|
@resource = resource_class.new(
|
||||||
|
request.unescaped_path_info, request, response, @options
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Class of the resource in use
|
||||||
|
def resource_class
|
||||||
|
unless @options[:resource_class]
|
||||||
|
require 'dav4rack/resources/file_resource'
|
||||||
|
@options[:resource_class] = FileResource
|
||||||
|
@options[:root] ||= Dir.pwd
|
||||||
|
end
|
||||||
|
|
||||||
|
@options[:resource_class]
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def add_dav_header
|
||||||
|
unless response['Dav']
|
||||||
|
dav_support = ['1']
|
||||||
|
if resource.supports_locking?
|
||||||
|
# compliance is resource specific, only advertise 2 (locking) if
|
||||||
|
# supported on the resource.
|
||||||
|
dav_support << '2'
|
||||||
|
end
|
||||||
|
dav_support += @dav_extensions if @dav_extensions
|
||||||
|
response['Dav'] = dav_support * ', '
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
39
lib/dav4rack/destination_header.rb
Normal file
39
lib/dav4rack/destination_header.rb
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
require 'addressable/uri'
|
||||||
|
|
||||||
|
module DAV4Rack
|
||||||
|
class DestinationHeader
|
||||||
|
|
||||||
|
attr_reader :host, :path, :path_info
|
||||||
|
|
||||||
|
def initialize(value, script_name: nil)
|
||||||
|
@script_name = script_name.to_s
|
||||||
|
@value = value.to_s.strip
|
||||||
|
parse
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse
|
||||||
|
uri = Addressable::URI.parse @value
|
||||||
|
|
||||||
|
@host = uri.host
|
||||||
|
@path = Addressable::URI.unencode uri.path
|
||||||
|
|
||||||
|
if @script_name
|
||||||
|
if @path =~ /\A(?<path>#{Regexp.escape @script_name}(?<path_info>\/.*))\z/
|
||||||
|
@path_info = $~[:path_info]
|
||||||
|
else
|
||||||
|
raise ArgumentError, 'invalid destination header value'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# host must be the same, but path must differ
|
||||||
|
def validate(host: nil, resource_path: nil)
|
||||||
|
if host and self.host and self.host != host
|
||||||
|
DAV4Rack::HTTPStatus::BadGateway
|
||||||
|
elsif self.path == resource_path
|
||||||
|
DAV4Rack::HTTPStatus::Forbidden
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
39
lib/dav4rack/file.rb
Normal file
39
lib/dav4rack/file.rb
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
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
|
||||||
|
#
|
||||||
|
# FIXME is that used anywhere?
|
||||||
|
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(env)
|
||||||
|
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
|
||||||
162
lib/dav4rack/file_resource_lock.rb
Normal file
162
lib/dav4rack/file_resource_lock.rb
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
require 'pstore'
|
||||||
|
|
||||||
|
module DAV4Rack
|
||||||
|
class FileResourceLock
|
||||||
|
attr_accessor :path
|
||||||
|
attr_accessor :token
|
||||||
|
attr_accessor :timeout
|
||||||
|
attr_accessor :depth
|
||||||
|
attr_accessor :user
|
||||||
|
attr_accessor :scope
|
||||||
|
attr_accessor :kind
|
||||||
|
attr_accessor :owner
|
||||||
|
attr_reader :created_at
|
||||||
|
attr_reader :root
|
||||||
|
|
||||||
|
class << self
|
||||||
|
def explicitly_locked?(path, croot=nil)
|
||||||
|
store = init_pstore(croot)
|
||||||
|
!!store.transaction(true){
|
||||||
|
store[:paths][path]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def implicitly_locked?(path, croot=nil)
|
||||||
|
store = init_pstore(croot)
|
||||||
|
!!store.transaction(true){
|
||||||
|
store[:paths].keys.detect do |check|
|
||||||
|
check.start_with?(path)
|
||||||
|
end
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def explicit_locks(path, croot, args={})
|
||||||
|
end
|
||||||
|
|
||||||
|
def implicit_locks(path, croot)
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_by_path(path, croot=nil)
|
||||||
|
lock = self.class.new(:path => path, :root => croot)
|
||||||
|
lock.token.nil? ? nil : lock
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_by_token(token, croot=nil)
|
||||||
|
store = init_pstore(croot)
|
||||||
|
struct = store.transaction(true){
|
||||||
|
store[:tokens][token]
|
||||||
|
}
|
||||||
|
if(struct)
|
||||||
|
new(path: struct[:path], root: croot)
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate(path, user, token, croot)
|
||||||
|
lock = self.new(:root => croot)
|
||||||
|
lock.user = user
|
||||||
|
lock.path = path
|
||||||
|
lock.token = token
|
||||||
|
lock.save
|
||||||
|
lock
|
||||||
|
end
|
||||||
|
|
||||||
|
def root=(path)
|
||||||
|
@root = path
|
||||||
|
end
|
||||||
|
|
||||||
|
def root
|
||||||
|
@root || '/tmp/dav4rack'
|
||||||
|
end
|
||||||
|
|
||||||
|
def init_pstore(croot)
|
||||||
|
path = File.join(croot, '.attribs', 'locks.pstore')
|
||||||
|
FileUtils.mkdir_p(File.dirname(path)) unless File.directory?(File.dirname(path))
|
||||||
|
store = IS_18 ? PStore.new(path) : PStore.new(path, true)
|
||||||
|
store.transaction do
|
||||||
|
unless(store[:paths])
|
||||||
|
store[:paths] = {}
|
||||||
|
store[:tokens] = {}
|
||||||
|
store.commit
|
||||||
|
end
|
||||||
|
end
|
||||||
|
store
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(args={})
|
||||||
|
@path = args[:path]
|
||||||
|
@root = args[:root]
|
||||||
|
@owner = args[:owner]
|
||||||
|
@store = init_pstore(@root)
|
||||||
|
@max_timeout = args[:max_timeout] || 86400
|
||||||
|
@default_timeout = args[:max_timeout] || 60
|
||||||
|
load_if_exists!
|
||||||
|
@new_record = true if token.nil?
|
||||||
|
end
|
||||||
|
|
||||||
|
def owner?(user)
|
||||||
|
user == owner
|
||||||
|
end
|
||||||
|
|
||||||
|
def reload
|
||||||
|
load_if_exists
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def remaining_timeout
|
||||||
|
t = timeout.to_i - (Time.now.to_i - created_at.to_i)
|
||||||
|
t < 0 ? 0 : t
|
||||||
|
end
|
||||||
|
|
||||||
|
def save
|
||||||
|
struct = {
|
||||||
|
:path => path,
|
||||||
|
:token => token,
|
||||||
|
:timeout => timeout,
|
||||||
|
:depth => depth,
|
||||||
|
:created_at => Time.now,
|
||||||
|
:owner => owner
|
||||||
|
}
|
||||||
|
@store.transaction do
|
||||||
|
@store[:paths][path] = struct
|
||||||
|
@store[:tokens][token] = struct
|
||||||
|
@store.commit
|
||||||
|
end
|
||||||
|
@new_record = false
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@store.transaction do
|
||||||
|
@store[:paths].delete(path)
|
||||||
|
@store[:tokens].delete(token)
|
||||||
|
@store.commit
|
||||||
|
end
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_if_exists!
|
||||||
|
struct = @store.transaction do
|
||||||
|
@store[:paths][path]
|
||||||
|
end
|
||||||
|
if(struct)
|
||||||
|
@path = struct[:path]
|
||||||
|
@token = struct[:token]
|
||||||
|
@timeout = struct[:timeout]
|
||||||
|
@depth = struct[:depth]
|
||||||
|
@created_at = struct[:created_at]
|
||||||
|
@owner = struct[:owner]
|
||||||
|
end
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def init_pstore(croot=nil)
|
||||||
|
self.class.init_pstore(croot || @root)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
80
lib/dav4rack/handler.rb
Normal file
80
lib/dav4rack/handler.rb
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'dav4rack/logger'
|
||||||
|
|
||||||
|
module DAV4Rack
|
||||||
|
class Handler
|
||||||
|
include DAV4Rack::HTTPStatus
|
||||||
|
|
||||||
|
# Options:
|
||||||
|
#
|
||||||
|
# - resource_class: your Resource implementation
|
||||||
|
# - controller_class: custom Controller implementation (optional).
|
||||||
|
# - root_uri_path: Path the handler is mapped to. Any resources
|
||||||
|
# instantiated will only see the part of the path below this root.
|
||||||
|
# - recursive_propfind_allowed (true) : set to false to disable
|
||||||
|
# potentially expensive Depth: Infinity propfinds
|
||||||
|
# - all options are passed on to your resource implementation and are
|
||||||
|
# accessible there as @options.
|
||||||
|
#
|
||||||
|
def initialize(options={})
|
||||||
|
@options = options.dup
|
||||||
|
|
||||||
|
Logger.set(*@options[:log_to])
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(env)
|
||||||
|
start = Time.now
|
||||||
|
request = setup_request env
|
||||||
|
response = Rack::Response.new
|
||||||
|
|
||||||
|
Logger.info "Processing WebDAV request: #{request.path} (for #{request.ip} at #{Time.now}) [#{request.request_method}]"
|
||||||
|
|
||||||
|
controller = setup_controller request, response
|
||||||
|
controller.process
|
||||||
|
postprocess_response response
|
||||||
|
|
||||||
|
# Apache wants the body dealt with, so just read it and junk it
|
||||||
|
buf = true
|
||||||
|
buf = request.body.read(8192) while buf
|
||||||
|
|
||||||
|
if Logger.debug? and response.body.is_a?(String)
|
||||||
|
Logger.debug "Response String:\n#{response.body}"
|
||||||
|
end
|
||||||
|
Logger.info "Completed in: #{((Time.now.to_f - start.to_f) * 1000).to_i} ms | #{response.status} [#{request.url}]"
|
||||||
|
|
||||||
|
response.finish
|
||||||
|
|
||||||
|
rescue Exception => e
|
||||||
|
Logger.error "WebDAV Error: #{e}"
|
||||||
|
raise e
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
|
||||||
|
def postprocess_response(response)
|
||||||
|
if response.body.is_a?(String)
|
||||||
|
response['Content-Length'] ||= response.body.length.to_s
|
||||||
|
end
|
||||||
|
response.body = [response.body] unless response.body.respond_to?(:each)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def setup_request(env)
|
||||||
|
::DAV4Rack::Request.new env, @options
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def setup_controller(request, response)
|
||||||
|
controller_class.new(request, response, @options)
|
||||||
|
end
|
||||||
|
|
||||||
|
def controller_class
|
||||||
|
@options[:controller_class] || ::DAV4Rack::Controller
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
100
lib/dav4rack/http_status.rb
Normal file
100
lib/dav4rack/http_status.rb
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
|
||||||
|
StatusClasses = { }
|
||||||
|
|
||||||
|
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)
|
||||||
|
StatusClasses[code] = klass
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
22
lib/dav4rack/interceptor.rb
Normal file
22
lib/dav4rack/interceptor.rb
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
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
|
||||||
119
lib/dav4rack/interceptor_resource.rb
Normal file
119
lib/dav4rack/interceptor_resource.rb
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
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
|
||||||
40
lib/dav4rack/lock.rb
Normal file
40
lib/dav4rack/lock.rb
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
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
|
||||||
61
lib/dav4rack/lock_store.rb
Normal file
61
lib/dav4rack/lock_store.rb
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
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
|
||||||
30
lib/dav4rack/logger.rb
Normal file
30
lib/dav4rack/logger.rb
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
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
|
||||||
149
lib/dav4rack/remote_file.rb
Normal file
149
lib/dav4rack/remote_file.rb
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
require 'net/http'
|
||||||
|
require 'uri'
|
||||||
|
require 'digest/sha1'
|
||||||
|
require 'rack/file'
|
||||||
|
|
||||||
|
module DAV4Rack
|
||||||
|
|
||||||
|
#FIXME unused?
|
||||||
|
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.to_s
|
||||||
|
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
|
||||||
191
lib/dav4rack/request.rb
Normal file
191
lib/dav4rack/request.rb
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'uri'
|
||||||
|
require 'addressable/uri'
|
||||||
|
require 'dav4rack/logger'
|
||||||
|
|
||||||
|
module DAV4Rack
|
||||||
|
class Request < Rack::Request
|
||||||
|
|
||||||
|
# Root URI path for the resource
|
||||||
|
attr_reader :root_uri_path
|
||||||
|
|
||||||
|
# options:
|
||||||
|
#
|
||||||
|
# recursive_propfind_allowed (true) : set to false to disable
|
||||||
|
# potentially expensive recursive propfinds
|
||||||
|
#
|
||||||
|
def initialize(env, options = {})
|
||||||
|
super env
|
||||||
|
@options = { recursive_propfind_allowed: true }.merge options
|
||||||
|
sanitize_path_info
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def authorization?
|
||||||
|
!!env['HTTP_AUTHORIZATION']
|
||||||
|
end
|
||||||
|
|
||||||
|
# path relative to root uri
|
||||||
|
def unescaped_path_info
|
||||||
|
@unescaped_path_info ||= self.class.unescape_path path_info
|
||||||
|
end
|
||||||
|
|
||||||
|
# the full path (script_name aka rack mount point + path_info)
|
||||||
|
def unescaped_path
|
||||||
|
@unescaped_path ||= self.class.unescape_path path
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.unescape_path(p)
|
||||||
|
p = p.dup
|
||||||
|
p.force_encoding 'UTF-8'
|
||||||
|
Addressable::URI.unencode p
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Namespace being used within XML document
|
||||||
|
def ns(wanted_uri = XmlElements::DAV_NAMESPACE)
|
||||||
|
if document and
|
||||||
|
root = document.root and
|
||||||
|
ns_defs = root.namespace_definitions and
|
||||||
|
ns_defs.size > 0
|
||||||
|
|
||||||
|
result = ns_defs.detect{ |nd| nd.href == wanted_uri } || ns_defs.first
|
||||||
|
result = result.prefix.nil? ? 'xmlns' : result.prefix.to_s
|
||||||
|
result += ':' unless result.empty?
|
||||||
|
result
|
||||||
|
else
|
||||||
|
''
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Lock token if provided by client
|
||||||
|
def lock_token
|
||||||
|
get_header 'HTTP_LOCK_TOKEN'
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Requested depth
|
||||||
|
def depth
|
||||||
|
@http_depth ||= begin
|
||||||
|
if d = get_header('HTTP_DEPTH') and (d == '0' or d == '1')
|
||||||
|
d.to_i
|
||||||
|
elsif infinity_depth_allowed?
|
||||||
|
:infinity
|
||||||
|
else
|
||||||
|
1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Destination header
|
||||||
|
def destination
|
||||||
|
@destination ||= if h = get_header('HTTP_DESTINATION')
|
||||||
|
DestinationHeader.new h, script_name: script_name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Overwrite is allowed
|
||||||
|
def overwrite?
|
||||||
|
get_header('HTTP_OVERWRITE').to_s.upcase != 'F'
|
||||||
|
end
|
||||||
|
|
||||||
|
# content_length as a Fixnum (nil if the header is unset / empty)
|
||||||
|
def content_length
|
||||||
|
if length = (super || get_header('HTTP_X_EXPECTED_ENTITY_LENGTH'))
|
||||||
|
length.to_i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# parsed XML request body if any (Nokogiri XML doc)
|
||||||
|
def document
|
||||||
|
@request_document ||= parse_request_body if content_length && content_length > 0
|
||||||
|
end
|
||||||
|
|
||||||
|
# builds a URL for path using this requests scheme, host, port and
|
||||||
|
# script_name
|
||||||
|
# path must be valid UTF-8 and will be url encoded by this method
|
||||||
|
def url_for(rel_path, collection: false)
|
||||||
|
path = path_for rel_path, collection: collection
|
||||||
|
"#{scheme}://#{host}:#{port}#{path}"
|
||||||
|
end
|
||||||
|
|
||||||
|
# returns an url encoded, absolute path for the given relative path
|
||||||
|
def path_for(rel_path, collection: false)
|
||||||
|
path = Addressable::URI.encode_component rel_path, Addressable::URI::CharacterClasses::PATH
|
||||||
|
if collection and path[-1] != ?/
|
||||||
|
path << '/'
|
||||||
|
end
|
||||||
|
"#{script_name}#{expand_path path}"
|
||||||
|
end
|
||||||
|
|
||||||
|
# expands '/foo/../bar' to '/bar'
|
||||||
|
def expand_path(path)
|
||||||
|
path.squeeze! '/'
|
||||||
|
path = Addressable::URI.normalize_component path, Addressable::URI::CharacterClasses::PATH
|
||||||
|
URI("http://example.com/").merge(path).path
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
REDIRECTABLE_CLIENTS = [
|
||||||
|
/cyberduck/i,
|
||||||
|
/konqueror/i
|
||||||
|
]
|
||||||
|
|
||||||
|
# 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 client_allows_redirect?
|
||||||
|
ua = self.user_agent
|
||||||
|
REDIRECTABLE_CLIENTS.any? { |re| ua =~ re }
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
MS_CLIENTS = [
|
||||||
|
/microsoft-webdav/i,
|
||||||
|
/microsoft office/i
|
||||||
|
]
|
||||||
|
|
||||||
|
# Basic user agent testing for MS authored client
|
||||||
|
def is_ms_client?
|
||||||
|
ua = self.user_agent
|
||||||
|
MS_CLIENTS.any? { |re| ua =~ re }
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_header(name)
|
||||||
|
@env[name]
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# true if Depth: Infinity is allowed for this request.
|
||||||
|
#
|
||||||
|
# http://www.webdav.org/specs/rfc4918.html#METHOD_PROPFIND
|
||||||
|
# Servers ... should support "infinity" requests. In practice,
|
||||||
|
# support for infinite-depth requests may be disabled, due to the
|
||||||
|
# performance and security concerns associated with this behavior
|
||||||
|
def infinity_depth_allowed?
|
||||||
|
request_method != 'PROPFIND' or @options[:recursive_propfind_allowed]
|
||||||
|
end
|
||||||
|
|
||||||
|
def sanitize_path_info
|
||||||
|
self.path_info.force_encoding 'UTF-8'
|
||||||
|
self.path_info = expand_path path_info
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_request_body
|
||||||
|
return Nokogiri.XML(body.read){ |config|
|
||||||
|
config.strict
|
||||||
|
} if body
|
||||||
|
rescue
|
||||||
|
DAV4Rack::Logger.error $!.message
|
||||||
|
raise ::DAV4Rack::HTTPStatus::BadRequest
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
657
lib/dav4rack/resource.rb
Normal file
657
lib/dav4rack/resource.rb
Normal file
@ -0,0 +1,657 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'uuidtools'
|
||||||
|
require 'dav4rack/lock_store'
|
||||||
|
require 'dav4rack/xml_elements'
|
||||||
|
|
||||||
|
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
|
||||||
|
include DAV4Rack::Utils
|
||||||
|
include DAV4Rack::XmlElements
|
||||||
|
|
||||||
|
attr_reader :path, :request,
|
||||||
|
:response, :propstat_relative_path, :root_xml_attributes, :namespaces
|
||||||
|
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
|
||||||
|
|
||||||
|
# path:: Internal resource path (unescaped PATH_INFO)
|
||||||
|
# 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(path, request, response, options)
|
||||||
|
if path.nil? || path.empty? || path[0] != ?/
|
||||||
|
raise ArgumentError, 'path must be present and start with a /'
|
||||||
|
end
|
||||||
|
@path = path
|
||||||
|
|
||||||
|
@propstat_relative_path = !!options[:propstat_relative_path]
|
||||||
|
@root_xml_attributes = options.delete(:root_xml_attributes) || {}
|
||||||
|
@namespaces = (options[:namespaces] || {}).merge({DAV_NAMESPACE => DAV_NAMESPACE_NAME})
|
||||||
|
@request = request
|
||||||
|
@response = response
|
||||||
|
unless(options.has_key?(:lock_class))
|
||||||
|
@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
|
||||||
|
@max_timeout = options[:max_timeout] || 86400
|
||||||
|
@default_timeout = options[:default_timeout] || 60
|
||||||
|
@user = @options[:user] || request.ip
|
||||||
|
|
||||||
|
setup
|
||||||
|
end
|
||||||
|
|
||||||
|
# returns a new instance for the given path
|
||||||
|
def new_for_path(path)
|
||||||
|
self.class.new path, request, response,
|
||||||
|
@options.merge(user: @user, namespaces: @namespaces)
|
||||||
|
end
|
||||||
|
|
||||||
|
# override to implement custom authentication
|
||||||
|
# should return true for successful authentication, false otherwise
|
||||||
|
def authenticate(username, password)
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def authentication_error_message
|
||||||
|
'Not Authorized'
|
||||||
|
end
|
||||||
|
|
||||||
|
def authentication_realm
|
||||||
|
'Locked content'
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# override in child classes for custom setup
|
||||||
|
def setup
|
||||||
|
end
|
||||||
|
private :setup
|
||||||
|
|
||||||
|
|
||||||
|
# Returns if resource supports locking
|
||||||
|
def supports_locking?
|
||||||
|
false #true
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns supported lock types (an array of [lockscope, locktype] pairs)
|
||||||
|
# i.e. [%w(D:exclusive D:write)]
|
||||||
|
def supported_locks
|
||||||
|
[]
|
||||||
|
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
|
||||||
|
|
||||||
|
# Is the parent resource a collection?
|
||||||
|
def parent_collection?
|
||||||
|
parent.collection?
|
||||||
|
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 OPTIONS request.
|
||||||
|
# resources should override this to set the Allow header to indicate the
|
||||||
|
# allowed methods. By default, all WebDAV methods are advertised on all
|
||||||
|
# resources.
|
||||||
|
def options(request, response)
|
||||||
|
OK
|
||||||
|
end
|
||||||
|
|
||||||
|
# HTTP GET request.
|
||||||
|
#
|
||||||
|
# Write the content of the resource to the response.body.
|
||||||
|
def get(request, response)
|
||||||
|
NotImplemented
|
||||||
|
end
|
||||||
|
|
||||||
|
def head(request, response)
|
||||||
|
#no-op, but called by the controller
|
||||||
|
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 path.
|
||||||
|
def copy(dest_path, overwrite = false, depth = nil)
|
||||||
|
NotImplemented
|
||||||
|
end
|
||||||
|
|
||||||
|
# HTTP MOVE request.
|
||||||
|
#
|
||||||
|
# Move this resource to given destination path.
|
||||||
|
def move(dest_path, 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)
|
||||||
|
raise NotImplemented unless @lock_class
|
||||||
|
raise Conflict unless parent_exists?
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
return NotImplemented unless @lock_class
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
# 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
|
||||||
|
#
|
||||||
|
# These are returned by PROPFIND without body, or with an allprop body.
|
||||||
|
DAV_PROPERTIES = %w(
|
||||||
|
getetag
|
||||||
|
resourcetype
|
||||||
|
getcontenttype
|
||||||
|
getcontentlength
|
||||||
|
getlastmodified
|
||||||
|
creationdate
|
||||||
|
displayname
|
||||||
|
).map{|prop| { name: prop, ns_href: DAV_NAMESPACE } }.freeze
|
||||||
|
|
||||||
|
def properties
|
||||||
|
props = DAV_PROPERTIES
|
||||||
|
if supports_locking?
|
||||||
|
props = props.dup # do not attempt to modify the (frozen) constant
|
||||||
|
props << { name: 'supportedlock', ns_href: DAV_NAMESPACE }
|
||||||
|
end
|
||||||
|
props
|
||||||
|
end
|
||||||
|
|
||||||
|
# Properties to be returned for <propname/> PROPFIND
|
||||||
|
#
|
||||||
|
# this should include the names of all properties defined on the resource
|
||||||
|
def propname_properties
|
||||||
|
props = self.properties
|
||||||
|
if supports_locking?
|
||||||
|
props = props.dup if props.frozen?
|
||||||
|
props << { name: 'lockdiscovery', ns_href: DAV_NAMESPACE }
|
||||||
|
end
|
||||||
|
props
|
||||||
|
end
|
||||||
|
|
||||||
|
# name:: String - Property name
|
||||||
|
# Returns the value of the given property
|
||||||
|
def get_property(element)
|
||||||
|
return NotFound if (element[:ns_href] != DAV_NAMESPACE)
|
||||||
|
case element[: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
|
||||||
|
when 'supportedlock' then supported_locks_xml
|
||||||
|
when 'lockdiscovery' then lockdiscovery_xml
|
||||||
|
else NotImplemented
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# name:: String - Property name
|
||||||
|
# value:: New value
|
||||||
|
# Set the property to the given value
|
||||||
|
#
|
||||||
|
# This default implementation does not allow any properties to be changed.
|
||||||
|
def set_property(element, value)
|
||||||
|
return Forbidden
|
||||||
|
end
|
||||||
|
|
||||||
|
# name:: Property name
|
||||||
|
# Remove the property from the resource
|
||||||
|
def remove_property(element)
|
||||||
|
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_path = @path.dup
|
||||||
|
new_path << ?/ unless new_path[-1] == ?/
|
||||||
|
new_path << name
|
||||||
|
new_for_path new_path
|
||||||
|
end
|
||||||
|
|
||||||
|
# Return parent of this resource
|
||||||
|
def parent
|
||||||
|
return nil if @path == '/'
|
||||||
|
unless @path.to_s.empty?
|
||||||
|
new_for_path File.split(@path).first
|
||||||
|
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
|
||||||
|
|
||||||
|
def properties_xml_with_depth(process_properties, depth = request.depth)
|
||||||
|
xml_with_depth self, depth do |element, ox_doc|
|
||||||
|
ox_doc << element.properties_xml(process_properties)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def propnames_xml_with_depth(depth = request.depth)
|
||||||
|
xml_with_depth self, depth do |element, ox_doc|
|
||||||
|
ox_doc << element.propnames_xml
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns a complete URL for this resource.
|
||||||
|
# If the propstat_relative_path option is set, just an absolute path will
|
||||||
|
# be returned.
|
||||||
|
# If this is a collection, the result will end with a '/'
|
||||||
|
def href
|
||||||
|
@href ||= build_href(path, collection: self.collection?)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns a complete URL for the given path.
|
||||||
|
#
|
||||||
|
# If the propstat_relative_path option is set, just an absolute path will
|
||||||
|
# be returned. If the :collection argument is true, the returned path will
|
||||||
|
# end with a '/'
|
||||||
|
def build_href(path, collection: false)
|
||||||
|
if propstat_relative_path
|
||||||
|
request.path_for path, collection: collection
|
||||||
|
else
|
||||||
|
request.url_for path, collection: collection
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def propnames_xml
|
||||||
|
response = Ox::Element.new(D_RESPONSE)
|
||||||
|
response << ox_element(D_HREF, href)
|
||||||
|
propstats response, { OK => Hash[propname_properties.map{|p| [p,nil]}] }
|
||||||
|
response
|
||||||
|
end
|
||||||
|
|
||||||
|
def properties_xml(process_properties)
|
||||||
|
response = Ox::Element.new(D_RESPONSE)
|
||||||
|
response << ox_element(D_HREF, href)
|
||||||
|
|
||||||
|
process_properties.each do |type, properties|
|
||||||
|
propstats(response, self.send("#{type}_properties_with_status",properties))
|
||||||
|
end
|
||||||
|
response
|
||||||
|
end
|
||||||
|
|
||||||
|
def supported_locks_xml
|
||||||
|
supported_locks.map do |scope, type|
|
||||||
|
ox_lockentry scope, type
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# array of lock info hashes
|
||||||
|
# required keys are :time, :token, :depth
|
||||||
|
# other valid keys are :scope, :type, :root and :owner
|
||||||
|
def lockdiscovery
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
|
||||||
|
# returns an array of activelock ox elements
|
||||||
|
def lockdiscovery_xml
|
||||||
|
if supports_locking?
|
||||||
|
lockdiscovery.map do |lock|
|
||||||
|
ox_activelock(**lock)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_properties_with_status(properties)
|
||||||
|
stats = Hash.new { |h, k| h[k] = [] }
|
||||||
|
properties.each do |property|
|
||||||
|
val = self.get_property(property[:element])
|
||||||
|
if val.is_a?(Class)
|
||||||
|
stats[val] << property[:element]
|
||||||
|
else
|
||||||
|
stats[OK] << [property[:element], val]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
stats
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_properties_with_status(properties)
|
||||||
|
stats = Hash.new { |h, k| h[k] = [] }
|
||||||
|
properties.each do |property|
|
||||||
|
val = self.set_property(property[:element], property[:value])
|
||||||
|
if val.is_a?(Class)
|
||||||
|
stats[val] << property[:element]
|
||||||
|
else
|
||||||
|
stats[OK] << [property[:element], val]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
stats
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# resource:: Resource
|
||||||
|
# elements:: Property hashes (name, namespace, children)
|
||||||
|
# Removes the given properties from a resource
|
||||||
|
def remove_properties_with_status(properties)
|
||||||
|
stats = Hash.new { |h, k| h[k] = [] }
|
||||||
|
properties.each do |property|
|
||||||
|
val = self.remove_property(property[:element])
|
||||||
|
if val.is_a?(Class)
|
||||||
|
stats[val] << property[:element]
|
||||||
|
else
|
||||||
|
stats[OK] << [property[:element], val]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
stats
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# adds the given xml namespace to namespaces and returns the prefix
|
||||||
|
def add_namespace(ns, prefix = "unknown#{rand 65536}")
|
||||||
|
return nil if ns.nil? || ns.empty?
|
||||||
|
unless namespaces.key? ns
|
||||||
|
namespaces[ns] = prefix
|
||||||
|
return prefix
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# returns the prefix for the given namespace, adding it if necessary
|
||||||
|
def prefix_for(ns_href)
|
||||||
|
namespaces[ns_href] || add_namespace(ns_href)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# response:: parent Ox::Element
|
||||||
|
# stats:: Array of stats
|
||||||
|
# Build propstats response
|
||||||
|
def propstats(response, stats)
|
||||||
|
return if stats.empty?
|
||||||
|
stats.each do |status, props|
|
||||||
|
propstat = Ox::Element.new(D_PROPSTAT)
|
||||||
|
prop = Ox::Element.new(D_PROP)
|
||||||
|
|
||||||
|
props.each do |element, value|
|
||||||
|
|
||||||
|
name = element[:name]
|
||||||
|
if prefix = prefix_for(element[:ns_href])
|
||||||
|
name = "#{prefix}:#{name}"
|
||||||
|
end
|
||||||
|
|
||||||
|
prop_element = Ox::Element.new(name)
|
||||||
|
ox_append prop_element, value, prefix: prefix
|
||||||
|
prop << prop_element
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
propstat << prop
|
||||||
|
propstat << ox_element(D_STATUS, "#{http_version} #{status.status_line}")
|
||||||
|
|
||||||
|
response << propstat
|
||||||
|
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])
|
||||||
|
request.is_ms_client?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Callback function that adds additional properties to the propfind REQUEST
|
||||||
|
# These properties will then be parsed and processed as though they were sent
|
||||||
|
# by the client. This makes sure we can add whatever property we want
|
||||||
|
# to the response and make it look like the client asked for them.
|
||||||
|
def propfind_add_additional_properties(properties)
|
||||||
|
# Default implementation doesn't need to add anything
|
||||||
|
properties
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
379
lib/dav4rack/resources/file_resource.rb
Normal file
379
lib/dav4rack/resources/file_resource.rb
Normal file
@ -0,0 +1,379 @@
|
|||||||
|
# encoding: UTF-8
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'pstore'
|
||||||
|
require 'webrick/httputils'
|
||||||
|
require 'dav4rack/file_resource_lock'
|
||||||
|
require 'dav4rack/security_utils'
|
||||||
|
|
||||||
|
module DAV4Rack
|
||||||
|
|
||||||
|
class FileResource < Resource
|
||||||
|
|
||||||
|
include WEBrick::HTTPUtils
|
||||||
|
include DAV4Rack::Utils
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
return NotFound unless exist?
|
||||||
|
if stat.directory?
|
||||||
|
response.body = "".dup
|
||||||
|
Rack::Directory.new(root).call(request.env)[2].each do |line|
|
||||||
|
response.body << line
|
||||||
|
end
|
||||||
|
response['Content-Length'] = response.body.bytesize.to_s
|
||||||
|
OK
|
||||||
|
else
|
||||||
|
status, headers, body = Rack::File.new(root).call(request.env)
|
||||||
|
headers.each do |k, v|
|
||||||
|
response[k] = v
|
||||||
|
end
|
||||||
|
response.body = body
|
||||||
|
StatusClasses[status]
|
||||||
|
end
|
||||||
|
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
|
||||||
|
::File.unlink(prop_path) if ::File.exist?(prop_path)
|
||||||
|
@_prop_hash = nil
|
||||||
|
NoContent
|
||||||
|
end
|
||||||
|
|
||||||
|
# HTTP COPY request.
|
||||||
|
#
|
||||||
|
# Copy this resource to given destination path.
|
||||||
|
def copy(dest_path, overwrite, depth = nil)
|
||||||
|
|
||||||
|
is_new = true
|
||||||
|
|
||||||
|
dest = new_for_path dest_path
|
||||||
|
unless dest.parent.exist? and dest.parent.collection?
|
||||||
|
return Conflict
|
||||||
|
end
|
||||||
|
|
||||||
|
if dest.exist?
|
||||||
|
if overwrite
|
||||||
|
FileUtils.rm_r dest.file_path, secure: true
|
||||||
|
else
|
||||||
|
return PreconditionFailed
|
||||||
|
end
|
||||||
|
is_new = false
|
||||||
|
end
|
||||||
|
|
||||||
|
if collection?
|
||||||
|
|
||||||
|
if request.depth == 0
|
||||||
|
Dir.mkdir dest.file_path
|
||||||
|
else
|
||||||
|
FileUtils.cp_r(file_path, dest.file_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
else
|
||||||
|
|
||||||
|
FileUtils.cp(file_path, dest.file_path.sub(/\/$/, ''))
|
||||||
|
FileUtils.cp(prop_path, dest.prop_path) if ::File.exist? prop_path
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
is_new ? Created : NoContent
|
||||||
|
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)) && !::File.exist?(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)
|
||||||
|
if name[:ns_href] == DAV_NAMESPACE
|
||||||
|
super
|
||||||
|
else
|
||||||
|
custom_props(name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# name:: String - Property name
|
||||||
|
# value:: New value
|
||||||
|
# Set the property to the given value
|
||||||
|
def set_property(name, value)
|
||||||
|
# let Resource handle DAV properties
|
||||||
|
if name[:ns_href] == DAV_NAMESPACE
|
||||||
|
super
|
||||||
|
else
|
||||||
|
set_custom_props name, value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_property(element)
|
||||||
|
prop_hash.transaction do
|
||||||
|
prop_hash.delete(to_element_key(element))
|
||||||
|
prop_hash.commit
|
||||||
|
end
|
||||||
|
val = prop_hash.transaction{ prop_hash[to_element_key(element)] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def lock(args)
|
||||||
|
unless(parent_exists?)
|
||||||
|
Conflict
|
||||||
|
else
|
||||||
|
lock_check(args[:type])
|
||||||
|
lock = FileResourceLock.explicit_locks(@path, root, :scope => args[:scope], :kind => args[:type], :user => @user)
|
||||||
|
unless(lock)
|
||||||
|
token = UUIDTools::UUID.random_create.to_s
|
||||||
|
lock = FileResourceLock.generate(@path, @user, token, root)
|
||||||
|
lock.scope = args[:scope]
|
||||||
|
lock.kind = args[:type]
|
||||||
|
lock.owner = args[:owner]
|
||||||
|
lock.depth = args[:depth]
|
||||||
|
if(args[:timeout])
|
||||||
|
lock.timeout = args[:timeout] <= @max_timeout && args[:timeout] > 0 ? args[:timeout] : @max_timeout
|
||||||
|
else
|
||||||
|
lock.timeout = @default_timeout
|
||||||
|
end
|
||||||
|
lock.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
|
||||||
|
|
||||||
|
def unlock(token)
|
||||||
|
token = token.slice(1, token.length - 2)
|
||||||
|
if(token.nil? || token.empty?)
|
||||||
|
BadRequest
|
||||||
|
else
|
||||||
|
lock = FileResourceLock.find_by_token(token, root)
|
||||||
|
if(lock.nil? || lock.user != @user)
|
||||||
|
Forbidden
|
||||||
|
elsif(lock.path !~ /^#{Regexp.escape(@path)}.*$/)
|
||||||
|
Conflict
|
||||||
|
else
|
||||||
|
lock.destroy
|
||||||
|
NoContent
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def authenticate(user, pass)
|
||||||
|
if(@options[:username])
|
||||||
|
# This comparison uses & so that it doesn't short circuit and
|
||||||
|
# uses `variable_size_secure_compare` so that length information
|
||||||
|
# isn't leaked.
|
||||||
|
SecurityUtils.variable_size_secure_compare(
|
||||||
|
user, @options[:username]
|
||||||
|
) &
|
||||||
|
SecurityUtils.variable_size_secure_compare(
|
||||||
|
pass, @options[:password]
|
||||||
|
)
|
||||||
|
else
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def file_path
|
||||||
|
::File.expand_path ::File.join(root, path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def prop_path
|
||||||
|
path = ::File.join(store_directory, "#{::File.join(::File.dirname(file_path), ::File.basename(file_path)).gsub('/', '_')}.pstore")
|
||||||
|
unless(::File.directory?(::File.dirname(path)))
|
||||||
|
FileUtils.mkdir_p(::File.dirname(path))
|
||||||
|
end
|
||||||
|
path
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def lock_check(lock_type=nil)
|
||||||
|
if(FileResourceLock.explicitly_locked?(@path, root))
|
||||||
|
raise Locked if lock_type && lock_type == 'exclusive'
|
||||||
|
#raise Locked if FileResourceLock.explicit_locks(@path, root).find(:all, :conditions => ["scope = 'exclusive' AND user_id != ?", @user.id]).size > 0
|
||||||
|
elsif(FileResourceLock.implicitly_locked?(@path, root))
|
||||||
|
if(lock_type.to_s == 'exclusive')
|
||||||
|
locks = FileResourceLock.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 = FileResourceLock.implict_locks(@path).find(:all, :conditions => ["scope = 'exclusive' AND user_id != ?", @user.id])
|
||||||
|
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
|
||||||
|
|
||||||
|
def set_custom_props(element, val)
|
||||||
|
prop_hash.transaction do
|
||||||
|
prop_hash[to_element_key(element)] = val
|
||||||
|
prop_hash.commit
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def custom_props(element)
|
||||||
|
val = prop_hash.transaction(true) do
|
||||||
|
prop_hash[to_element_key(element)]
|
||||||
|
end
|
||||||
|
val || NotFound
|
||||||
|
end
|
||||||
|
|
||||||
|
def store_directory
|
||||||
|
path = ::File.join(root, '.attrib_store')
|
||||||
|
unless(::File.directory?(::File.dirname(path)))
|
||||||
|
FileUtils.mkdir_p(::File.dirname(path))
|
||||||
|
end
|
||||||
|
path
|
||||||
|
end
|
||||||
|
|
||||||
|
def lock_path
|
||||||
|
path = ::File.join(store_directory, 'locks.pstore')
|
||||||
|
unless(::File.directory?(::File.dirname(path)))
|
||||||
|
FileUtils.mkdir_p(::File.dirname(path))
|
||||||
|
end
|
||||||
|
path
|
||||||
|
end
|
||||||
|
|
||||||
|
def prop_hash
|
||||||
|
@_prop_hash ||= IS_18 ? PStore.new(prop_path) : PStore.new(prop_path, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def root
|
||||||
|
@options[:root]
|
||||||
|
end
|
||||||
|
|
||||||
|
def stat
|
||||||
|
@stat ||= ::File.stat(file_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
268
lib/dav4rack/resources/mongo_resource.rb
Normal file
268
lib/dav4rack/resources/mongo_resource.rb
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'mime/types'
|
||||||
|
require 'mongo'
|
||||||
|
require 'erb'
|
||||||
|
require 'dav4rack/security_utils'
|
||||||
|
|
||||||
|
module DAV4Rack
|
||||||
|
|
||||||
|
class MongoResource < DAV4Rack::Resource
|
||||||
|
|
||||||
|
def self.database=(db)
|
||||||
|
@database = db
|
||||||
|
end
|
||||||
|
def self.database; @database end
|
||||||
|
|
||||||
|
|
||||||
|
def setup
|
||||||
|
@filesystem = Mongo::Grid::FSBucket.new(self.class.database)
|
||||||
|
if @options[:bson]
|
||||||
|
@bson = @options[:bson]
|
||||||
|
elsif path.empty? || path == '/'
|
||||||
|
@bson = {'filename' => '/'}
|
||||||
|
else
|
||||||
|
@bson = @filesystem.find(filename: /^#{Regexp.escape(path)}\/?$/).first rescue nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def child(bson)
|
||||||
|
our_options = @options.dup
|
||||||
|
@options[:bson] = bson
|
||||||
|
child = new_for_path bson['filename']
|
||||||
|
@options = our_options
|
||||||
|
child
|
||||||
|
end
|
||||||
|
|
||||||
|
# If this is a collection, return the child resources.
|
||||||
|
def children
|
||||||
|
@filesystem.find(filename: /^#{Regexp.escape(@bson['filename'])}[^\/]+\/?$/).map do |bson|
|
||||||
|
child bson
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Is this resource a collection?
|
||||||
|
def collection?
|
||||||
|
@bson && _collection?(@bson['filename'])
|
||||||
|
end
|
||||||
|
|
||||||
|
# Does this recource exist?
|
||||||
|
def exist?
|
||||||
|
!!@bson
|
||||||
|
end
|
||||||
|
|
||||||
|
# Return the creation time.
|
||||||
|
def creation_date
|
||||||
|
@bson['uploadDate'] || Date.new
|
||||||
|
end
|
||||||
|
|
||||||
|
# Return the time of last modification.
|
||||||
|
def last_modified
|
||||||
|
@bson['uploadDate'] || Date.new
|
||||||
|
end
|
||||||
|
|
||||||
|
# Set the time of last modification.
|
||||||
|
def last_modified=(time)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Return an Etag, an unique hash value for this resource.
|
||||||
|
def etag
|
||||||
|
@bson['_id'].to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
# Return the mime type of this resource.
|
||||||
|
def content_type
|
||||||
|
@bson['contentType'] || "text/html"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Return the size in bytes for this resource.
|
||||||
|
def content_length
|
||||||
|
@bson['length'] || 0
|
||||||
|
end
|
||||||
|
|
||||||
|
# HTTP GET request.
|
||||||
|
#
|
||||||
|
# Write the content of the resource to the response.body.
|
||||||
|
def get(request, response)
|
||||||
|
return NotFound unless exist?
|
||||||
|
|
||||||
|
if collection?
|
||||||
|
response.body = "<html>".dup
|
||||||
|
response.body << "<h2>" + ERB::Util.html_escape(path) + "</h2>"
|
||||||
|
children.each do |child|
|
||||||
|
name = ERB::Util.html_escape child.name
|
||||||
|
|
||||||
|
path = ERB::Util.html_escape request.path_for(child.path,
|
||||||
|
collection: child.collection?)
|
||||||
|
response.body << "<a href='" + path + "'>" + name + "</a>"
|
||||||
|
response.body << "</br>"
|
||||||
|
end
|
||||||
|
response.body << "</html>"
|
||||||
|
response['Content-Type'] = 'text/html'
|
||||||
|
else
|
||||||
|
@filesystem.open_download_stream_by_name(path, revision: -1) do |s|
|
||||||
|
# not sure if the copying is necessary, but keeping the reference to
|
||||||
|
# s outside the block seems unsafe as we dont know if/when that
|
||||||
|
# stream will be closed
|
||||||
|
#response.body = s
|
||||||
|
response.body = s.read
|
||||||
|
response['Content-Type'] = @bson['contentType']
|
||||||
|
end
|
||||||
|
end
|
||||||
|
OK
|
||||||
|
rescue Mongo::Error::InvalidFileRevision
|
||||||
|
NotFound
|
||||||
|
end
|
||||||
|
|
||||||
|
# HTTP PUT request.
|
||||||
|
#
|
||||||
|
# Save the content of the request.body.
|
||||||
|
def put(request, response)
|
||||||
|
exists = exist?
|
||||||
|
|
||||||
|
@filesystem.open_upload_stream(
|
||||||
|
path, content_type: content_type_for(path)
|
||||||
|
) { |f| f.write request.body }
|
||||||
|
|
||||||
|
exists ? NoContent : Created
|
||||||
|
end
|
||||||
|
|
||||||
|
# HTTP POST request.
|
||||||
|
#
|
||||||
|
# Usually forbidden.
|
||||||
|
def post(request, response)
|
||||||
|
raise HTTPStatus::Forbidden
|
||||||
|
end
|
||||||
|
|
||||||
|
# HTTP DELETE request.
|
||||||
|
#
|
||||||
|
# Delete this resource.
|
||||||
|
def delete
|
||||||
|
if collection?
|
||||||
|
@filesystem.find(filename: /^#{Regexp.escape(@bson['filename'])}/).each do |bson|
|
||||||
|
@filesystem.delete(bson['_id'])
|
||||||
|
end
|
||||||
|
else
|
||||||
|
@filesystem.delete(@bson['_id'])
|
||||||
|
end
|
||||||
|
NoContent
|
||||||
|
end
|
||||||
|
|
||||||
|
# HTTP COPY request.
|
||||||
|
#
|
||||||
|
# Copy this resource to given destination path.
|
||||||
|
def copy(dest_path, overwrite = false, depth = :infinity)
|
||||||
|
dest = new_for_path dest_path
|
||||||
|
dest.collection! if collection?
|
||||||
|
|
||||||
|
src = @bson['filename']
|
||||||
|
dst = dest.path
|
||||||
|
exists = @filesystem.find(filename: /^#{Regexp.escape(dst)}/).any?
|
||||||
|
|
||||||
|
if overwrite
|
||||||
|
@filesystem.find(filename: /^#{Regexp.escape(dst)}/).each do |bson|
|
||||||
|
@filesystem.delete bson['_id']
|
||||||
|
end
|
||||||
|
elsif @filesystem.find(filename: /^#{Regexp.escape(dst)}/).any?
|
||||||
|
return PreconditionFailed
|
||||||
|
end
|
||||||
|
|
||||||
|
@filesystem.find(filename: /^#{Regexp.escape(src)}/).each do |bson|
|
||||||
|
src_name = bson['filename']
|
||||||
|
dst_name = dst + src_name.slice(src.length, src_name.length)
|
||||||
|
|
||||||
|
@filesystem.open_download_stream_by_name(src_name) do |i|
|
||||||
|
@filesystem.open_upload_stream(dst_name,
|
||||||
|
content_type: bson['contentType']) do |o|
|
||||||
|
i.each{|chunk| o.write chunk }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
exists ? NoContent : Created
|
||||||
|
end
|
||||||
|
|
||||||
|
# HTTP MOVE request.
|
||||||
|
#
|
||||||
|
# Move this resource to given destination path.
|
||||||
|
def move(dest_path, overwrite = false)
|
||||||
|
dest = new_for_path dest_path
|
||||||
|
dest.collection! if collection?
|
||||||
|
|
||||||
|
src = @bson['filename']
|
||||||
|
dst = dest.path
|
||||||
|
exists = @filesystem.find(filename: /^#{Regexp.escape(dst)}/).any?
|
||||||
|
|
||||||
|
if overwrite
|
||||||
|
@filesystem.find(filename: /^#{Regexp.escape(dst)}/).each do |bson|
|
||||||
|
@filesystem.delete bson['_id']
|
||||||
|
end
|
||||||
|
elsif @filesystem.find(filename: /^#{Regexp.escape(dst)}/).any?
|
||||||
|
return PreconditionFailed
|
||||||
|
end
|
||||||
|
|
||||||
|
@filesystem.find(filename: /^#{Regexp.escape(src)}/).each do |bson|
|
||||||
|
src_name = bson['filename']
|
||||||
|
dst_name = dst + src_name.slice(src.length, src_name.length)
|
||||||
|
|
||||||
|
# http://mongoid.org/docs/persistence/atomic.html
|
||||||
|
# http://rubydoc.info/github/mongoid/mongoid/master/Mongoid/Collection#update-instance_method
|
||||||
|
mongo_collection.find_one_and_update({'_id' => bson['_id']}, {'$set' => {'filename' => dst_name}}, safe: true)
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
exists ? NoContent : Created
|
||||||
|
end
|
||||||
|
|
||||||
|
# HTTP MKCOL request.
|
||||||
|
#
|
||||||
|
# Create this resource as collection.
|
||||||
|
def make_collection
|
||||||
|
|
||||||
|
if @filesystem.find(filename: path).any?
|
||||||
|
raise 'resource exists'
|
||||||
|
end
|
||||||
|
collection!
|
||||||
|
@filesystem.open_upload_stream(path) { |f| }
|
||||||
|
|
||||||
|
Created
|
||||||
|
end
|
||||||
|
|
||||||
|
def collection!
|
||||||
|
path << '/' unless _collection?(path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def authenticate(user, pass)
|
||||||
|
if(@options[:username])
|
||||||
|
# This comparison uses & so that it doesn't short circuit and
|
||||||
|
# uses `variable_size_secure_compare` so that length information
|
||||||
|
# isn't leaked.
|
||||||
|
SecurityUtils.variable_size_secure_compare(
|
||||||
|
user, @options[:username]
|
||||||
|
) &
|
||||||
|
SecurityUtils.variable_size_secure_compare(
|
||||||
|
pass, @options[:password]
|
||||||
|
)
|
||||||
|
else
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def mongo_collection
|
||||||
|
@mongo_collection ||= self.class.database['fs.files']
|
||||||
|
end
|
||||||
|
|
||||||
|
def content_type_for(filename)
|
||||||
|
MIME::Types.type_for(filename).first.to_s || 'text/html'
|
||||||
|
end
|
||||||
|
|
||||||
|
def _collection?(path)
|
||||||
|
path && path.end_with?('/')
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
24
lib/dav4rack/security_utils.rb
Normal file
24
lib/dav4rack/security_utils.rb
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
require 'digest'
|
||||||
|
|
||||||
|
module DAV4Rack
|
||||||
|
|
||||||
|
# Implements secure string comparison methods.
|
||||||
|
# Taken straight from ActiveSupport
|
||||||
|
module SecurityUtils
|
||||||
|
def secure_compare(a, b)
|
||||||
|
return false unless a.bytesize == b.bytesize
|
||||||
|
|
||||||
|
l = a.unpack "C#{a.bytesize}"
|
||||||
|
|
||||||
|
res = 0
|
||||||
|
b.each_byte { |byte| res |= byte ^ l.shift }
|
||||||
|
res == 0
|
||||||
|
end
|
||||||
|
module_function :secure_compare
|
||||||
|
|
||||||
|
def variable_size_secure_compare(a, b)
|
||||||
|
secure_compare(::Digest::SHA256.hexdigest(a), ::Digest::SHA256.hexdigest(b))
|
||||||
|
end
|
||||||
|
module_function :variable_size_secure_compare
|
||||||
|
end
|
||||||
|
end
|
||||||
48
lib/dav4rack/utils.rb
Normal file
48
lib/dav4rack/utils.rb
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'ostruct'
|
||||||
|
|
||||||
|
module DAV4Rack
|
||||||
|
|
||||||
|
# Simple wrapper for formatted elements
|
||||||
|
class DAVElement < OpenStruct
|
||||||
|
def [](key)
|
||||||
|
self.send(key)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
module Utils
|
||||||
|
DEFAULT_HTTP_VERSION = 'HTTP/1.1'
|
||||||
|
|
||||||
|
def to_element_hash(element)
|
||||||
|
ns = element.namespace
|
||||||
|
DAVElement.new(
|
||||||
|
:namespace => ns,
|
||||||
|
:name => element.name,
|
||||||
|
:ns_href => (ns.href if ns),
|
||||||
|
:children => element.children.collect{|e|
|
||||||
|
to_element_hash(e) if e.element?
|
||||||
|
}.compact,
|
||||||
|
:attributes => attributes_hash(element)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_element_key(element)
|
||||||
|
ns = element.namespace
|
||||||
|
"#{ns.href if ns}!!#{element.name}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def http_version
|
||||||
|
DEFAULT_HTTP_VERSION
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def attributes_hash(node)
|
||||||
|
node.attributes.inject({}) do |ret, (key,attr)|
|
||||||
|
ret[attr.name] = attr.value
|
||||||
|
ret
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
17
lib/dav4rack/version.rb
Normal file
17
lib/dav4rack/version.rb
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
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('1.0.0')
|
||||||
|
end
|
||||||
129
lib/dav4rack/xml_elements.rb
Normal file
129
lib/dav4rack/xml_elements.rb
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DAV4Rack
|
||||||
|
module XmlElements
|
||||||
|
|
||||||
|
DAV_NAMESPACE = 'DAV:'
|
||||||
|
DAV_NAMESPACE_NAME = 'd'
|
||||||
|
DAV_XML_NS = 'xmlns:d'
|
||||||
|
|
||||||
|
XML_VERSION = '1.0'
|
||||||
|
XML_CONTENT_TYPE = 'application/xml; charset=utf-8'
|
||||||
|
|
||||||
|
%w(
|
||||||
|
activelock
|
||||||
|
depth
|
||||||
|
error
|
||||||
|
href
|
||||||
|
lockdiscovery
|
||||||
|
lockentry
|
||||||
|
lockroot
|
||||||
|
lockscope
|
||||||
|
locktoken
|
||||||
|
lock-token-submitted
|
||||||
|
locktype
|
||||||
|
multistatus
|
||||||
|
owner
|
||||||
|
prop
|
||||||
|
propstat
|
||||||
|
response
|
||||||
|
status
|
||||||
|
timeout
|
||||||
|
).each do |name|
|
||||||
|
const_set "D_#{name.upcase.gsub('-', '_')}", "#{DAV_NAMESPACE_NAME}:#{name}"
|
||||||
|
end
|
||||||
|
|
||||||
|
INFINITY = 'infinity'
|
||||||
|
ZERO = '0'
|
||||||
|
|
||||||
|
def ox_element(name, content = nil)
|
||||||
|
e = Ox::Element.new(name)
|
||||||
|
if content
|
||||||
|
e << content
|
||||||
|
end
|
||||||
|
e
|
||||||
|
end
|
||||||
|
|
||||||
|
def ox_append(element, value, prefix: DAV_NAMESPACE_NAME)
|
||||||
|
case value
|
||||||
|
when Ox::Element
|
||||||
|
element << value
|
||||||
|
when Symbol
|
||||||
|
element << Ox::Element.new("#{prefix}:#{value}")
|
||||||
|
when Enumerable
|
||||||
|
value.each{|v| ox_append element, v, prefix: prefix }
|
||||||
|
else
|
||||||
|
element << value.to_s if value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def ox_lockentry(scope, type)
|
||||||
|
Ox::Element.new(D_LOCKENTRY).tap do |e|
|
||||||
|
e << ox_element(D_LOCKSCOPE, Ox::Element.new(scope))
|
||||||
|
e << ox_element(D_LOCKTYPE, Ox::Element.new(type))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def ox_response(path, status)
|
||||||
|
Ox::Element.new(D_RESPONSE).tap do |e|
|
||||||
|
# path = "#{scheme}://#{host}:#{port}#{URI.escape(path)}"
|
||||||
|
e << ox_element(D_HREF, path)
|
||||||
|
e << ox_element(D_STATUS, "#{http_version} #{status.status_line}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# returns an activelock Ox::Element for the given lock data
|
||||||
|
def ox_activelock(time: nil, token:, depth:,
|
||||||
|
scope: nil, type: nil, owner: nil, root: nil)
|
||||||
|
|
||||||
|
Ox::Element.new(D_ACTIVELOCK).tap do |activelock|
|
||||||
|
if scope
|
||||||
|
activelock << ox_element(D_LOCKSCOPE, scope)
|
||||||
|
end
|
||||||
|
if type
|
||||||
|
activelock << ox_element(D_LOCKTYPE, type)
|
||||||
|
end
|
||||||
|
activelock << ox_element(D_DEPTH, depth)
|
||||||
|
activelock << ox_element(D_TIMEOUT,
|
||||||
|
(time ? "Second-#{time}" : INFINITY))
|
||||||
|
|
||||||
|
token = ox_element(D_HREF, token)
|
||||||
|
activelock << ox_element(D_LOCKTOKEN, token)
|
||||||
|
|
||||||
|
if owner
|
||||||
|
activelock << ox_element(D_OWNER, owner)
|
||||||
|
end
|
||||||
|
|
||||||
|
if root
|
||||||
|
root = ox_element(D_HREF, root)
|
||||||
|
activelock << ox_element(D_LOCKROOT, root)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
# block is called for each element (at least self, depending on depth also
|
||||||
|
# with children / further descendants)
|
||||||
|
def xml_with_depth(resource, depth, &block)
|
||||||
|
partial_document = Ox::Document.new()
|
||||||
|
|
||||||
|
yield resource, partial_document
|
||||||
|
|
||||||
|
case depth
|
||||||
|
when 0
|
||||||
|
when 1
|
||||||
|
resource.children.each do |child|
|
||||||
|
yield child, partial_document
|
||||||
|
end
|
||||||
|
else
|
||||||
|
resource.descendants.each do |desc|
|
||||||
|
yield desc, partial_document
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Ox.dump(partial_document, {indent: -1})
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
120
lib/dav4rack/xml_response.rb
Normal file
120
lib/dav4rack/xml_response.rb
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DAV4Rack
|
||||||
|
class XmlResponse
|
||||||
|
include XmlElements
|
||||||
|
|
||||||
|
def initialize(response, namespaces, http_version: 'HTTP/1.1')
|
||||||
|
@response = response
|
||||||
|
@namespaces = namespaces
|
||||||
|
@http_version = http_version
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_xml(xml_body)
|
||||||
|
@namespaces.each do |href, prefix|
|
||||||
|
xml_body["xmlns:#{prefix}"] = href
|
||||||
|
end
|
||||||
|
|
||||||
|
xml_doc = Ox::Document.new(:version => '1.0')
|
||||||
|
xml_doc << xml_body
|
||||||
|
|
||||||
|
@response.body = Ox.dump(xml_doc, {indent: -1, with_xml: true})
|
||||||
|
|
||||||
|
@response["Content-Type"] = 'application/xml; charset=utf-8'
|
||||||
|
@response["Content-Length"] = @response.body.bytesize.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def multistatus
|
||||||
|
multistatus = Ox::Element.new(D_MULTISTATUS)
|
||||||
|
yield multistatus
|
||||||
|
render_xml multistatus
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def render_failed_precondition(status, href)
|
||||||
|
error = Ox::Element.new(D_ERROR)
|
||||||
|
case status.code
|
||||||
|
when 423
|
||||||
|
l = Ox::Element.new(D_LOCK_TOKEN_SUBMITTED)
|
||||||
|
l << ox_element(D_HREF, href)
|
||||||
|
error << l
|
||||||
|
end
|
||||||
|
render_xml error
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def render_lock_errors(errors)
|
||||||
|
multistatus do |xml|
|
||||||
|
errors.each do |href, status|
|
||||||
|
r = response href, status
|
||||||
|
if status.code == 423
|
||||||
|
r << ox_element(D_ERROR, Ox::Element.new(D_LOCK_TOKEN_SUBMITTED))
|
||||||
|
end
|
||||||
|
xml << r
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def render_lockdiscovery(*args)
|
||||||
|
render_xml ox_element(D_PROP,
|
||||||
|
ox_element(D_LOCKDISCOVERY,
|
||||||
|
activelock(*args))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# helpers for creating single elements
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
# returns an activelock Ox::Element for the given lock data
|
||||||
|
def activelock(time: nil, token:, depth:,
|
||||||
|
scope: nil, type: nil, owner: nil, root: nil)
|
||||||
|
|
||||||
|
Ox::Element.new(D_ACTIVELOCK).tap do |activelock|
|
||||||
|
if scope
|
||||||
|
activelock << ox_element(D_LOCKSCOPE, scope)
|
||||||
|
end
|
||||||
|
if type
|
||||||
|
activelock << ox_element(D_LOCKTYPE, type)
|
||||||
|
end
|
||||||
|
activelock << ox_element(D_DEPTH, depth)
|
||||||
|
activelock << ox_element(D_TIMEOUT,
|
||||||
|
(time ? "Second-#{time}" : INFINITY))
|
||||||
|
|
||||||
|
token = ox_element(D_HREF, token)
|
||||||
|
activelock << ox_element(D_LOCKTOKEN, token)
|
||||||
|
|
||||||
|
if owner
|
||||||
|
activelock << ox_element(D_OWNER, owner)
|
||||||
|
end
|
||||||
|
|
||||||
|
if root
|
||||||
|
root = ox_element(D_HREF, root)
|
||||||
|
activelock << ox_element(D_LOCKROOT, root)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
def response(href, status)
|
||||||
|
r = Ox::Element.new(D_RESPONSE)
|
||||||
|
r << ox_element(D_HREF, href)
|
||||||
|
r << self.status(status)
|
||||||
|
r
|
||||||
|
end
|
||||||
|
|
||||||
|
def raw(xml)
|
||||||
|
Ox::Raw.new xml
|
||||||
|
end
|
||||||
|
|
||||||
|
def status(status)
|
||||||
|
ox_element D_STATUS, "#{@http_version} #{status.status_line}"
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -41,6 +41,7 @@ if defined?(EasyExtensions)
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Load up classes that make up our WebDAV solution ontop of DAV4Rack
|
# Load up classes that make up our WebDAV solution ontop of DAV4Rack
|
||||||
|
require 'dav4rack'
|
||||||
require 'redmine_dmsf/webdav/custom_middleware'
|
require 'redmine_dmsf/webdav/custom_middleware'
|
||||||
require 'redmine_dmsf/webdav/base_resource'
|
require 'redmine_dmsf/webdav/base_resource'
|
||||||
require 'redmine_dmsf/webdav/dmsf_resource'
|
require 'redmine_dmsf/webdav/dmsf_resource'
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user