Skip to content

Commit

Permalink
feat(content cleanup) : Automatically prune File Asset versions excee…
Browse files Browse the repository at this point in the history
…ding a configurable limit #26188 (#26350)

* fix(content cleanup) : Automatically prune File Asset versions exceeding a configurable limit #26188

* Implementing SonarQube feedback.

* Implementing SonarQube feedback.

* Fixing test class name.
  • Loading branch information
jcastro-dotcms committed Oct 6, 2023
1 parent 457dccf commit 64ac9bd
Show file tree
Hide file tree
Showing 13 changed files with 736 additions and 162 deletions.
9 changes: 4 additions & 5 deletions dotCMS/src/integration-test/java/com/dotcms/MainSuite.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@
import com.dotcms.graphql.DotGraphQLHttpServletTest;
import com.dotcms.integritycheckers.ContentFileAssetIntegrityCheckerTest;
import com.dotcms.integritycheckers.ContentPageIntegrityCheckerTest;
import com.dotcms.integritycheckers.HostIntegrityCheckerTest;
import com.dotcms.integritycheckers.FolderIntegrityCheckerTest;
import com.dotcms.integritycheckers.HostIntegrityCheckerTest;
import com.dotcms.integritycheckers.IntegrityUtilTest;
import com.dotcms.junit.MainBaseSuite;
import com.dotcms.mail.MailAPIImplTest;
Expand Down Expand Up @@ -152,6 +152,7 @@
import com.dotmarketing.portlets.workflows.model.TestWorkflowAction;
import com.dotmarketing.quartz.DotStatefulJobTest;
import com.dotmarketing.quartz.job.CleanUpFieldReferencesJobTest;
import com.dotmarketing.quartz.job.DropOldContentVersionsJobTest;
import com.dotmarketing.quartz.job.IntegrityDataGenerationJobTest;
import com.dotmarketing.quartz.job.PopulateContentletAsJSONJobTest;
import com.dotmarketing.quartz.job.StartEndScheduledExperimentsJobTest;
Expand Down Expand Up @@ -206,8 +207,8 @@
import com.dotmarketing.startup.runonce.Task230110MakeSomeSystemFieldsRemovableByBaseTypeTest;
import com.dotmarketing.startup.runonce.Task230328AddMarkedForDeletionColumnTest;
import com.dotmarketing.startup.runonce.Task230426AlterVarcharLengthOfLockedByColTest;
import com.dotmarketing.startup.runonce.Task230701AddHashIndicesToWorkflowTablesTest;
import com.dotmarketing.startup.runonce.Task230523CreateVariantFieldInContentletIntegrationTest;
import com.dotmarketing.startup.runonce.Task230701AddHashIndicesToWorkflowTablesTest;
import com.dotmarketing.startup.runonce.Task230713IncreaseDisabledWysiwygColumnSizeTest;
import com.dotmarketing.util.HashBuilderTest;
import com.dotmarketing.util.MaintenanceUtilTest;
Expand All @@ -224,9 +225,6 @@
/* grep -l -r "@Test" dotCMS/src/integration-test */
/* ./gradlew integrationTest -Dtest.single=com.dotcms.MainSuite */




@RunWith(MainBaseSuite.class)
@SuiteClasses({
StartEndScheduledExperimentsJobTest.class,
Expand Down Expand Up @@ -654,6 +652,7 @@
Task230701AddHashIndicesToWorkflowTablesTest.class,
Task230713IncreaseDisabledWysiwygColumnSizeTest.class,
BundleFactoryImplTest.class,
DropOldContentVersionsJobTest.class
})

public class MainSuite {
Expand Down
10 changes: 6 additions & 4 deletions dotCMS/src/integration-test/java/com/dotcms/MainSuite2b.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
import com.dotcms.rendering.velocity.viewtools.content.StoryBlockTest;
import com.dotcms.rest.BundlePublisherResourceIntegrationTest;
import com.dotcms.rest.IntegrityResourceIntegrationTest;
import com.dotcms.rest.api.v1.apps.SiteViewPaginatorIntegrationTest;
import com.dotcms.rest.api.v1.apps.view.AppsInterpolationTest;
import com.dotcms.rest.api.v1.asset.AssetPathResolverImplIntegrationTest;
import com.dotcms.rest.api.v1.asset.WebAssetHelperIntegrationTest;
Expand Down Expand Up @@ -95,6 +96,7 @@
import com.dotmarketing.portlets.workflows.actionlet.SaveContentAsDraftActionletIntegrationTest;
import com.dotmarketing.portlets.workflows.actionlet.VelocityScriptActionletAbortTest;
import com.dotmarketing.quartz.DotStatefulJobTest;
import com.dotmarketing.quartz.job.DropOldContentVersionsJobTest;
import com.dotmarketing.quartz.job.IntegrityDataGenerationJobTest;
import com.dotmarketing.quartz.job.PopulateContentletAsJSONJobTest;
import com.dotmarketing.startup.StartupTasksExecutorDataTest;
Expand Down Expand Up @@ -141,16 +143,15 @@
import com.dotmarketing.startup.runonce.Task230110MakeSomeSystemFieldsRemovableByBaseTypeTest;
import com.dotmarketing.startup.runonce.Task230328AddMarkedForDeletionColumnTest;
import com.dotmarketing.startup.runonce.Task230426AlterVarcharLengthOfLockedByColTest;
import com.dotmarketing.startup.runonce.Task230707CreateSystemTableTest;
import com.dotmarketing.startup.runonce.Task230523CreateVariantFieldInContentletIntegrationTest;
import com.dotmarketing.startup.runonce.Task230701AddHashIndicesToWorkflowTablesTest;
import com.dotmarketing.startup.runonce.Task230707CreateSystemTableTest;
import com.dotmarketing.startup.runonce.Task230713IncreaseDisabledWysiwygColumnSizeTest;
import com.dotmarketing.startup.runonce.Task230523CreateVariantFieldInContentletIntegrationTest;
import com.dotmarketing.util.MaintenanceUtilTest;
import com.dotmarketing.util.ResourceCollectorUtilTest;
import com.dotmarketing.util.UtilMethodsITest;
import com.dotmarketing.util.contentlet.pagination.PaginatedContentletsIntegrationTest;
import org.apache.velocity.tools.view.tools.CookieToolTest;
import com.dotcms.rest.api.v1.apps.SiteViewPaginatorIntegrationTest;
import org.junit.runner.RunWith;
import org.junit.runners.Suite.SuiteClasses;

Expand Down Expand Up @@ -313,7 +314,8 @@
IndexRegexUrlPatterStrategyIntegrationTest.class,
RootIndexRegexUrlPatterStrategyIntegrationTest.class,
SiteViewPaginatorIntegrationTest.class,
Task230523CreateVariantFieldInContentletIntegrationTest.class
Task230523CreateVariantFieldInContentletIntegrationTest.class,
DropOldContentVersionsJobTest.class
})

public class MainSuite2b {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package com.dotmarketing.quartz.job;

import com.dotcms.contenttype.model.type.ContentType;
import com.dotcms.datagen.ContentletDataGen;
import com.dotcms.datagen.TestDataUtils;
import com.dotcms.util.IntegrationTestInitService;
import com.dotmarketing.business.APILocator;
import com.dotmarketing.business.Versionable;
import com.dotmarketing.business.VersionableAPI;
import com.dotmarketing.exception.DotDataException;
import com.dotmarketing.exception.DotSecurityException;
import com.dotmarketing.portlets.contentlet.business.ContentletAPI;
import com.dotmarketing.portlets.contentlet.model.Contentlet;
import com.dotmarketing.util.Config;
import com.dotmarketing.util.DateUtil;
import com.liferay.portal.model.User;
import org.jetbrains.annotations.NotNull;
import org.junit.BeforeClass;
import org.junit.Test;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.List;

import static org.junit.Assert.assertEquals;

/**
* Verifies that the {@link DropOldContentVersionsJob} Quartz Job is working as expected.
*
* @author Jose Castro
* @since Oct 2nd, 2023
*/
public class DropOldContentVersionsJobTest {

private static User SYSTEM_USER = null;
private static ContentletDataGen contentletDataGen = null;
private final VersionableAPI versionableAPI = APILocator.getVersionableAPI();
private final ContentletAPI contentletAPI = APILocator.getContentletAPI();

@BeforeClass
public static void prepare() throws Exception {
// Setting up the web app environment
IntegrationTestInitService.getInstance().init();
final ContentType webPageContentContentType =
APILocator.getContentTypeAPI(APILocator.systemUser()).find("webPageContent");
contentletDataGen = new ContentletDataGen(webPageContentContentType.id());
SYSTEM_USER = APILocator.getUserAPI().getSystemUser();
}

/**
* <ul>
* <li><b>Method to Test:</b>
* {@link DropOldContentVersionsJob#execute(JobExecutionContext)}</li>
* <li><b>Given Scenario:</b> Create 4 versions of a 2-year-old piece of Content, and
* create 105 versions of another one-year-old Content so that the
* Drop Old Content Versions Job can analyze them. The default rules are: Dropping
* versions OLDER than 365 days, and keeping NO MORE than 100 versions per language</li>
* <li><b>Expected Result:</b> For the two-year-old Contentlet, there will only be 1
* version as it is the Published version. For the one-year-old Contentlet, 5 versions will
* be deleted so that only 100 versions are kept.</li>
* </ul>
*/
@Test
public void deleteOldContentlets() throws DotDataException, DotSecurityException,
JobExecutionException {
final int olderThan =
Config.getIntProperty(DropOldContentVersionsJob.OLDER_THAN_DAYS_PROP, 365);
final Date oneYearAgo = localDateToDate(olderThan);
final Date twoYearsAgo = localDateToDate(olderThan * 2);

Contentlet veryOldContentlet = TestDataUtils.getGenericContentContent(false, 1);
// Create first version in English from two years ago
veryOldContentlet = createContentlet(veryOldContentlet, "VERY Old Contentlet", twoYearsAgo);
// Create three more versions
veryOldContentlet = createContentletVersions(veryOldContentlet, "VERY Old Contentlet", twoYearsAgo, 2, 4);
contentletAPI.unlock(veryOldContentlet, SYSTEM_USER, false);
ContentletDataGen.publish(veryOldContentlet);

Contentlet contentWithManyVersions = TestDataUtils.getGenericContentContent(false, 1);
// Create first version in English form one year ago
contentWithManyVersions = createContentlet(contentWithManyVersions, "Old Contentlet Version", oneYearAgo);
// Create 100 more versions
contentWithManyVersions = createContentletVersions(contentWithManyVersions, "Old Contentlet Version", oneYearAgo, 2, 101);
// Create three more versions with the current date
contentWithManyVersions = createContentletVersions(contentWithManyVersions, "Old Contentlet Version", new Date(), 102, 105);
contentletAPI.unlock(contentWithManyVersions, SYSTEM_USER, false);
ContentletDataGen.publish(contentWithManyVersions);

final DropOldContentVersionsJob oldContentVersionsJob = new DropOldContentVersionsJob();
oldContentVersionsJob.execute(null);

// The two-year-old Contentlet should have all of its versions removed, except for the
// published one
final List<Versionable> allVersions =
versionableAPI.findAllVersions(veryOldContentlet.getIdentifier());
// The one-year-old Contentlet should have 5 versions removed, so that only 100 are kept
final List<Versionable> contentletVersions =
versionableAPI.findAllVersions(contentWithManyVersions.getIdentifier());

assertEquals("There should only be 1 version of the two-year-old Contentlet!" + allVersions.size(), 1, allVersions.size());
assertEquals("There must be only 100 versions of the one-year-old Contentlet!" + contentletVersions.size(), 100, contentletVersions.size());

}

/**
* Initial creation of a test Contentlet.
*/
private Contentlet createContentlet(final Contentlet contentlet, final String title,
final Date modDate) {
contentlet.setProperty("title", title);
contentlet.setProperty("_use_mod_date", modDate);
contentlet.setModDate(modDate);
return contentletDataGen.persist(contentlet);
}

/**
* Creates several versions of the specified Contentlet. If {@code startIndex} equals 1 and
* {@code endIndex} equals 3, then 3 versions will be created with each index value appended to
* the title.
*/
@NotNull
private Contentlet createContentletVersions(Contentlet contentlet, String title, final Date modDate,
final int startIndex, final int endIndex) throws DotDataException, DotSecurityException {
if (null == title) {
title = contentlet.getTitle();
}
for (int i = startIndex; i <= endIndex; i++) {
contentlet = contentletAPI.checkout(contentlet.getInode(), SYSTEM_USER, false);
contentlet.setProperty("title", title + " " + i);
contentlet.setProperty("_use_mod_date", modDate);
contentlet.setModDate(modDate);
contentlet = contentletDataGen.persist(contentlet);
}
return contentlet;
}

/**
* Utility method used to get the current date minus a specific number of days.
*/
private Date localDateToDate(final int olderThan) {
final LocalDate currentDate = LocalDate.now(ZoneId.of(DateUtil.UTC));
final LocalDate localDate = currentDate.minusDays(olderThan);
// Convert LocalDate to LocalDateTime by adding midnight time (00:00:00)
final LocalDateTime localDateTime = localDate.atStartOfDay();
final Instant instant = localDateTime.atZone(ZoneId.of(DateUtil.UTC)).toInstant();
return Date.from(instant);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,15 @@
import com.dotmarketing.portlets.structure.model.Field;
import com.dotmarketing.portlets.structure.model.Structure;
import com.dotmarketing.portlets.workflows.business.WorkFlowFactory;
import com.dotmarketing.util.*;
import com.dotmarketing.util.Config;
import com.dotmarketing.util.InodeUtils;
import com.dotmarketing.util.Logger;
import com.dotmarketing.util.NumberUtil;
import com.dotmarketing.util.PaginatedArrayList;
import com.dotmarketing.util.RegEX;
import com.dotmarketing.util.RegExMatch;
import com.dotmarketing.util.UUIDGenerator;
import com.dotmarketing.util.UtilMethods;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.annotations.VisibleForTesting;
Expand All @@ -68,7 +76,6 @@
import com.google.common.primitives.Ints;
import com.liferay.portal.model.User;
import io.vavr.control.Try;
import java.util.Collection;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
Expand Down Expand Up @@ -107,6 +114,7 @@
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
Expand Down Expand Up @@ -729,29 +737,36 @@ protected int deleteOldContent(final Date deleteFrom) throws DotDataException {
}

/**
* Deletes the content data associated to the specified list of Inodes. Based on such a list, the {@code contentlet}
* will be cleaned up as well.
* Deletes the Contentlets - versions - that match the specified list of Inodes. To improve
* performance, the list of Inodes is split into smaller lists of 100 elements each.
*
* @param inodeList The list of Inodes that will be deleted.
*
* @throws DotDataException An error occurred when interacting with the data source.
*/
private void deleteContentData(final List<String> inodeList) throws DotDataException {
if (UtilMethods.isNotSet(inodeList)) {
return;
}
final int splitAt = 100;
// Split all records into lists of size 'truncateAt'
final List<List<String>> inodesToDelete = Lists.partition(inodeList, splitAt);
final List<String> queries = Lists.newArrayList("DELETE FROM contentlet WHERE inode IN (?)",
"DELETE FROM inode WHERE inode IN (?)");
Logger.debug(this, String.format("Deleting %d Contentlets with the following Inodes:",
inodeList.size()));
for (final String query : queries) {
for (final List<String> inodes : inodesToDelete) {
final DotConnect dc = new DotConnect();
// Generate the "(?,?,?...)" string depending of the number of inodes
// Generate the "(?,?,?...)" string depending on the number of inodes
final String parameterPlaceholders = DotConnect.createParametersPlaceholder(inodes.size());
dc.setSQL(query.replace("?", parameterPlaceholders));
for (final String inode : inodes) {
dc.addParam(inode);
Logger.debug(this, "-> " + inode);
}
dc.loadResult();
Logger.debug(this, String.format("%d Inodes have been deleted!", inodes.size()));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
package com.dotcms.content.model.hydration;

import static com.dotcms.content.model.hydration.HydrationUtils.findLinkedBinary;
import static com.dotcms.storage.FileMetadataAPI.DEFAULT_METADATA_GROUP_NAME;
import static com.dotcms.storage.StoragePersistenceProvider.METADATA_GROUP_NAME;
import static com.dotcms.util.ReflectionUtils.setValue;

import com.dotcms.content.model.FieldValueBuilder;
import com.dotcms.contenttype.model.field.BinaryField;
import com.dotcms.contenttype.model.field.Field;
import com.dotcms.contenttype.model.field.ImageField;
import com.dotcms.exception.ExceptionUtil;
import com.dotcms.storage.FileMetadataAPI;
import com.dotcms.storage.FileStorageAPI;
import com.dotcms.storage.GenerateMetadataConfig;
Expand All @@ -25,6 +21,7 @@
import com.dotmarketing.util.Logger;
import com.google.common.annotations.VisibleForTesting;
import io.vavr.control.Try;

import java.io.File;
import java.io.Serializable;
import java.nio.file.Path;
Expand All @@ -36,6 +33,11 @@
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static com.dotcms.content.model.hydration.HydrationUtils.findLinkedBinary;
import static com.dotcms.storage.FileMetadataAPI.DEFAULT_METADATA_GROUP_NAME;
import static com.dotcms.storage.StoragePersistenceProvider.METADATA_GROUP_NAME;
import static com.dotcms.util.ReflectionUtils.setValue;

/**
* Little reusable component meant to populate a field in the FieldValue Object through the FieldValue's Builder
*/
Expand Down Expand Up @@ -88,8 +90,10 @@ private Map<String, Object> getMetadataMap(final Field field, final Contentlet c
}
}
}
} catch (Throwable e) {
Logger.warnAndDebug(MetadataDelegate.class, "error calculating metadata ", e);
} catch (final Throwable e) {
Logger.warnAndDebug(MetadataDelegate.class, String.format("Error calculating metadata" +
" for field '%s' [ %s ] in Contentlet '%s': %s", field.variable(), field.id()
, contentlet.getIdentifier(), ExceptionUtil.getErrorMessage(e)), e);
}
return (metadataMap == null || metadataMap.isEmpty() ? null : filterMetadataFields(metadataMap));
}
Expand Down
Loading

0 comments on commit 64ac9bd

Please sign in to comment.