001/* 002 * (C) Copyright 2006-2016 Nuxeo SA (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * Thierry Delprat 018 * Florent Guillaume 019 */ 020package org.nuxeo.ecm.quota.size; 021 022import static org.nuxeo.ecm.core.api.LifeCycleConstants.DELETED_STATE; 023import static org.nuxeo.ecm.core.api.LifeCycleConstants.DELETE_TRANSITION; 024 025import java.io.Serializable; 026import java.util.Collection; 027import java.util.HashSet; 028import java.util.List; 029import java.util.Map; 030 031import org.apache.commons.logging.Log; 032import org.apache.commons.logging.LogFactory; 033import org.nuxeo.ecm.core.api.Blob; 034import org.nuxeo.ecm.core.api.CoreSession; 035import org.nuxeo.ecm.core.api.DocumentModel; 036import org.nuxeo.ecm.core.api.IdRef; 037import org.nuxeo.ecm.core.api.IterableQueryResult; 038import org.nuxeo.ecm.core.event.Event; 039import org.nuxeo.ecm.core.utils.BlobsExtractor; 040import org.nuxeo.ecm.quota.AbstractQuotaStatsUpdater; 041import org.nuxeo.ecm.quota.QuotaStatsInitialWork; 042import org.nuxeo.runtime.api.Framework; 043 044/** 045 * {@link org.nuxeo.ecm.quota.QuotaStatsUpdater} counting space used by Blobs in document. This implementation does not 046 * track the space used by non-Blob properties. 047 * 048 * @since 8.3 049 */ 050public class DocumentsSizeUpdater extends AbstractQuotaStatsUpdater { 051 052 private static Log log = LogFactory.getLog(DocumentsSizeUpdater.class); 053 054 public static final String DISABLE_QUOTA_CHECK_LISTENER = "disableQuotaListener"; 055 056 public static final String USER_WORKSPACES_ROOT = "UserWorkspacesRoot"; 057 058 @Override 059 public void computeInitialStatistics(CoreSession session, QuotaStatsInitialWork currentWorker) { 060 log.debug("Starting initial Quota computation"); 061 String query = "SELECT ecm:uuid FROM Document WHERE ecm:isCheckedInVersion = 0 AND ecm:isProxy = 0"; 062 063 // reset on all documents 064 // this will force an update if the quota addon was installed and then removed 065 long count = 0; 066 IterableQueryResult res = session.queryAndFetch(query, "NXQL"); 067 try { 068 count = res.size(); 069 log.debug("Start iteration on " + count + " items"); 070 for (Map<String, Serializable> r : res) { 071 String uuid = (String) r.get("ecm:uuid"); 072 DocumentModel doc = session.getDocument(new IdRef(uuid)); 073 QuotaAwareDocumentFactory.unmake(doc); 074 } 075 } finally { 076 res.close(); 077 } 078 QuotaAwareDocumentFactory.unmake(session.getRootDocument()); 079 session.save(); 080 081 // recompute quota on each doc 082 res = session.queryAndFetch(query, "NXQL"); 083 try { 084 long idx = 0; 085 for (Map<String, Serializable> r : res) { 086 String uuid = (String) r.get("ecm:uuid"); 087 DocumentModel doc = session.getDocument(new IdRef(uuid)); 088 if (log.isTraceEnabled()) { 089 log.trace("process Quota initial computation on uuid " + doc.getId()); 090 log.trace("doc with uuid " + doc.getId() + " started update"); 091 } 092 initDocument(session, doc); 093 if (log.isTraceEnabled()) { 094 log.trace("doc with uuid " + doc.getId() + " update completed"); 095 } 096 currentWorker.notifyProgress(++idx, count); 097 } 098 } finally { 099 res.close(); 100 } 101 } 102 103 protected void initDocument(CoreSession session, DocumentModel doc) { 104 boolean isDeleted = DELETED_STATE.equals(doc.getCurrentLifeCycleState()); 105 long size = getBlobsSize(doc); 106 long versionsSize = getVersionsSize(session, doc); 107 updateDocumentAndAncestors(session, doc, size, size + versionsSize, isDeleted ? size : 0, versionsSize); 108 } 109 110 @Override 111 protected void handleQuotaExceeded(QuotaExceededException e, Event event) { 112 String msg = "Current event " + event.getName() + " would break Quota restriction, rolling back"; 113 log.info(msg); 114 e.addInfo(msg); 115 event.markRollBack("Quota Exceeded", e); 116 } 117 118 @Override 119 protected void processDocumentCreated(CoreSession session, DocumentModel doc) { 120 if (doc.isVersion()) { 121 // version taken into account by checkout 122 return; 123 } 124 QuotaAware quotaDoc = doc.getAdapter(QuotaAware.class); 125 if (quotaDoc == null) { 126 // always add the quota facet 127 quotaDoc = QuotaAwareDocumentFactory.make(doc); 128 quotaDoc.save(); 129 } 130 long size = getBlobsSize(doc); 131 checkQuota(session, doc, size); 132 updateDocumentAndAncestors(session, doc, size, size, 0, 0); 133 } 134 135 @Override 136 protected void processDocumentCheckedIn(CoreSession session, DocumentModel doc) { 137 // on checkin the versions size is incremented (and also the total) 138 long size = getBlobsSize(doc); 139 // no constraints check as total size is not impacted 140 updateDocumentAndAncestors(session, doc, 0, size, 0, size); 141 } 142 143 @Override 144 protected void processDocumentCheckedOut(CoreSession session, DocumentModel doc) { 145 // on checkout we account in the total for the last version size 146 long size = getBlobsSize(doc); 147 checkQuota(session, doc, size); 148 // all quota computation handled on checkin 149 } 150 151 @Override 152 protected void processDocumentUpdated(CoreSession session, DocumentModel doc) { 153 // nothing to do, we do things at beforeDocumentModification time 154 } 155 156 @Override 157 protected void processDocumentBeforeUpdate(CoreSession session, DocumentModel doc) { 158 QuotaAware quotaDoc = doc.getAdapter(QuotaAware.class); 159 long oldSize = quotaDoc == null ? 0 : quotaDoc.getInnerSize(); 160 long delta = getBlobsSize(doc) - oldSize; 161 checkQuota(session, doc, delta); 162 updateDocument(doc, delta, delta, 0, 0, false); // DO NOT SAVE as this is a "before" event 163 updateAncestors(session, doc, delta, 0, 0); 164 } 165 166 @Override 167 protected void processDocumentCopied(CoreSession session, DocumentModel doc) { 168 QuotaAware quotaDoc = doc.getAdapter(QuotaAware.class); 169 if (quotaDoc == null) { 170 return; 171 } 172 long size = quotaDoc.getTotalSize() - quotaDoc.getVersionsSize() - quotaDoc.getTrashSize(); 173 checkQuota(session, doc, size); 174 if (!doc.isFolder() && size > 0) { 175 // when we copy some doc that is not folderish, we don't 176 // copy the versions so we can't rely on the copied quotaDocInfo 177 quotaDoc.resetInfos(); 178 quotaDoc.save(); 179 updateDocument(doc, size, size, 0, 0); 180 } 181 updateAncestors(session, doc, size, 0, 0); 182 } 183 184 @Override 185 protected void processDocumentMoved(CoreSession session, DocumentModel doc, DocumentModel sourceParent) { 186 QuotaAware quotaDoc = doc.getAdapter(QuotaAware.class); 187 long size = quotaDoc == null ? 0 : quotaDoc.getTotalSize(); 188 checkQuota(session, doc, size); 189 long versionsSize = quotaDoc.getVersionsSize(); 190 // add on new ancestors 191 updateAncestors(session, doc, size, 0, versionsSize); 192 // remove from old ancestors 193 if (sourceParent != null) { 194 updateDocumentAndAncestors(session, sourceParent, 0, -size, 0, -versionsSize); 195 } 196 } 197 198 @Override 199 protected void processDocumentAboutToBeRemoved(CoreSession session, DocumentModel doc) { 200 if (doc.isVersion()) { 201 // for versions we need to decrement the live doc + its parents 202 // We only have to decrement the inner size of this doc 203 // we do not write the right quota on the version, so we need to recompute it instead of 204 // quotaDoc#getInnerSize 205 long size = getBlobsSize(doc); 206 String sourceId = doc.getSourceId(); 207 if (size > 0 && sourceId != null) { 208 DocumentModel liveDoc = session.getDocument(new IdRef(sourceId)); 209 updateDocumentAndAncestors(session, liveDoc, 0, -size, 0, -size); 210 } 211 return; 212 } 213 214 QuotaAware quotaDoc = doc.getAdapter(QuotaAware.class); 215 long size; 216 long versionsSize; 217 if (quotaDoc == null) { 218 // the document could have been just created and the previous computation 219 // hasn't finished yet, see NXP-13665 220 size = getBlobsSize(doc); 221 versionsSize = 0; 222 if (log.isTraceEnabled()) { 223 log.trace( 224 "Document " + doc.getId() + " doesn't have the facet quotaDoc. Compute impacted size:" + size); 225 } 226 } else { 227 size = quotaDoc.getTotalSize(); 228 versionsSize = quotaDoc.getVersionsSize(); 229 if (log.isTraceEnabled()) { 230 log.trace("Found facet quotaDoc on document " + doc.getId() + ". Total size: " + size 231 + " and versions size: " + versionsSize); 232 } 233 } 234 // remove size for all its versions from sizeVersions on parents 235 boolean isDeleted = DELETED_STATE.equals(doc.getCurrentLifeCycleState()); 236 // when permanently deleting the doc clean the trash if the doc is in trash 237 // and all archived versions size 238 if (log.isTraceEnabled()) { 239 log.trace("Processing document about to be removed on parents. Total: " + size + " , trash size: " 240 + (isDeleted ? size : 0) + " , versions size: " + versionsSize); 241 } 242 long deltaTrash = isDeleted ? versionsSize - size : 0; 243 updateAncestors(session, doc, -size, deltaTrash, -versionsSize); 244 } 245 246 @Override 247 protected void processDocumentTrashOp(CoreSession session, DocumentModel doc, String transition) { 248 QuotaAware quotaDoc = doc.getAdapter(QuotaAware.class); 249 if (quotaDoc == null) { 250 return; 251 } 252 long size = quotaDoc.getInnerSize(); 253 if (log.isTraceEnabled()) { 254 if (quotaDoc.getDoc().isFolder()) { 255 log.trace(quotaDoc.getDoc().getPathAsString() + " is a folder, just inner size (" + size 256 + ") taken into account for trash size"); 257 } 258 } 259 boolean isDelete = DELETE_TRANSITION.equals(transition); // otherwise undelete 260 long delta = isDelete ? size : -size; 261 // constraints check not needed, since the documents stays in the same folder 262 updateDocumentAndAncestors(session, doc, 0, 0, delta, 0); 263 } 264 265 @Override 266 protected void processDocumentBeforeRestore(CoreSession session, DocumentModel doc) { 267 QuotaAware quotaDoc = doc.getAdapter(QuotaAware.class); 268 if (quotaDoc == null) { 269 return; 270 } 271 // remove versions size on parents since they will be recalculated on restore 272 long size = quotaDoc.getTotalSize(); 273 long versionsSize = quotaDoc.getVersionsSize(); 274 updateAncestors(session, doc, -size, 0, -versionsSize); 275 } 276 277 @Override 278 protected void processDocumentRestored(CoreSession session, DocumentModel doc) { 279 QuotaAware quotaDoc = QuotaAwareDocumentFactory.make(doc); 280 quotaDoc.resetInfos(); 281 quotaDoc.save(); 282 long size = getBlobsSize(doc); 283 long versionsSize = getVersionsSize(session, doc); 284 updateDocumentAndAncestors(session, doc, size, size + versionsSize, 0, versionsSize); 285 } 286 287 @Override 288 protected boolean needToProcessEventOnDocument(Event event, DocumentModel doc) { 289 if (doc == null) { 290 return false; 291 } 292 if (doc.isProxy()) { 293 return false; 294 } 295 if (Boolean.TRUE.equals(doc.getContextData(DISABLE_QUOTA_CHECK_LISTENER))) { 296 // avoid reentrancy 297 return false; 298 } 299 return true; 300 } 301 302 /** Checks the size delta against the maximum quota specified for this document or an ancestor. */ 303 protected void checkQuota(CoreSession session, DocumentModel doc, long delta) { 304 if (delta <= 0) { 305 return; 306 } 307 for (DocumentModel parent : getAncestors(session, doc)) { 308 if (log.isTraceEnabled()) { 309 log.trace("processing " + parent.getId() + " " + parent.getPathAsString()); 310 } 311 QuotaAware quotaDoc = parent.getAdapter(QuotaAware.class); 312 // when enabling quota on user workspaces, the max size set on the 313 // UserWorkspacesRoot is the max size set on every user workspace 314 if (quotaDoc == null || quotaDoc.getMaxQuota() <= 0 || USER_WORKSPACES_ROOT.equals(parent.getType())) { 315 continue; 316 } 317 if (quotaDoc.getTotalSize() + delta > quotaDoc.getMaxQuota()) { 318 log.info("Raising Quota Exception on " + doc.getId() + " (" + doc.getPathAsString() + ")"); 319 throw new QuotaExceededException(parent, doc, quotaDoc.getMaxQuota()); 320 } 321 } 322 } 323 324 /** Gets the sum of all blobs sizes for all the document's versions. */ 325 protected long getVersionsSize(CoreSession session, DocumentModel doc) { 326 long versionsSize = 0; 327 for (DocumentModel version : session.getVersions(doc.getRef())) { 328 versionsSize += getBlobsSize(version); 329 } 330 return versionsSize; 331 } 332 333 /** Gets the sum of all blobs sizes for this document. */ 334 protected long getBlobsSize(DocumentModel doc) { 335 long size = 0; 336 for (Blob blob : getAllBlobs(doc)) { 337 size += blob.getLength(); 338 } 339 return size; 340 } 341 342 /** Returns the list of blobs for this document. */ 343 protected List<Blob> getAllBlobs(DocumentModel doc) { 344 QuotaSizeService sizeService = Framework.getService(QuotaSizeService.class); 345 Collection<String> excludedPaths = sizeService.getExcludedPathList(); 346 BlobsExtractor extractor = new BlobsExtractor(); 347 extractor.setExtractorProperties(null, new HashSet<>(excludedPaths), true); 348 return extractor.getBlobs(doc); 349 } 350 351 protected void updateDocument(DocumentModel doc, long deltaInner, long deltaTotal, long deltaTrash, 352 long deltaVersions) { 353 updateDocument(doc, deltaInner, deltaTotal, deltaTrash, deltaVersions, true); 354 } 355 356 protected void updateDocument(DocumentModel doc, long deltaInner, long deltaTotal, long deltaTrash, 357 long deltaVersions, boolean allowSave) { 358 QuotaAware quotaDoc = doc.getAdapter(QuotaAware.class); 359 boolean save = false; 360 if (quotaDoc == null) { 361 if (log.isTraceEnabled()) { 362 log.trace(" add quota on: " + doc.getId() + " (" + doc.getPathAsString() + ")"); 363 } 364 quotaDoc = QuotaAwareDocumentFactory.make(doc); 365 save = true; 366 } else { 367 if (log.isTraceEnabled()) { 368 log.trace(" update quota on: " + doc.getId() + " (" + doc.getPathAsString() + ") (" + quotaDoc.getQuotaInfo() + ")"); 369 } 370 } 371 if (deltaInner != 0) { 372 quotaDoc.addInnerSize(deltaInner); 373 save = true; 374 } 375 if (deltaTotal != 0) { 376 quotaDoc.addTotalSize(deltaTotal); 377 save = true; 378 } 379 if (deltaTrash != 0) { 380 quotaDoc.addTrashSize(deltaTrash); 381 save = true; 382 } 383 if (deltaVersions != 0) { 384 quotaDoc.addVersionsSize(deltaVersions); 385 save = true; 386 } 387 if (save && allowSave) { 388 quotaDoc.save(); 389 } 390 if (log.isTraceEnabled()) { 391 log.trace(" ==> " + doc.getId() + " (" + doc.getPathAsString() + ") (" + quotaDoc.getQuotaInfo() + ")"); 392 } 393 } 394 395 protected void updateAncestors(CoreSession session, DocumentModel doc, long deltaTotal, long deltaTrash, 396 long deltaVersions) { 397 if (deltaTotal == 0 && deltaTrash == 0 && deltaVersions == 0) { 398 // avoids computing ancestors if there's no update to do 399 return; 400 } 401 List<DocumentModel> ancestors = getAncestors(session, doc); 402 for (DocumentModel ancestor : ancestors) { 403 updateDocument(ancestor, 0, deltaTotal, deltaTrash, deltaVersions); 404 } 405 } 406 407 protected void updateDocumentAndAncestors(CoreSession session, DocumentModel doc, long deltaInner, long deltaTotal, 408 long deltaTrash, long deltaVersions) { 409 updateDocument(doc, deltaInner, deltaTotal, deltaTrash, deltaVersions); 410 updateAncestors(session, doc, deltaTotal, deltaTrash, deltaVersions); 411 } 412 413}