Implemented caching of PROPSTATS and PROPFIND using MemCacheStore (memcached).
This commit is contained in:
parent
640018c597
commit
75a443dac9
1
Gemfile
1
Gemfile
@ -27,6 +27,7 @@ gem 'zip-zip'
|
||||
gem 'simple_enum'
|
||||
gem 'uuidtools'
|
||||
gem 'dav4rack'
|
||||
gem 'dalli'
|
||||
|
||||
group :xapian do
|
||||
gem 'xapian-full-alaveteli', :require => false
|
||||
|
||||
@ -478,4 +478,33 @@ class DmsfFile < ActiveRecord::Base
|
||||
nil
|
||||
end
|
||||
|
||||
def save(*args)
|
||||
RedmineDmsf::Webdav::Cache.invalidate_item(propfind_cache_key)
|
||||
super(*args)
|
||||
end
|
||||
|
||||
def save!(*args)
|
||||
RedmineDmsf::Webdav::Cache.invalidate_item(propfind_cache_key)
|
||||
super(*args)
|
||||
end
|
||||
|
||||
def destroy
|
||||
RedmineDmsf::Webdav::Cache.invalidate_item(propfind_cache_key)
|
||||
super
|
||||
end
|
||||
|
||||
def destroy!
|
||||
RedmineDmsf::Webdav::Cache.invalidate_item(propfind_cache_key)
|
||||
super
|
||||
end
|
||||
|
||||
def propfind_cache_key
|
||||
if dmsf_folder_id.nil?
|
||||
# File is in project root
|
||||
return "PROPFIND/#{project_id}"
|
||||
else
|
||||
return "PROPFIND/#{project_id}/#{dmsf_folder_id}"
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@ -109,6 +109,7 @@ class DmsfFileRevision < ActiveRecord::Base
|
||||
dependencies = DmsfFileRevision.where(:disk_filename => self.disk_filename).all.count
|
||||
File.delete(self.disk_file) if dependencies <= 1 && File.exist?(self.disk_file)
|
||||
end
|
||||
RedmineDmsf::Webdav::Cache.invalidate_item(propfind_cache_key)
|
||||
super
|
||||
end
|
||||
|
||||
@ -291,4 +292,18 @@ class DmsfFileRevision < ActiveRecord::Base
|
||||
ActionView::Base.full_sanitizer.sanitize(text)
|
||||
end
|
||||
|
||||
def save(*args)
|
||||
RedmineDmsf::Webdav::Cache.invalidate_item(propfind_cache_key)
|
||||
super(*args)
|
||||
end
|
||||
|
||||
def save!(*args)
|
||||
RedmineDmsf::Webdav::Cache.invalidate_item(propfind_cache_key)
|
||||
super(*args)
|
||||
end
|
||||
|
||||
def propfind_cache_key
|
||||
dmsf_file.propfind_cache_key
|
||||
end
|
||||
|
||||
end
|
||||
@ -395,6 +395,35 @@ class DmsfFolder < ActiveRecord::Base
|
||||
nil
|
||||
end
|
||||
|
||||
def save(*args)
|
||||
RedmineDmsf::Webdav::Cache.invalidate_item(propfind_cache_key)
|
||||
super(*args)
|
||||
end
|
||||
|
||||
def save!(*args)
|
||||
RedmineDmsf::Webdav::Cache.invalidate_item(propfind_cache_key)
|
||||
super(*args)
|
||||
end
|
||||
|
||||
def destroy
|
||||
RedmineDmsf::Webdav::Cache.invalidate_item(propfind_cache_key)
|
||||
super
|
||||
end
|
||||
|
||||
def destroy!
|
||||
RedmineDmsf::Webdav::Cache.invalidate_item(propfind_cache_key)
|
||||
super
|
||||
end
|
||||
|
||||
def propfind_cache_key
|
||||
if dmsf_folder_id.nil?
|
||||
# Folder is in project root
|
||||
return "PROPFIND/#{project_id}"
|
||||
else
|
||||
return "PROPFIND/#{project_id}/#{dmsf_folder_id}"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def self.directory_subtree(tree, folder, level, current_folder)
|
||||
|
||||
@ -176,6 +176,13 @@
|
||||
<%= l(:note_webdav_strategy).html_safe %> <%= l(:label_default) %>: <%= l(:select_option_webdav_readonly) %>
|
||||
</em>
|
||||
</p>
|
||||
<p>
|
||||
<%= content_tag(:label, l(:label_memcached_servers)) %>
|
||||
<%= text_field_tag 'settings[dmsf_memcached_servers]', @settings['dmsf_memcached_servers'], :size => 50 %>
|
||||
<em class="info">
|
||||
<%= l(:text_memcached_servers) %>
|
||||
</em>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
<hr/>
|
||||
|
||||
@ -339,3 +339,7 @@ en:
|
||||
label_webdav: WebDAV
|
||||
label_full_text: Full-text search
|
||||
link_extension: Ext
|
||||
|
||||
label_memcached_servers: "memcached server address"
|
||||
text_memcached_servers: "Address to memcached server. Only a single server is supported, if empty then caching is disabled. After changing this the server must be restarted!"
|
||||
|
||||
|
||||
3
init.rb
3
init.rb
@ -47,7 +47,8 @@ Redmine::Plugin.register :redmine_dmsf do
|
||||
'dmsf_webdav' => '1',
|
||||
'dmsf_display_notified_recipients' => 0,
|
||||
'dmsf_global_title_format' => '',
|
||||
'dmsf_columns' => %w(title size modified version workflow author)
|
||||
'dmsf_columns' => %w(title size modified version workflow author),
|
||||
'dmsf_memcached_servers' => ''
|
||||
}
|
||||
|
||||
menu :project_menu, :dmsf, { :controller => 'dmsf', :action => 'show' }, :caption => :menu_dmsf, :before => :documents, :param => :id
|
||||
|
||||
@ -35,6 +35,7 @@ require 'redmine_dmsf/patches/user_patch'
|
||||
# Load up classes that make up our WebDAV solution ontop of DAV4Rack
|
||||
require 'redmine_dmsf/webdav/base_resource'
|
||||
require 'redmine_dmsf/webdav/controller'
|
||||
require 'redmine_dmsf/webdav/cache'
|
||||
require 'redmine_dmsf/webdav/dmsf_resource'
|
||||
require 'redmine_dmsf/webdav/download'
|
||||
require 'redmine_dmsf/webdav/index_resource'
|
||||
@ -57,3 +58,7 @@ require 'redmine_dmsf/hooks/views/my_account_view_hooks'
|
||||
|
||||
# Macros
|
||||
require 'redmine_dmsf/macros'
|
||||
|
||||
# Add the plugin view folder into ActionMailer's paths to search
|
||||
ActionMailer::Base.append_view_path(File.expand_path(
|
||||
File.dirname(__FILE__) + '/../app/views'))
|
||||
|
||||
83
lib/redmine_dmsf/webdav/cache.rb
Normal file
83
lib/redmine_dmsf/webdav/cache.rb
Normal file
@ -0,0 +1,83 @@
|
||||
# encoding: utf-8
|
||||
#
|
||||
# Redmine plugin for Document Management System "Features"
|
||||
#
|
||||
# Copyright (C) 2012 Daniel Munn <dan.munn@munnster.co.uk>
|
||||
# Copyright (C) 2011-16 Karel Picman <karel.picman@kontron.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
module RedmineDmsf
|
||||
module Webdav
|
||||
class Cache
|
||||
def self.read(name, options = nil)
|
||||
init unless defined?(@@WebDAVCache)
|
||||
@@WebDAVCache.read(name, options)
|
||||
end
|
||||
|
||||
def self.write(name, value, options = nil)
|
||||
init unless defined?(@@WebDAVCache)
|
||||
@@WebDAVCache.write(name, value, options)
|
||||
end
|
||||
|
||||
def self.delete(name, options = nil)
|
||||
init unless defined?(@@WebDAVCache)
|
||||
@@WebDAVCache.delete(name, options)
|
||||
end
|
||||
|
||||
def self.exist?(name, options = nil)
|
||||
init unless defined?(@@WebDAVCache)
|
||||
@@WebDAVCache.exist?(name, options)
|
||||
end
|
||||
|
||||
def self.invalidate_item(key)
|
||||
init unless defined?(@@WebDAVCache)
|
||||
# Write an .invalid entry to notify anyone that is currently creating a response
|
||||
# that that response is invalid and should not be cached
|
||||
@@WebDAVCache.write("#{key}.invalid", expires_in: 60.seconds)
|
||||
# Delete any existing entry in the cache
|
||||
@@WebDAVCache.delete(key)
|
||||
end
|
||||
|
||||
def self.cache
|
||||
@@WebDAVCache
|
||||
end
|
||||
|
||||
def self.init_testcache
|
||||
puts "Webdav::Cache: Enable MemoryStore cache."
|
||||
@@WebDAVCache = ActiveSupport::Cache::MemoryStore.new(options={:namespace => "RedmineDmsfWebDAV"})
|
||||
end
|
||||
|
||||
def self.init_nullcache
|
||||
puts "Webdav::Cache: Disable cache."
|
||||
@@WebDAVCache = ActiveSupport::Cache::NullStore.new
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def self.init
|
||||
if Setting.plugin_redmine_dmsf['dmsf_memcached_servers'].nil? || Setting.plugin_redmine_dmsf['dmsf_memcached_servers'].empty?
|
||||
# Disable caching by using a null cache
|
||||
Rails.logger.info "Webdav::Cache: Cache disabled!"
|
||||
@@WebDAVCache = ActiveSupport::Cache::NullStore.new
|
||||
else
|
||||
# Create cache using the provided server address
|
||||
Rails.logger.info "Webdav::Cache: Cache enabled, using memcached server '#{Setting.plugin_redmine_dmsf['dmsf_memcached_servers']}'"
|
||||
@@WebDAVCache = ActiveSupport::Cache::MemCacheStore.new(Setting.plugin_redmine_dmsf['dmsf_memcached_servers'], options={:namespace => "RedmineDmsfWebDAV"})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -107,18 +107,44 @@ module RedmineDmsf
|
||||
raise BadRequest
|
||||
end
|
||||
end
|
||||
multistatus do |xml|
|
||||
find_resources.each do |resource|
|
||||
xml.response do
|
||||
unless(resource.propstat_relative_path)
|
||||
xml.href "#{scheme}://#{host}:#{port}#{url_format(resource)}"
|
||||
|
||||
if depth != 0
|
||||
# Only use cache for requests with a depth>0, depth=0 responses are already fast.
|
||||
pinfo = resource.path.split('/').drop(1)
|
||||
if (pinfo.length == 0) # If this is the base_path, we're at root
|
||||
# Don't know when projects are added/removed from the visibility list for this user,
|
||||
# so don't cache root.
|
||||
elsif (pinfo.length == 1) #This is first level, and as such, project path
|
||||
propfind_key = "PROPFIND/#{resource.resource.project_id}"
|
||||
else # We made it all the way to DMSF Data
|
||||
if resource.collection?
|
||||
# Only store collections in the cache since responses to files are simple and fast already.
|
||||
propfind_key = "PROPFIND/#{resource.resource.project_id}/#{resource.resource.folder.id}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if propfind_key.nil?
|
||||
# This PROPFIND is never cached so always create a new response
|
||||
create_propfind_response(properties)
|
||||
else
|
||||
xml.href url_format(resource)
|
||||
end
|
||||
propstats(xml, get_properties(resource, properties.empty? ? resource.properties : properties))
|
||||
end
|
||||
response.body = RedmineDmsf::Webdav::Cache.read(propfind_key)
|
||||
if !response.body.nil?
|
||||
# Found cached PROPFIND, fill in Content-Type and Content-Length
|
||||
response["Content-Type"] = 'text/xml; charset="utf-8"'
|
||||
response["Content-Length"] = response.body.size.to_s
|
||||
else
|
||||
# No cached PROPFIND found
|
||||
# Remove .invalid entry for this propfind since we are now creating a new valid propfind
|
||||
RedmineDmsf::Webdav::Cache.delete("#{propfind_key}.invalid")
|
||||
create_propfind_response(properties)
|
||||
|
||||
# Cache response.body, but only if no .invalid entry was stored while creating the propfind
|
||||
RedmineDmsf::Webdav::Cache.write(propfind_key, response.body) unless RedmineDmsf::Webdav::Cache.exist?("#{propfind_key}.invalid")
|
||||
end
|
||||
end
|
||||
# Return HTTP code.
|
||||
MultiStatus
|
||||
end
|
||||
end
|
||||
|
||||
@ -179,6 +205,57 @@ module RedmineDmsf
|
||||
Addressable::URI.unescape str
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_propfind_response(properties)
|
||||
# Generate response, is stored in response.body
|
||||
render_xml(:multistatus) do |xml|
|
||||
find_resources.each do |resource|
|
||||
if resource.collection?
|
||||
# Index, Project or Folder
|
||||
# path is unique enough for the key and is available for all three, and the path doesn't change
|
||||
# for this path as long as it stays. On its path. The path does not stray from its path without
|
||||
# changing its path.
|
||||
propstats_key = "PROPSTATS/#{resource.path}"
|
||||
else
|
||||
# File
|
||||
# Use file.id & file.last_revision.id as key
|
||||
# When revision changes then the key will change and the old cached item will eventually be evicted
|
||||
propstats_key = "PROPSTATS/#{resource.resource.file.id}-#{resource.resource.file.last_revision.id}"
|
||||
end
|
||||
|
||||
xml_str = RedmineDmsf::Webdav::Cache.read(propstats_key)
|
||||
if xml_str.nil?
|
||||
# Create the complete PROPSTATS response
|
||||
propstats_builder = Nokogiri::XML::Builder.new do |propstats_xml|
|
||||
propstats_xml.send('propstat', {'xmlns:D' => 'DAV:'}.merge(resource.root_xml_attributes)) do
|
||||
propstats_xml.parent.namespace = propstats_xml.parent.namespace_definitions.first
|
||||
xml2 = propstats_xml['D']
|
||||
|
||||
xml2.response do
|
||||
unless(resource.propstat_relative_path)
|
||||
xml2.href "#{scheme}://#{host}:#{port}#{url_format(resource)}"
|
||||
else
|
||||
xml2.href url_format(resource)
|
||||
end
|
||||
propstats(xml2, get_properties(resource, properties.empty? ? resource.properties : properties))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Just want to add the <:D:response> so extract it.
|
||||
# Q: Is there a better/faster way to do this?
|
||||
xml_str = Nokogiri::XML.parse(propstats_builder.to_xml).xpath('//D:response').first.to_xml
|
||||
|
||||
# Add PROPSTATS to cache
|
||||
# Caching the PROPSTATS response as xml text string.
|
||||
RedmineDmsf::Webdav::Cache.write(propstats_key, xml_str)
|
||||
end
|
||||
xml << xml_str
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -80,6 +80,10 @@ module RedmineDmsf
|
||||
return project && project.module_enabled?('dmsf') && (folder || file)
|
||||
end
|
||||
|
||||
def project_id
|
||||
project.id unless project.nil?
|
||||
end
|
||||
|
||||
# Is this entity a folder?
|
||||
def collection?
|
||||
folder.present? # No need to check if entity exists, as false is returned if entity does not exist anyways
|
||||
|
||||
@ -79,6 +79,10 @@ module RedmineDmsf
|
||||
project.name unless project.nil?
|
||||
end
|
||||
|
||||
def project_id
|
||||
project.id unless project.nil?
|
||||
end
|
||||
|
||||
def content_type
|
||||
'inode/directory'
|
||||
end
|
||||
|
||||
@ -85,6 +85,10 @@ module RedmineDmsf
|
||||
@resource_c.really_exist?
|
||||
end
|
||||
|
||||
def project_id
|
||||
@resource_c.project_id
|
||||
end
|
||||
|
||||
def creation_date
|
||||
@resource_c.creation_date
|
||||
end
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user