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 GmbH & Co. KG, 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.loader.imagecache; 029 030import org.opencms.loader.CmsRfsImageCache; 031import org.opencms.loader.imagecache.CmsImageCacheCapabilities.Capability; 032 033import java.nio.file.DirectoryNotEmptyException; 034import java.nio.file.Files; 035import java.nio.file.LinkOption; 036import java.nio.file.Path; 037import java.nio.file.Paths; 038import java.nio.file.attribute.BasicFileAttributes; 039import java.nio.file.attribute.FileTime; 040import java.time.Duration; 041import java.util.Iterator; 042import java.util.stream.Stream; 043 044/** 045 * Maintenance adapter for the classic RFS image cache.<p> 046 */ 047public class CmsRfsImageCacheMaintenance implements I_CmsImageCacheMaintenance { 048 049 /** The supported capabilities. */ 050 private static final CmsImageCacheCapabilities CAPABILITIES = CmsImageCacheCapabilities.of( 051 Capability.LIST_ENTRIES, 052 Capability.ENTRY_TIMESTAMPS, 053 Capability.DELETE_ENTRIES, 054 Capability.CLEAR, 055 Capability.RENEW_ENTRIES); 056 057 /** The cache. */ 058 private final CmsRfsImageCache m_cache; 059 060 /** The normalized repository path. */ 061 private final Path m_repository; 062 063 /** 064 * Creates a maintenance adapter.<p> 065 * 066 * @param cache the RFS cache 067 */ 068 public CmsRfsImageCacheMaintenance(CmsRfsImageCache cache) { 069 070 m_cache = cache; 071 m_repository = Paths.get(cache.getRepositoryPath()).toAbsolutePath().normalize(); 072 } 073 074 /** 075 * @see org.opencms.loader.imagecache.I_CmsImageCacheMaintenance#execute(org.opencms.loader.imagecache.CmsImageCacheMaintenanceRequest) 076 */ 077 @Override 078 public CmsImageCacheMaintenanceResult execute(CmsImageCacheMaintenanceRequest request) throws Exception { 079 080 long start = System.nanoTime(); 081 int requested = request.getOperation() == CmsImageCacheMaintenanceRequest.Operation.CLEAR 082 ? 1 083 : request.getEntries().size(); 084 CmsImageCacheMaintenanceResult.Builder result = new CmsImageCacheMaintenanceResult.Builder( 085 request.getOperation(), 086 requested); 087 switch (request.getOperation()) { 088 case CLEAR: 089 try { 090 m_cache.clear(); 091 result.addSuccess(); 092 } catch (Exception e) { 093 result.addFailure(getBackendId(), e); 094 } 095 break; 096 case DELETE: 097 for (CmsImageCacheEntry entry : request.getEntries()) { 098 delete(entry, result); 099 } 100 removeEmptyDirectories(); 101 break; 102 case RENEW: 103 FileTime renewalTime = FileTime.from(request.getRenewalTime()); 104 for (CmsImageCacheEntry entry : request.getEntries()) { 105 renew(entry, renewalTime, result); 106 } 107 break; 108 default: 109 throw new IllegalArgumentException( 110 "Unsupported RFS image cache maintenance request: " + request.getOperation()); 111 } 112 return result.build(Duration.ofNanos(System.nanoTime() - start)); 113 } 114 115 /** 116 * @see org.opencms.loader.imagecache.I_CmsImageCacheMaintenance#getBackendId() 117 */ 118 @Override 119 public String getBackendId() { 120 121 return "rfs"; 122 } 123 124 /** 125 * @see org.opencms.loader.imagecache.I_CmsImageCacheMaintenance#getCapabilities() 126 */ 127 @Override 128 public CmsImageCacheCapabilities getCapabilities() { 129 130 return CAPABILITIES; 131 } 132 133 /** 134 * @see org.opencms.loader.imagecache.I_CmsImageCacheMaintenance#getEntry(java.lang.String) 135 */ 136 @Override 137 public CmsImageCacheEntry getEntry(String key) throws Exception { 138 139 Path path = toPath(key); 140 BasicFileAttributes attributes; 141 try { 142 attributes = Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); 143 } catch (java.nio.file.NoSuchFileException e) { 144 return null; 145 } 146 if (!attributes.isRegularFile()) { 147 return null; 148 } 149 return new CmsImageCacheEntry( 150 toKey(path), 151 attributes.size(), 152 attributes.lastModifiedTime().toInstant(), 153 attributes.lastModifiedTime().toMillis() + ":" + attributes.size()); 154 } 155 156 /** 157 * @see org.opencms.loader.imagecache.I_CmsImageCacheMaintenance#visitEntries(org.opencms.loader.imagecache.I_CmsImageCacheMaintenanceEntryVisitor) 158 */ 159 @Override 160 public void visitEntries(I_CmsImageCacheMaintenanceEntryVisitor visitor) throws Exception { 161 162 try (Stream<Path> paths = Files.walk(m_repository)) { 163 Iterator<Path> iterator = paths.filter( 164 path -> Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)).iterator(); 165 while (iterator.hasNext()) { 166 Path path = iterator.next(); 167 BasicFileAttributes attributes = Files.readAttributes( 168 path, 169 BasicFileAttributes.class, 170 LinkOption.NOFOLLOW_LINKS); 171 String key = toKey(path); 172 String revision = attributes.lastModifiedTime().toMillis() + ":" + attributes.size(); 173 visitor.visit( 174 new CmsImageCacheEntry( 175 key, 176 attributes.size(), 177 attributes.lastModifiedTime().toInstant(), 178 revision)); 179 } 180 } 181 } 182 183 /** 184 * Called after an entry has been deleted.<p> 185 * 186 * @param key the deleted cache key 187 * @throws Exception if post-processing fails 188 */ 189 protected void afterDelete(String key) throws Exception { 190 191 // default no-op 192 } 193 194 /** 195 * Returns the normalized repository path.<p> 196 * 197 * @return the repository path 198 */ 199 protected Path getRepository() { 200 201 return m_repository; 202 } 203 204 /** Removes empty repository subdirectories. */ 205 protected void removeEmptyDirectories() throws Exception { 206 207 try (Stream<Path> paths = Files.walk(m_repository)) { 208 Iterator<Path> iterator = paths.filter(path -> !m_repository.equals(path)).sorted( 209 java.util.Comparator.reverseOrder()).iterator(); 210 while (iterator.hasNext()) { 211 Path path = iterator.next(); 212 if (Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) { 213 try { 214 Files.delete(path); 215 } catch (DirectoryNotEmptyException e) { 216 // expected for directories which still contain cache entries 217 } 218 } 219 } 220 } 221 } 222 223 /** Returns the normalized cache path for a key. */ 224 protected Path toPath(String key) { 225 226 String normalizedKey = key; 227 while (normalizedKey.startsWith("/")) { 228 normalizedKey = normalizedKey.substring(1); 229 } 230 Path result = m_repository.resolve(normalizedKey).normalize(); 231 if (!result.startsWith(m_repository) || result.equals(m_repository)) { 232 throw new IllegalArgumentException("Image cache key outside repository: " + key); 233 } 234 return result; 235 } 236 237 /** Deletes a selected entry. */ 238 private void delete(CmsImageCacheEntry entry, CmsImageCacheMaintenanceResult.Builder result) { 239 240 try { 241 if (Files.deleteIfExists(toPath(entry.getKey()))) { 242 afterDelete(entry.getKey()); 243 result.addSuccess(); 244 } else { 245 result.addSkipped(); 246 } 247 } catch (Exception e) { 248 result.addFailure(entry.getKey(), e); 249 } 250 } 251 252 /** Renews a selected entry. */ 253 private void renew(CmsImageCacheEntry entry, FileTime renewalTime, CmsImageCacheMaintenanceResult.Builder result) { 254 255 try { 256 Path path = toPath(entry.getKey()); 257 if (!Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) { 258 result.addSkipped(); 259 return; 260 } 261 Files.setLastModifiedTime(path, renewalTime); 262 result.addSuccess(); 263 } catch (Exception e) { 264 result.addFailure(entry.getKey(), e); 265 } 266 } 267 268 /** Returns a portable cache key for a repository path. */ 269 private String toKey(Path path) { 270 271 return m_repository.relativize(path).toString().replace(path.getFileSystem().getSeparator(), "/"); 272 } 273}