AEM Cloud Service - Assets server side check for Duplicate file names

Goal

AEM Cloud Version : 2021.2.4944.20210221T230729Z-210225 (Feb 21, 2021)

The client side duplicate check post provides a helpful error message when user tries to upload duplicate files using a browser, however it does not protect when user uploads files using other means, say AEM Desktop app. This post provides a server side way of handling duplicate file names

Demo | Package Install | Github


Error uploading duplicate in Browser



Error uploading duplicate in Desktop App


Solution

1) Add a service user eaem-service-user in repo init script ui.config\src\main\content\jcr_root\apps\eaem-cs-server-duplicate-check\osgiconfig\config.author\org.apache.sling.jcr.repoinit.RepositoryInitializer-eaem.config

scripts=[
        "
        create service user eaem-service-user with path system/cq:services/experience-aem
        set principal ACL for eaem-service-user
                allow jcr:read on /apps
                allow jcr:read on /conf
                allow jcr:read on /content/dam
        end
        "
]


2) Provide the service user to bundle mapping in ui.config\src\main\content\jcr_root\apps\eaem-cs-server-duplicate-check\osgiconfig\config.author\org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended-ea.xml

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root
        xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
        xmlns:jcr="http://www.jcp.org/jcr/1.0"
        jcr:primaryType="sling:OsgiConfig"
        user.mapping="[eaem-cs-server-duplicate-check.core:eaem-service-user=[eaem-service-user]]"/>


3) Add a filter apps.experienceaem.assets.core.filters.DuplicateAssetNameCheck executing for .initiateUpload.json and .createasset.html requests to check for duplicate file names across the repo

package apps.experienceaem.assets.core.filters;

import com.day.cq.search.PredicateGroup;
import com.day.cq.search.Query;
import com.day.cq.search.QueryBuilder;
import com.day.cq.search.result.Hit;
import com.day.cq.search.result.SearchResult;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.request.RequestParameter;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.servlet.*;

@Component(
        service = Filter.class,
        immediate = true,
        name = "Experience AEM DAM server side duplicate file name check",
        property = {
                Constants.SERVICE_RANKING + ":Integer=-99",
                "sling.filter.scope=COMPONENT",
                "sling.filter.pattern=((.*.initiateUpload.json)|(.*.createasset.html))",
        }
)
public class DuplicateAssetNameCheck implements Filter {
    private static Logger log = LoggerFactory.getLogger(DuplicateAssetNameCheck.class);

    private static String INITIATE_UPLOAD_JSON = ".initiateUpload.json";
    private static String CREATE_ASSET_HTML = ".createasset.html";
    private static final String EAEM_SERVICE_USER = "eaem-service-user";

    @Reference
    private ResourceResolverFactory factory;

    @Reference
    private QueryBuilder builder;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        try{
            SlingHttpServletRequest slingRequest = (SlingHttpServletRequest)request;
            SlingHttpServletResponse slingResponse = (SlingHttpServletResponse)response;

            String uri = slingRequest.getRequestURI();

            if(!uri.endsWith(INITIATE_UPLOAD_JSON) && !uri.endsWith(CREATE_ASSET_HTML)){
                chain.doFilter(request, response);
                return;
            }

            /*String userAgent = slingRequest.getHeader("User-Agent");

            if(isBrowser(userAgent)){
                //duplicate filename check in browsers is already done on client side
                log.info("A Browser User agent : " + userAgent);
                chain.doFilter(request, response);
                return;
            }*/

            String fileNames[] = request.getParameterValues("fileName");

            if(fileNames == null){
                RequestParameter params[] = slingRequest.getRequestParameters("file");

                if(ArrayUtils.isEmpty(params)){
                    log.warn("Skipping duplicate check, 'fileName' and 'file' params, both are empty");
                    chain.doFilter(request, response);
                    return;
                }

                for (final RequestParameter param : params) {
                    if (param.getFileName() == null) {
                        continue;
                    }

                    fileNames = new String[1];
                    fileNames[0] = param.getFileName();

                    break;
                }
            }

            if(ArrayUtils.isEmpty(fileNames)){
                log.warn("Skipping duplicate check, 'fileName' and 'file' params, both are empty");
                chain.doFilter(request, response);
                return;
            }

            List<String> duplicatePaths = getDuplicateFilePaths(factory, builder, fileNames);

            log.info("duplicatePaths : " + duplicatePaths + ", for file : " +  String.join(",", fileNames));

            if(!CollectionUtils.isEmpty(duplicatePaths)){
                log.info("Duplicate file names detected while upload : " + duplicatePaths);
                slingResponse.sendError(SlingHttpServletResponse.SC_FORBIDDEN, "Duplicates found: " + String.join(",", duplicatePaths));
                return;
            }

            chain.doFilter(request, response);
        }catch(Exception e){
            log.error("Error checking for duplicates", e);
        }
    }

    public static List<String> getDuplicateFilePaths(ResourceResolverFactory resourceResolverFactory,
                                                     QueryBuilder builder, String fileNames[]) throws RepositoryException {
        ResourceResolver resourceResolver = getServiceResourceResolver(resourceResolverFactory);
        List<String> duplicates = new ArrayList<String>();

        Query query = builder.createQuery(PredicateGroup.create(getQueryPredicateMap(fileNames)), resourceResolver.adaptTo(Session.class));

        SearchResult result = query.getResult();

        for (Hit hit : result.getHits()) {
            duplicates.add(hit.getPath());
        }

        return duplicates;
    }

    private static Map<String, String> getQueryPredicateMap(String[] fileNames) {
        Map<String, String> map = new HashMap<>();
        map.put("path", "/content/dam");
        map.put("type", "dam:Asset");
        map.put("group.p.or", "true");

        for(int index = 0; index < fileNames.length; index++){
            map.put("group." + index + "_nodename", fileNames[index]);
        }

        return map;
    }

    public static ResourceResolver getServiceResourceResolver(ResourceResolverFactory resourceResolverFactory) {
        Map<String, Object> subServiceUser = new HashMap<>();
        subServiceUser.put(ResourceResolverFactory.SUBSERVICE, EAEM_SERVICE_USER);
        try {
            return resourceResolverFactory.getServiceResourceResolver(subServiceUser);
        } catch (LoginException ex) {
            log.error("Could not login as SubService user {}, exiting SearchService service.", EAEM_SERVICE_USER, ex);
            return null;
        }
    }

    private boolean isBrowser(String userAgent){
        return (userAgent.contains("Mozilla") || userAgent.contains("Chrome") || userAgent.contains("Safari"));
    }

    @Override
    public void destroy() {
    }
}


No comments:

Post a Comment