/** * Licensed to The Apereo Foundation under one or more contributor license * agreements. See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * * The Apereo Foundation licenses this file to you under the Educational * Community License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of the License * at: * * http://opensource.org/licenses/ecl2.txt * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. * */ package org.opencastproject.migration; import static org.apache.commons.lang3.exception.ExceptionUtils.getMessage; import static org.opencastproject.assetmanager.api.fn.Enrichments.enrich; import static org.opencastproject.util.OsgiUtil.getOptContextProperty; import static org.opencastproject.util.PathSupport.path; import org.opencastproject.assetmanager.api.AssetManager; import org.opencastproject.assetmanager.api.query.AQueryBuilder; import org.opencastproject.assetmanager.api.query.AResult; import org.opencastproject.mediapackage.MediaPackage; import org.opencastproject.mediapackage.MediaPackageElement; import org.opencastproject.search.api.SearchQuery; import org.opencastproject.search.api.SearchResult; import org.opencastproject.search.api.SearchResultItem; import org.opencastproject.search.impl.SearchServiceImpl; import org.opencastproject.search.impl.persistence.SearchServiceDatabase; import org.opencastproject.security.api.AccessControlList; import org.opencastproject.security.api.AuthorizationService; import org.opencastproject.security.api.Organization; import org.opencastproject.security.api.OrganizationDirectoryService; import org.opencastproject.security.api.SecurityService; import org.opencastproject.security.util.SecurityUtil; import org.opencastproject.util.FileSupport; import org.opencastproject.util.UrlSupport; import org.opencastproject.util.data.Effect0; import org.opencastproject.util.data.Function0; import com.entwinemedia.fn.data.Opt; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; import org.osgi.service.component.ComponentContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; import java.net.URI; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * This class provides migration index and DB migrations to Matterhorn. */ public class DistributionMigrationService { private static final Logger logger = LoggerFactory.getLogger(DistributionMigrationService.class); /** The security service */ private SecurityService securityService; /** The organization directory service */ private OrganizationDirectoryService organizationDirectoryService; /** The authorization service */ private AuthorizationService authorizationService; /** The search service */ private SearchServiceImpl searchService; /** The search database service */ private SearchServiceDatabase searchServiceDatabase; /** The asset manager */ private AssetManager assetManager; /** The component context */ private ComponentContext cc; /** OSGi DI callback. */ public void setSecurityService(SecurityService securityService) { this.securityService = securityService; } /** OSGi DI callback. */ public void setOrganizationDirectoryService(OrganizationDirectoryService organizationDirectoryService) { this.organizationDirectoryService = organizationDirectoryService; } /** OSGi DI callback. */ public void setAuthorizationService(AuthorizationService authorizationService) { this.authorizationService = authorizationService; } /** OSGi DI callback. */ public void setSearchService(SearchServiceImpl searchService) { this.searchService = searchService; } /** OSGi DI callback. */ public void setSearchServiceDatabase(SearchServiceDatabase searchServiceDatabase) { this.searchServiceDatabase = searchServiceDatabase; } /** OSGi DI callback. */ public void setAssetManager(AssetManager assetManager) { this.assetManager = assetManager; } public void activate(final ComponentContext cc) { this.cc = cc; logger.info("Start migration distribution artifacts to tenants"); Opt<String> downloadDirectoryPath = getOptContextProperty(cc, "org.opencastproject.download.directory").toOpt(); Opt<String> downloadUrl = getOptContextProperty(cc, "org.opencastproject.download.url").toOpt(); if (downloadDirectoryPath.isSome() && downloadUrl.isSome()) { migrateDistributionDirectory(downloadDirectoryPath.get(), downloadUrl.get(), "download", false); } else { logger.info("No download distribution directory {} and/or URL {} found to migrate, skip it!", downloadDirectoryPath, downloadUrl); } Opt<String> streamingDirectoryPath = getOptContextProperty(cc, "org.opencastproject.streaming.directory").toOpt(); Opt<String> streamingUrl = getOptContextProperty(cc, "org.opencastproject.streaming.url").toOpt(); if (streamingDirectoryPath.isSome() && streamingUrl.isSome()) { migrateDistributionDirectory(streamingDirectoryPath.get(), streamingUrl.get(), "streaming", true); } else { logger.info("No streaming distribution directory and/or URL found to migrate, skip it!", streamingDirectoryPath, streamingUrl); } logger.info("Finished migration distribution artifacts to tenants"); } private void migrateDistributionDirectory(final String distributionDirectoryPath, final String distributionUrl, String serviceName, final boolean isStreaming) { try { final File distributionDirectory = new File(distributionDirectoryPath); // Check for existing organization directories final List<Path> tenantPaths = new ArrayList<>(); final List<Organization> organizations = new ArrayList<>(); for (Organization org : organizationDirectoryService.getOrganizations()) { Path orgPath = new File(path(distributionDirectory.getAbsolutePath(), org.getId())).toPath(); if (Files.exists(orgPath)) { logger.info("Migrating '{}' distribution artifacts on tenant '{}' already done, skip migration!", serviceName, org); return; } else { // Create tenant directory according to organization directory tenantPaths.add(Files.createDirectory(orgPath)); organizations.add(org); logger.info("Found '{}' distribution artifacts for tenant '{}'!", serviceName, org); } } // move files Map<Organization, Set<String>> mediapackages = moveFiles(serviceName, distributionDirectory, tenantPaths, organizations); adjustDistributedSearchURL(serviceName, mediapackages, distributionUrl, isStreaming); } catch (Exception e) { logger.error("Unable to migrate {} distribution artifacts, aborting migration!", serviceName); } finally { securityService.setOrganization(null); securityService.setUser(null); } } private Map<Organization, Set<String>> moveFiles(String serviceName, final File distributionDirectory, final List<Path> tenantPaths, final List<Organization> organizations) throws IOException { logger.info("Start moving {} distribution files to new location", serviceName); final Integer[] errors = new Integer[1]; errors[0] = 0; final Integer[] sucess = new Integer[1]; sucess[0] = 0; // Loop over all mps in channel directories (filter out tenant dirs) final Map<Organization, Set<String>> mediapackages = new HashMap<>(); Files.walkFileTree(distributionDirectory.toPath(), new SimpleFileVisitor<Path>() { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { for (Path orgPath : tenantPaths) { if (Files.isSameFile(dir, orgPath)) return FileVisitResult.SKIP_SUBTREE; } return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFile(final Path file, BasicFileAttributes attrs) throws IOException { // Parse mediapackage identifier final String channelPath = file.toFile().getAbsolutePath() .substring(distributionDirectory.getAbsolutePath().length() + 1); String[] splitUrl = channelPath.split("/"); if (splitUrl.length < 2) { logger.info("Skip migrating {}", file); return FileVisitResult.CONTINUE; } final String mpId = splitUrl[1]; // Loop over all organizations and try to look up mediapackage in archive for (Organization org : organizations) { // if only one tenant, skip look up if (organizations.size() != 1 && !lookUpArchive(mpId, org)) continue; Set<String> ids = mediapackages.get(org); if (ids == null) ids = new HashSet<>(); ids.add(mpId); mediapackages.put(org, ids); File newPath = new File(path(distributionDirectory.getAbsolutePath(), org.getId(), channelPath)) .getParentFile(); logger.info("Try to move {} to new location {}", file, newPath); try { FileUtils.forceMkdir(newPath); FileSupport.move(file.toFile(), newPath); FileSupport.deleteHierarchyIfEmpty(distributionDirectory, file.toFile().getParentFile()); sucess[0]++; logger.info("Successfully moved file {}", file); } catch (IOException e) { errors[0]++; logger.error("Unable to move file {} to new location {}: {}", new Object[] { file, newPath, getMessage(e) }); } return FileVisitResult.CONTINUE; } logger.warn("No matching organization found for file {}, skip migration it", file); return FileVisitResult.CONTINUE; } }); logger.info( "Finished moving {} distribution files to new location. {} files moved. {} files couldn't be moved. Check logs for errror", new Object[] { serviceName, sucess[0], errors[0] }); return mediapackages; } private Boolean lookUpArchive(final String mpId, final Organization org) { return SecurityUtil.runAs(securityService, org, SecurityUtil.createSystemUser(cc, org), new Function0<Boolean>() { @Override public Boolean apply() { AQueryBuilder q = assetManager.createQuery(); final AResult r = q.select(q.snapshot()) .where(q.mediaPackageId(mpId).and(q.version().isLatest()).and(q.organizationId().eq(org.getId()))) .run(); if (enrich(r).getSize() > 0) { return true; } return false; } }); } private void adjustDistributedSearchURL(final String serviceName, final Map<Organization, Set<String>> mps, final String distributionUrl, final boolean isStreaming) { // Adjust distribution URLs in engage mediapackage for (final Organization org : mps.keySet()) { SecurityUtil.runAs(securityService, org, SecurityUtil.createSystemUser(cc, org), new Effect0() { @Override protected void run() { int sucess = 0; int errors = 0; int total = mps.get(org).size(); logger.info("Start adjusting {} distribution URL's on search service for tenant {}", serviceName, org); logger.info("{} mediapackages to adjust...", total); int i = 0; for (String mpId : mps.get(org)) { try { SearchResult result = searchService.getForAdministrativeRead(new SearchQuery().withId(mpId)); for (SearchResultItem item : result.getItems()) { MediaPackage mediaPackage = item.getMediaPackage(); boolean mediapackageChanged = false; // migration distribution URL's! for (MediaPackageElement e : mediaPackage.getElements()) { String uri = e.getURI().toString(); logger.debug("Looking to migrate '{}'", uri); if (uri.indexOf(org.getId()) > 0) { logger.debug("Mediapackage element {} has already been migrated", uri); continue; } if (uri.startsWith(distributionUrl)) { String path = uri.substring(distributionUrl.length()); URI newUri = URI.create(UrlSupport.concat(distributionUrl, org.getId(), path)); if (isStreaming) { String[] tag = StringUtils.split(path, ":"); if (tag.length > 1) { newUri = URI.create(UrlSupport.concat(distributionUrl, tag[0] + ":" + org.getId(), path.substring(tag[0].length() + 1))); } } e.setURI(newUri); mediapackageChanged = true; } } if (!mediapackageChanged) { sucess++; logger.info("Mediapackage {} ({}/{}) has already been migrated", new Object[] { mpId, i++, total }); continue; } // Write mediapackage to DB and update the index AccessControlList acl = authorizationService.getActiveAcl(mediaPackage).getA(); searchService.getSolrIndexManager().add(mediaPackage, acl, item.getModified()); searchServiceDatabase.storeMediaPackage(mediaPackage, acl, item.getModified()); sucess++; logger.info("Successfully migrated {} ({}/{})", new Object[] { mpId, i++, total }); } } catch (Exception e) { errors++; logger.info("Unable to migrate {} ({}/{}): {}", new Object[] { mpId, i++, total, getMessage(e) }); } } logger.info( "Finished adjusting {} distribution URL's on search service for tenant {}. {} entries migrated. {} entries couldn't be migrated. Check logs for errror", new Object[] { serviceName, org, sucess, errors }); } }); } } }