001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://www.alkacon.com) 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software, please see the 018 * company website: https://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: https://www.opencms.org 022 * 023 * You should have received a copy of the GNU Lesser General Public 024 * License along with this library; if not, write to the Free Software 025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 026 */ 027 028package org.opencms.file.quota; 029 030import org.opencms.file.CmsObject; 031import org.opencms.file.CmsResource; 032import org.opencms.main.CmsException; 033import org.opencms.main.CmsLog; 034import org.opencms.main.OpenCms; 035import org.opencms.util.CmsFileUtil; 036 037import java.util.Collection; 038import java.util.HashMap; 039import java.util.HashSet; 040import java.util.List; 041import java.util.Map; 042import java.util.Set; 043import java.util.TreeMap; 044 045import org.apache.commons.logging.Log; 046 047import com.google.common.collect.ArrayListMultimap; 048import com.google.common.collect.Multimap; 049 050/** 051 * Object for storing folder size information for all folders in the VFS, and for efficiently updating and retrieving it. 052 * 053 * <p>Mutable, not threadsafe by itself. 054 */ 055public class CmsFolderSizeTable { 056 057 /** Logger instance for this class. */ 058 private static final Log LOG = CmsLog.getLog(CmsFolderSizeTable.class); 059 060 /** A CMS context. */ 061 private CmsObject m_cms; 062 063 private TreeMap<String, Long> m_folders = new TreeMap<>(); 064 065 private Map<String, Long> m_subtreeCache; 066 067 private boolean m_online; 068 069 /** 070 * Creates a deep copy of an existing instance. 071 * 072 * @param other 073 */ 074 public CmsFolderSizeTable(CmsFolderSizeTable other) { 075 076 try { 077 this.m_cms = OpenCms.initCmsObject(other.m_cms); 078 } catch (Exception e) { 079 // shouldn't happen 080 LOG.error(e.getLocalizedMessage(), e); 081 this.m_cms = other.m_cms; 082 } 083 this.m_online = other.m_online; 084 this.m_folders = new TreeMap<>(other.m_folders); 085 } 086 087 /** 088 * Create a new instance. 089 * 090 * @param cms the CMS context 091 */ 092 public CmsFolderSizeTable(CmsObject cms, boolean online) { 093 094 m_cms = cms; 095 m_online = online; 096 } 097 098 /** 099 * Prepares a folder report consisting of subtree sizes for a bunch of folders. 100 * 101 * <p>This is more efficient than querying for folder sizes individually. 102 * 103 * @param folders the folders 104 * @return the folder report 105 */ 106 public Map<String, CmsFolderReportEntry> getFolderReport(Collection<String> folders) { 107 108 Map<String, Long> subtreeCache = getSubtreeCache(); 109 Set<String> folderSet = new HashSet<>(); 110 for (String folder : folders) { 111 folderSet.add(normalize(folder)); 112 } 113 // to compute the exclusive folder sizes, we need to take the total subtree size for the given folder and then subtract 114 // the subtree sizes of its immediate descendants. 115 // For example, if our paths are /a/, /a/b/, and /a/b/d/, to get the exclusive size for /a/, we must *not* subtract the size of /a/b/d/. 116 // So we first need to determine the direct descendants for each folder. 117 Multimap<String, String> directDescendants = ArrayListMultimap.create(); 118 for (String folder : folderSet) { 119 String currentAncestor = CmsResource.getParentFolder(folder); 120 String directAncestor = null; 121 while (currentAncestor != null) { 122 if (folderSet.contains(currentAncestor)) { 123 directAncestor = currentAncestor; 124 break; 125 } 126 currentAncestor = CmsResource.getParentFolder(currentAncestor); 127 } 128 if (directAncestor != null) { 129 directDescendants.put(directAncestor, folder); 130 } 131 } 132 Map<String, CmsFolderReportEntry> result = new HashMap<>(); 133 for (String folder : folderSet) { 134 long size = subtreeCache.getOrDefault(folder, Long.valueOf(0)); 135 long exclusiveSize = size; 136 for (String child : directDescendants.get(folder)) { 137 exclusiveSize -= subtreeCache.getOrDefault(child, Long.valueOf(0)); 138 } 139 result.put(folder, new CmsFolderReportEntry(size, exclusiveSize)); 140 } 141 return result; 142 } 143 144 /** 145 * Gets the total folder size for the complete subtree at the given root path. 146 * 147 * @param rootPath the root path for which to compute the size 148 * @return the total size 149 */ 150 public long getTotalFolderSize(String rootPath) { 151 152 Long size = getSubtreeCache().get(normalize(rootPath)); 153 if (size != null) { 154 return size.longValue(); 155 } else { 156 return 0; 157 } 158 } 159 160 /** 161 * Gets the folder size for the subtree at the given root path, but without including any folder sizes 162 * of subtrees at any paths from 'otherPaths' of which rootPath is a proper prefix. 163 * 164 * @param rootPath the root path for which to calculate the size 165 * @param otherPaths the other paths to exclude from the size 166 * 167 * @return the total size 168 */ 169 public long getTotalFolderSizeExclusive(String rootPath, Collection<String> otherPaths) { 170 171 Set<String> paths = new HashSet<>(otherPaths); 172 rootPath = normalize(rootPath); 173 paths.add(rootPath); 174 return getFolderReport(paths).get(rootPath).getTreeSizeExclusive(); 175 176 } 177 178 /** 179 * Loads all folder size data. 180 * 181 * @throws CmsException if something goes wrong 182 */ 183 public void loadAll() throws CmsException { 184 185 TreeMap<String, Long> folders = new TreeMap<>(); 186 List<CmsFolderSizeEntry> stats = m_cms.readFolderSizeStats(new CmsFolderSizeOptions("/", m_online, true)); 187 for (CmsFolderSizeEntry entry : stats) { 188 folders.put(normalize(entry.getRootPath()), Long.valueOf(entry.getSize())); 189 } 190 m_folders = folders; 191 m_subtreeCache = null; 192 } 193 194 /** 195 * Updates the folder size for a single folder, not including subfolders. 196 * 197 * @param rootPath the root path of the folder for which to update the information 198 * @throws CmsException if something goes wrong 199 */ 200 public void updateSingle(String rootPath) throws CmsException { 201 202 List<CmsFolderSizeEntry> entries = m_cms.readFolderSizeStats( 203 new CmsFolderSizeOptions(rootPath, m_online, false)); 204 m_folders.remove(normalize(rootPath)); 205 if (entries.size() > 0) { 206 m_folders.put(normalize(entries.get(0).getRootPath()), Long.valueOf(entries.get(0).getSize())); 207 } 208 m_subtreeCache = null; 209 } 210 211 /** 212 * Updates the subtree cache. 213 */ 214 public void updateSubtreeCache() { 215 216 Map<String, Long> subtreeCache = new HashMap<>(); 217 for (Map.Entry<String, Long> entry : m_folders.entrySet()) { 218 String currentPath = entry.getKey(); 219 while (currentPath != null) { 220 subtreeCache.put( 221 currentPath, 222 Long.valueOf(subtreeCache.computeIfAbsent(currentPath, key -> 0l) + entry.getValue())); 223 currentPath = CmsResource.getParentFolder(currentPath); 224 } 225 } 226 m_subtreeCache = subtreeCache; 227 } 228 229 /** 230 * Updates the information for a complete subtree. 231 * 232 * @param rootPath the root path for which to update the information 233 * @throws CmsException if something goes wrong 234 */ 235 public void updateTree(String rootPath) throws CmsException { 236 237 List<CmsFolderSizeEntry> entries = m_cms.readFolderSizeStats( 238 new CmsFolderSizeOptions(rootPath, m_online, true)); 239 rootPath = normalize(rootPath); 240 m_folders.subMap(rootPath, rootPath + Character.MAX_VALUE).clear(); 241 for (CmsFolderSizeEntry entry : entries) { 242 m_folders.put(normalize(entry.getRootPath()), Long.valueOf(entry.getSize())); 243 } 244 m_subtreeCache = null; 245 } 246 247 /** 248 * Gets the subtree cache. 249 * 250 * @return the subtree cache 251 */ 252 private Map<String, Long> getSubtreeCache() { 253 254 if (m_subtreeCache == null) { 255 updateSubtreeCache(); 256 } 257 return m_subtreeCache; 258 } 259 260 /** 261 * Normalizes a path to include a trailing separator. 262 * 263 * @param path the path to normalize 264 * @return the normalized path 265 */ 266 private String normalize(String path) { 267 268 if ("".equals(path)) { 269 return "/"; 270 } 271 if ("/".equals(path)) { 272 return path; 273 } 274 return CmsFileUtil.addTrailingSeparator(path); 275 } 276 277}